Mint task ids that are not already in use
generate-task-id takes no store argument and so cannot check anything. Four random hex over this estate's 1291 task lines predicts ~12 colliding ids; 14 exist. Collisions are the expected output of the design.
The two creation handlers now mint through mint-unique-id, which rejects a candidate already present anywhere a mutation can reach and retries. Move paths are untouched: they carry an existing id by design, and creation is the only place a NEW collision can be born.
That last point was measured rather than assumed. Neither inbox-add-schema nor task-add-schema exposes an id property, and build-metadata-from-args can only emit source/ref/priority/due/tags -- so a caller cannot supply an id at creation, and supplied-id is reachable only from (folio-task-metadata task) on a move. 0.2.7's honour-a-supplied-id change therefore cannot create a collision; it can only preserve one that already existed, where the old re-roll might have broken it by luck.
THE FIRST VERSION OF THE MINT TEST WAS FALSE-GREEN. It occupied 200 ids and minted 40 times, which a check-free mint survives about 89% of the time -- it passed under sabotage. It now occupies 4096 (6.25% of the space) and mints 120 times, which a check-free mint survives with probability 0.9375^120, about 0.04%, and it reddens under the same sabotage. A deterministic companion test covers find-all-tasks-by-id, the predicate the mint depends on, since that one cannot pass by luck.
src/folio/tools.sgl | 28 ++++++++++++++++++++++++++--
test/test-id-ambiguity.sgl | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+), 2 deletions(-)src/folio/tools.sglmodified
make-tool-inbox-sweep make-tool-task-complete-batch make-tool-inbox-triage make-tool-inbox-add make-tool-task-add make-tool-task-move find-all-tasks-by-id make-tool-task-move-batch move-task-in-store!) (begin ;; Inbox Handlers ;; ============================================================ ;; Mint a task id that is not already in use anywhere a mutation can reach. ;; ;; `generate-task-id` takes no store argument and therefore cannot check ;; anything. Four random hex over this estate's 1291 task lines predicts ;; ~12 colliding ids, and 14 exist -- collisions are the expected outcome ;; of the design, not a defect in it. ;; ;; Checking at mint time makes NEW collisions impossible. It does nothing ;; for the ones already on disk, which is why `refuse-if-ambiguous` is the ;; more important half: this protects the future, that protects the corpus. (define (mint-unique-id store) (let loop ((tries 0)) (let ((candidate (generate-task-id))) (cond ((null? (find-all-tasks-by-id store candidate)) candidate) ((> tries 200) (error "Could not mint an unused task id after 200 attempts; the id space may be exhausted")) (else (loop (+ tries 1))))))) (define (make-tool-inbox store) (lambda (args) (backfill-ids! (store-inbox-path store)) (define (make-tool-inbox-add store) (lambda (args) (let* ((text (require-arg args text: "text")) (metadata (build-metadata-from-args args))) (metadata (cons (cons 'id (mint-unique-id store)) (build-metadata-from-args args)))) (let ((task (inbox-add! store text metadata))) (format "Added to inbox: ~a ~a" (folio-task-id task) (lambda (args) (let* ((text (require-arg args text: "text")) (project-name (dict-ref args project: #f)) (metadata (build-metadata-from-args args))) (metadata (cons (cons 'id (mint-unique-id store)) (build-metadata-from-args args)))) (if project-name ;; Add to project (let ((project-file (find-project-file store project-name)))test/test-id-ambiguity.sglmodified
(sigil struct) (sigil string) (sigil dict) (sigil math) (sigil fs) (sigil path) (folio store) (let ((result (complete-task-in-store! store "t-dead"))) (assert-true (string-contains? result "REFUSING")) (assert-true (string-contains? result "archived copy"))))))));; ============================================================;; Mint-time uniqueness;; ============================================================(test-group "newly minted ids avoid ids already in use" ;; POSITIVE CONTROL: the probe can observe a mint at all. (test "positive control: a fresh capture gets an id" (call-with-temp-directory (lambda (dir) (let* ((store (make-folio-store dir)) (result ((make-tool-inbox-add store) (dict text: "something")))) (assert-true (string-contains? result "Added to inbox")) (assert-true (string-starts-with? (folio-task-id (car (read-inbox store))) "t-")))))) ;; DETERMINISTIC: the predicate the mint depends on. If this is wrong the ;; mint cannot be right, and unlike the statistical test below it cannot ;; pass by luck. (test "find-all-tasks-by-id reports an occupied id as in use" (call-with-temp-directory (lambda (dir) (let ((store (make-folio-store dir))) (mkproject dir "p" "- [ ] taken {id: t-0abc}\n") (assert-equal 1 (length (find-all-tasks-by-id store "t-0abc"))) (assert-equal 0 (length (find-all-tasks-by-id store "t-0abd"))))))) ;; STATISTICAL, and labelled as such. A single mint cannot be made to ;; collide deterministically against a 65536-space, so this occupies 4096 ;; ids (t-0000..t-0fff, 6.25% of the space) and mints 120 times. A mint ;; with NO uniqueness check survives that with probability 0.9375^120, ;; about 0.04%. ;; ;; An earlier version of this test occupied 200 ids and minted 40 times, ;; which a check-free mint survived about 89% of the time -- it passed ;; under sabotage and was therefore not a gate at all. (test "120 mints against 6.25% occupancy never collide" (call-with-temp-directory (lambda (dir) (let ((store (make-folio-store dir)) (hexc "0123456789abcdef")) (mkproject dir "occupied" (string-join (map (lambda (i) (format "- [ ] occupied ~a {id: t-0~a~a~a}" i (string-ref hexc (modulo (quotient i 256) 16)) (string-ref hexc (modulo (quotient i 16) 16)) (string-ref hexc (modulo i 16)))) (iota 4096)) "\n")) (let loop ((i 0)) (when (< i 120) ((make-tool-inbox-add store) (dict text: (format "fresh ~a" i))) (loop (+ i 1)))) (let ((dups (filter (lambda (id) (> (length (find-all-tasks-by-id store id)) 1)) (map folio-task-id (read-inbox store))))) (assert-equal '() dups)))))) (test "task-add into a project also mints uniquely" (call-with-temp-directory (lambda (dir) (let ((store (make-folio-store dir))) (mkproject dir "target" "- [ ] existing {id: t-aaaa}\n") ((make-tool-task-add store) (dict text: "new one" project: "target")) (let ((ids (map folio-task-id (folio-project-tasks (proj dir "target"))))) (assert-equal 2 (length ids)) (assert-false (equal? (car ids) (cadr ids)))))))))