Commit316fdeefRecorded25 Aug 2026Repositoryfolio

F3b: a root-wide operation must PROVE its population complete

Message

Pre-review reach finding, closed before the reviewer sees the tree: the referrer scan iterates discovery's CANDIDATES, so anything discovery could not walk -- an unreadable directory, a contested candidate, a symlinked directory (deliberately unwalked, and the F3a containment escape is the standing reason to distrust them) -- could hold referrers no pass would ever see, and a rename or delete would proceed around them: silent knowledge loss through the census's blind spot.

population-guard refuses every root-wide destructive operation (rename, delete, cross-file move) while any such location exists, naming each; skips for 'not-a-source-format / 'not-a-regular-file are accepted BY DESIGN (a .txt or a socket is outside the document population the ruled independent pass covers). Probe arms: a symlinked directory in the root refuses both rename and delete with 'population-unprovable; battery 12/12.

Changed
 src/folio/doc/structural.sgl | 42 ++++++++++++++++++++++++++++++++++++++++--
 test/f3b-probe.sgl           | 19 ++++++++++++++++++-
 2 files changed, 58 insertions(+), 3 deletions(-)
Diff
src/folio/doc/structural.sglmodified
@@ -143,6 +143,37 @@
143
;; its population from discovery's census -- with the denominator kept,
144
;; never a filtered list -- and reads every candidate INSIDE the lock.
145
;; ------------------------------------------------------------------
+146
;; A root-wide destructive operation may only trust a population it can
+147
;; PROVE complete. Discovery reports what it could not walk, and three
+148
;; of its classes mean referrers could exist UNSEEN: unreadable
+149
;; directories, contested candidates (collisions), and symlinked
+150
;; directories (deliberately unwalked -- and the F3a containment escape
+151
;; is the standing reason to distrust them). Any of those REFUSES the
+152
;; operation; skips for reason 'not-a-source-format or
+153
;; 'not-a-regular-file are accepted BY DESIGN -- a .txt or a socket is
+154
;; outside the document population the ruled independent pass covers.
+155
;; Returns #f when the census is trustworthy, or a refusal naming what
+156
;; is not.
+157
(define (population-guard census)
+158
(let ((unwalkable
+159
(append
+160
(map (lambda (d) (list d 'unreadable-directory))
+161
(census-unreadable-dirs census))
+162
(map (lambda (c) (list (candidate-path c) 'contested-candidate))
+163
(census-collisions census))
+164
(let loop ((rest (census-skipped census)) (out '()))
+165
(cond ((null? rest) (reverse out))
+166
((eq? (skip-reason (car rest)) 'symlinked-directory)
+167
(loop (cdr rest)
+168
(cons (list (skip-path (car rest)) 'symlinked-directory) out)))
+169
(else (loop (cdr rest) out)))))))
+170
(if (null? unwalkable)
+171
#f
+172
(%txn-refuse 'population-unprovable
+173
(format "a root-wide operation cannot prove its referrer scan complete: ~a location(s) could not be walked: ~a -- refusing rather than renaming or deleting around unseen referrers"
+174
(length unwalkable) unwalkable)
+175
retryable: #t))))
+176
177
(define (root-population coord)
178
;; Returns (census . entries) where each entry is
179
;; (list rel-path abs-path format bytes-or-#f). Bytes are read for
@@ -427,11 +458,13 @@
458
(let* ((doomed (cons node-id (%txn-subtree-declared-ids node)))
459
(doomed (filter string? doomed))
460
(population (root-population coord))
+461
(guard-refusal (population-guard (car population)))
462
(found (find-referrers coord (cdr population) doomed))
463
(semantic (list-ref found 0))
464
(raw-only (list-ref found 1))
465
(troubles (list-ref found 2)))
466
(cond
+467
(guard-refusal guard-refusal)
468
((pair? troubles)
469
(%txn-refuse 'referrer-uninspectable
470
(format "cannot prove ~s unreferenced: ~a file(s) carrying its id could not be inspected: ~a"
@@ -562,7 +595,10 @@
595
(define (rename-under-lock coord old-id new-id expected-revision)
596
(let* ((population (root-population coord))
597
(census (car population))
−565
(entries (cdr population)))
+598
(entries (cdr population))
+599
(guard-refusal (population-guard census)))
+600
(if guard-refusal
+601
guard-refusal
602
;; 1. The NEW id must be completely fresh -- no boundary occurrence
603
;; anywhere, prose included. Conservative by design.
604
(let ((taken
@@ -602,7 +638,7 @@
638
old-id (length owners) (map car owners))))
639
(else
640
(rename-with-owner coord old-id new-id expected-revision
−605
entries (car owners)))))))))
+641
entries (car owners))))))))))
642
643
(define (rename-with-owner coord old-id new-id expected-revision entries owner)
644
(let* ((owner-rel (list-ref owner 0))
@@ -998,12 +1034,14 @@
1034
;; discipline as rename.
1035
(let* ((root (coordinator-root coord))
1036
(population (root-population coord))
+1037
(guard-refusal (population-guard (car population)))
1038
(entries (cdr population))
1039
(found (find-referrers coord entries moved-ids))
1040
(semantic (list-ref found 0))
1041
(raw-only (list-ref found 1))
1042
(troubles (list-ref found 2)))
1043
(cond
+1044
(guard-refusal guard-refusal)
1045
((pair? troubles)
1046
(%txn-refuse 'referrer-uninspectable
1047
(format "cannot account for every occurrence of ~a: ~a" moved-ids troubles)
test/f3b-probe.sglmodified
@@ -209,7 +209,24 @@
209
(assert-refusal "unknown id refuses"
210
(folio-rename-id! coord "t-nonesuch" "t-fresh"
211
(revision-of-file (rooted "owner.sgd")))
−212
'unknown-node))))
+212
'unknown-node)
+213
;; A location the scan could not WALK is a population it cannot prove
+214
;; complete: a symlinked directory inside the root (discovery skips
+215
;; those deliberately) must refuse every root-wide destructive op.
+216
(make-symlink (path-dirname (rooted "owner.sgd")) (rooted "esc-dir"))
+217
(assert-refusal "an unwalkable location refuses the rename"
+218
(folio-rename-id! coord "t-lonely" "t-fresh"
+219
(revision-of-file (rooted "owner.sgd")))
+220
'population-unprovable)
+221
(delete-file (rooted "esc-dir"))
+222
(assert-refusal "delete refuses over the same unprovable population"
+223
(begin
+224
(make-symlink (path-dirname (rooted "owner.sgd")) (rooted "esc-dir2"))
+225
(let ((r (folio-delete-node! coord (rooted "owner.sgd") "t-lonely"
+226
(revision-of-file (rooted "owner.sgd")))))
+227
(delete-file (rooted "esc-dir2"))
+228
r))
+229
'population-unprovable))))
230
231
;; ---------------------------------------------------------------------------
232
;; b7-delete -- the referrer discipline, then the clean path.