From 028bbda4282515b95a7555209d397aaf22d32244 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:07:12 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 8 +++++++- Omni/Task/Core.hs | 26 +++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index d1e672a..10136c9 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -312,7 +312,13 @@ unitTests = ready <- getReadyTasks -- Both should be ready since Related doesn't block (taskId task1 `elem` map taskId ready) Test.@?= True - (taskId task2 `elem` map taskId ready) Test.@?= True + (taskId task2 `elem` map taskId ready) Test.@?= True, + Test.unit "child task gets sequential ID" <| do + parent <- createTask "Parent" Epic Nothing Nothing P2 [] + child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] + child2 <- createTask "Child 2" WorkTask (Just (taskId parent)) Nothing P2 [] + taskId child1 Test.@?= taskId parent <> ".1" + taskId child2 Test.@?= taskId parent <> ".2" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index f7b7915..798f8fe 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -111,6 +111,28 @@ generateId = do encoded = toBase62 (fromIntegral microseconds) pure <| "t-" <> T.pack encoded +-- Generate a child ID based on parent ID (e.g. "t-abc.1") +generateChildId :: Text -> IO Text +generateChildId parentId = do + tasks <- loadTasks + let children = filter (\t -> taskParent t == Just parentId) tasks + -- Find the max suffix + suffixes = mapMaybe (\t -> getSuffix parentId (taskId t)) children + nextSuffix = case suffixes of + [] -> 1 + s -> maximum s + 1 + pure <| parentId <> "." <> T.pack (show nextSuffix) + +getSuffix :: Text -> Text -> Maybe Int +getSuffix parent childId = + if parent `T.isPrefixOf` childId && T.length childId > T.length parent + then + let rest = T.drop (T.length parent) childId + in if T.head rest == '.' + then readMaybe (T.unpack (T.tail rest)) + else Nothing + else Nothing + -- Convert number to base62 (0-9, a-z, A-Z) toBase62 :: Integer -> String toBase62 0 = "0" @@ -192,7 +214,9 @@ saveTask task = do -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> IO Task createTask title taskType parent namespace priority deps = do - tid <- generateId + tid <- case parent of + Nothing -> generateId + Just pid -> generateChildId pid now <- getCurrentTime let task = Task -- cgit v1.2.3 From cb37c2632cf945c1993d8b338abb1ce35899d5de Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:15:16 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 7 ++++++- Omni/Task/Core.hs | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index 10136c9..9b89a1d 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -318,7 +318,12 @@ unitTests = child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] child2 <- createTask "Child 2" WorkTask (Just (taskId parent)) Nothing P2 [] taskId child1 Test.@?= taskId parent <> ".1" - taskId child2 Test.@?= taskId parent <> ".2" + taskId child2 Test.@?= taskId parent <> ".2", + Test.unit "grandchild task gets sequential ID" <| do + parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] + child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] + grandchild <- createTask "Grandchild" WorkTask (Just (taskId child)) Nothing P2 [] + taskId grandchild Test.@?= taskId parent <> ".1.1" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 798f8fe..31c0981 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -117,7 +117,7 @@ generateChildId parentId = do tasks <- loadTasks let children = filter (\t -> taskParent t == Just parentId) tasks -- Find the max suffix - suffixes = mapMaybe (\t -> getSuffix parentId (taskId t)) children + suffixes = mapMaybe (getSuffix parentId <. taskId) children nextSuffix = case suffixes of [] -> 1 s -> maximum s + 1 @@ -127,10 +127,10 @@ getSuffix :: Text -> Text -> Maybe Int getSuffix parent childId = if parent `T.isPrefixOf` childId && T.length childId > T.length parent then - let rest = T.drop (T.length parent) childId + let rest = T.drop (T.length parent) childId in if T.head rest == '.' - then readMaybe (T.unpack (T.tail rest)) - else Nothing + then readMaybe (T.unpack (T.tail rest)) + else Nothing else Nothing -- Convert number to base62 (0-9, a-z, A-Z) @@ -214,9 +214,7 @@ saveTask task = do -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> IO Task createTask title taskType parent namespace priority deps = do - tid <- case parent of - Nothing -> generateId - Just pid -> generateChildId pid + tid <- maybe generateId generateChildId parent now <- getCurrentTime let task = Task -- cgit v1.2.3 From 0d8ba7ec8b7b06a490eb7f2d625e169b2ed0ad72 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:20:09 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'Omni') diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 31c0981..54ed04d 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -111,7 +111,8 @@ generateId = do encoded = toBase62 (fromIntegral microseconds) pure <| "t-" <> T.pack encoded --- Generate a child ID based on parent ID (e.g. "t-abc.1") +-- Generate a child ID based on parent ID (e.g. "t-abc.1", "t-abc.1.2") +-- Finds the next available sequential suffix among existing children. generateChildId :: Text -> IO Text generateChildId parentId = do tasks <- loadTasks -- cgit v1.2.3 From feda8ac221d88650850a3eaac2fbe2f2b215beac Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:39:25 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'Omni') diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 54ed04d..525ceb4 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -13,7 +13,8 @@ import qualified Data.ByteString.Lazy.Char8 as BLC import qualified Data.List as List import qualified Data.Text as T import qualified Data.Text.IO as TIO -import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDayTime) +import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDayTime, utctDay) +import Data.Time.Calendar (toModifiedJulianDay) import GHC.Generics () import System.Directory (createDirectoryIfMissing, doesFileExist) import System.Environment (lookupEnv) @@ -104,11 +105,15 @@ initTaskDb = do generateId :: IO Text generateId = do now <- getCurrentTime - -- Convert current time to microseconds since midnight - let dayTime = utctDayTime now - microseconds = diffTimeToPicoseconds dayTime `div` 1000000 - -- Convert to base62 for shorter IDs - encoded = toBase62 (fromIntegral microseconds) + -- Convert current time to microseconds since epoch (using MJD) + let day = utctDay now + dayTime = utctDayTime now + mjd = toModifiedJulianDay day + micros = diffTimeToPicoseconds dayTime `div` 1000000 + -- Combine MJD and micros to ensure uniqueness across days. + -- Multiplier 10^11 (100,000 seconds) is safe for any day length. + totalMicros = (mjd * 100000000000) + micros + encoded = toBase62 totalMicros pure <| "t-" <> T.pack encoded -- Generate a child ID based on parent ID (e.g. "t-abc.1", "t-abc.1.2") -- cgit v1.2.3 From 809c0009da015588ffbf8c4221df04c13aacc215 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:06:41 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'Omni') diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 525ceb4..98ef6f9 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -121,9 +121,10 @@ generateId = do generateChildId :: Text -> IO Text generateChildId parentId = do tasks <- loadTasks - let children = filter (\t -> taskParent t == Just parentId) tasks - -- Find the max suffix - suffixes = mapMaybe (getSuffix parentId <. taskId) children + -- Find the max suffix among ALL tasks that look like children (to avoid ID collisions) + -- We check all tasks, not just those with taskParent set, because we want to ensure + -- ID uniqueness even if the parent link is missing. + let suffixes = mapMaybe (getSuffix parentId <. taskId) tasks nextSuffix = case suffixes of [] -> 1 s -> maximum s + 1 -- cgit v1.2.3 From 9a256f5c8232713ee99cb78093434a32c304e192 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:12:52 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index 9b89a1d..6624b31 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -323,7 +323,14 @@ unitTests = parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] grandchild <- createTask "Grandchild" WorkTask (Just (taskId child)) Nothing P2 [] - taskId grandchild Test.@?= taskId parent <> ".1.1" + taskId grandchild Test.@?= taskId parent <> ".1.1", + Test.unit "siblings of grandchild task get sequential ID" <| do + parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] + child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] + grandchild1 <- createTask "Grandchild 1" WorkTask (Just (taskId child)) Nothing P2 [] + grandchild2 <- createTask "Grandchild 2" WorkTask (Just (taskId child)) Nothing P2 [] + taskId grandchild1 Test.@?= taskId parent <> ".1.1" + taskId grandchild2 Test.@?= taskId parent <> ".1.2" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage -- cgit v1.2.3 From 946ebcfbb2b19e7469c9519c231736563a6b6fb6 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:15:41 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'Omni') diff --git a/Omni/Task.hs b/Omni/Task.hs index 6624b31..24e528b 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -330,7 +330,29 @@ unitTests = grandchild1 <- createTask "Grandchild 1" WorkTask (Just (taskId child)) Nothing P2 [] grandchild2 <- createTask "Grandchild 2" WorkTask (Just (taskId child)) Nothing P2 [] taskId grandchild1 Test.@?= taskId parent <> ".1.1" - taskId grandchild2 Test.@?= taskId parent <> ".1.2" + taskId grandchild2 Test.@?= taskId parent <> ".1.2", + Test.unit "child ID generation skips gaps" <| do + parent <- createTask "Parent with gaps" Epic Nothing Nothing P2 [] + child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] + -- Manually create a task with .3 suffix to simulate a gap (or deleted task) + let child3Id = taskId parent <> ".3" + child3 = Task + { taskId = child3Id, + taskTitle = "Child 3", + taskType = WorkTask, + taskParent = Just (taskId parent), + taskNamespace = Nothing, + taskStatus = Open, + taskPriority = P2, + taskDependencies = [], + taskCreatedAt = taskCreatedAt child1, + taskUpdatedAt = taskUpdatedAt child1 + } + saveTask child3 + + -- Create a new child, it should get .4, not .2 + child4 <- createTask "Child 4" WorkTask (Just (taskId parent)) Nothing P2 [] + taskId child4 Test.@?= taskId parent <> ".4" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage -- cgit v1.2.3