Repository navigation
feat(plugin-audit): sys_comment gains a writer-stamped object_name + record_id pair with a backfill, and the comment-thread read gate leafs over it (ADR-0139 E7) - #23881
Conversation
…record_id pair, and the comment-thread read gate leafs over it (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…nt rows; pins for the leaf, the probe classes and the stamp (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…t parent-pair backfill (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…egenerated census and translations (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…t the old window on a real SecurityPlugin + AuditPlugin stack (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…nt pair (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
… old probe's thread window (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…stand-in engine holds the caller's bound and declares no findOne (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
… find double is judged by the limit gate (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…mment-parent-pair
📓 Docs Drift CheckThis PR changes 2 package(s): 59 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 34 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dce801dfa5347e5949e62a7dadbe00be088460c2 && git checkout dce801dfa5347e5949e62a7dadbe00be088460c2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 61e5ee9e5720433ccca483160bccbf880e6d9b84 f588d6631992c93ef870fbe53571d3f94fa5e56c && git checkout -B drift-repro 61e5ee9e5720433ccca483160bccbf880e6d9b84 && git merge --no-ff f588d6631992c93ef870fbe53571d3f94fa5e56c
node scripts/docs-audit/affected-docs.mjs --json 61e5ee9e5720433ccca483160bccbf880e6d9b84
|
Contract reviewServed-tier: Rendered at Read in this act:
Scope: every path of the code diff.
Read on
The changeset's prose and the docs are the lane seat's ACCEPT, except the changeset's ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL FAIL reason, against the diff at this head:
Owed before landing (one patch round, then a new record on the new head, since a FAIL does not carry):
Generated by Claude Code |
席位核对 · REWORK (patch round 1) ·
|
… thread re-derives it from the stored thread, so preserveAudit cannot land a writer's pair (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
…nt pair; a row re-pointed mid-backfill keeps its re-pointed pair (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
Patch round 1 addendum ·
|
Contract reviewServed-tier: A same-form re-review after patch round 1, at Read in this act:
Read in the cited code at the head, because the round's claims are about the engine:
Stamp of this act: 2026-10-16T13:24Z. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL FAIL reason, against the diff at this head:
Owed before landing (one patch round, then a new record on the new head, since a FAIL does not carry):
Generated by Claude Code |
席位核对 · REWORK (patch round 2) ·
|
…over rows on different records is refused whole, VALIDATION_ERROR / 400 (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
Patch round 2 addendum ·
|
…o one engine call, not closed (ADR-0139 E7) Claude-Session: https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK Co-authored-by: Claude <noreply@anthropic.com>
Patch round 2 addendum, supplement · head
|
…mment-parent-pair # Conflicts: # content/docs/deployment/cli.mdx
Base merge · head
|
Contract reviewServed-tier: A same-form re-review after patch round 2, at Read in this act:
Read in the cited code at the head:
Of these, Stamp of this act: 2026-10-16T14:56Z. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS At this head, the record Generated by Claude Code |
席位核对 · ACCEPT ·
|
Fixes #23857
Clause-②: yes
ADR-0139 E7, comments half, by the ruling on #23732 (record
6212058353, batch #354 item 2: letter A on Q1, A on Q2, maintainer 「同意」).sys_commentgains a writer-stamped parent pair, and the comment-thread read gate narrows by it with oneReadableIdsleaf per parent object, with no pre-scan. #23732 remains open; the seat handles it as the ruling's item 6 says.Rollout (the changeset's first paragraph)
Run
os migrate comment-parent-pair --applyat once after upgrading. Until it runs, the comments written before the upgrade carry no pair, and the gate answers them by the bounded thread probe it always had: at most 2,000 candidate rows per read, fail closed. So a broad comment list (one not scoped to a single record's thread) can omit comments the reader may see, and itstotalagrees with the short list. A record's own thread is unaffected. The door copiesos migrate activity-changed-fields(E6, PR #23814) one for one:--database-url,--apply,--yes,--force,--json; dry run by default; idempotent. Same measured difference as E6: the columns arrive with the upgraded deployment's schema sync, which--applyperforms itself, so a dry run against a database no upgraded process has opened cannot read them and exits 1.What changed
objects/sys-comment.object.ts).object_nameandrecord_idareField.text,readonly, not required, and indexed together as(object_name, record_id).thread_idstays the authored key. There is no ADR-0087 conversion and no writer change.planCommentParentPair(comment-access-hooks.ts). It parses the thread withparseCommentThreadId, the parse every comment gate authorizes by. A thread that names no record getsnull/null.beforeInserthook stamps every row, for every caller, the system included, and overwrites any value the writer sent. The engine's create-side readonly strip skipssys_objects, so this hook is the guard, as it is foris_edited.thread_idis re-stamped from it.preserveAudit(the REST import'streatAsHistorical) keeps a readonly value. A multi-row update of this kind over rows whose threads name different records is refused whole by the stamp itself, not by the engine's key-set divergence check (VALIDATION_ERROR/ 400, nothing written), because oneSETclause cannot carry two pairs. Over rows sharing one thread, it stores that thread's pair. Pinned on a real engine and through the REST import (patch rounds 1–2, addenda6216192076and6217048956).sys_commentdeclares the pair. This is E6'sdeclaresActivityStampColumnsprecedent, for harnesses that register their ownsys_comment.installCommentReadVisibility). It is nowcomputeParentRecordFilterfromparent-record-read-gate.ts. That function is imported, not edited, so no third copy of the D5 classification is written. It runs over the comment's pair with acommentThreadGate()config:sys_comment. All ofAUDIT_EXCLUDED_OBJECTSis named, because a comment can hang off any record. This is the same setparseCommentThreadIdaccepts.canReadObject(no leaf is written without it) and an engine that cannot classify.object_name IS NULL. Its branch carries that conjunct, so it never selects a stamped row. Rows the backfill has not reached are probed, never leafed.sys_commentthat declares no pair keeps the thread probe over every row, and the gate says so once.next()sits outside the gate'stry, so a composition refusal (ondriver-mongodb, a 500) is the read's own failure (D3).AuditPluginpasses the security source.total2,000 of 2,601.backfillCommentParentPair(comment-parent-pair-backfill.ts, exported fromsrc/index.ts):object_name $null, then a single-id SYSTEM update of both columns.preserveAuditwith the row's storedupdated_at, so no comment'supdated_atmoves. Measured through the door.unparentedand left unstamped.os migrate comment-parent-pair, registered in the one-shot family table, the JSON-purity family and the absent-database roster.deployment/cli.mdx: a section and a table row.platform-checklist/areas/cli.json: 17 → 18 subcommands.attachments-storage.read-inherits-parent-rls, revision 5, carries the two notes from6206614195.Landing places: everything is in
plugin-audit, the CLI door and its three family registrations, dogfood, docs, and the generated files the gates regenerate. None ofwriteAudit,packages/spec,objectql,core, any driver,parent-record-read-gate.ts,migrate/organization-ownership*,apply.ts,docs/adr/**or objectui is touched.Evidence
Local readings at head
e499adaf6(withorigin/main34b740f2dmerged in), on the shared box, underscripts/pm/os-verify-lock.sh. Patch rounds 1–2 (to7d41db652) change only the update stamp, docblocks, the changeset and tests. The read gate is untouched. The dev re-read these numbers at7d41db652(supplement6217097662), and they are unchanged: real engine member 2,303 of 2,303 and administratortotal2,605 of 2,605; the stack served 2,050 of 2,050. The rounds' own gates, suites and ablations are in addenda6216192076and6217048956.@objectstack/plugin-audit: tests 53 files / 863 passed;typecheckexit 0 (test-typecheck ledger OK).@objectstack/cli: unit project 301 files / 4,496 passed;typecheckexit 0.CLI integration, run locally because the diff touches it:
data-commands.absent-databaseandschema-migrate.one-shot-family: 147 / 147;json-stdout-purity.e2e(nightly tier): 62 / 62.@objectstack/dogfoodtypecheck exit 0. Dogfood: the new pin plus the eight suites that readsys_commentor share the gate (the E6 pin among them): 9 files / 79 passed.The bound case, before and after. "Before" is ablation A0 below: main's whole-read thread probe put back.
AuditPlugin+ SQLite (comment-parent-pair.integration, 2,607 rows): member served / total, of 2,303 visibletotal, of 2,605SecurityPlugin+AuditPluginstack at the HTTP door (dogfood, 4,151 comments, one thread each): member served, of 2,050 visibleThe administrator's newest-20 read is served 20 rows, with a total past 2 × 2,000. An unreadable parent's comments stay hidden: another member's record, a vault object, a deleted record, and a thread naming no record. The thread-scoped read of one of them answers 0 rows and
total0.No SYSTEM-composed read or leaf. On the real stack, no SYSTEM read of
sys_commentcarries a leaf. The only SYSTEM reads are the two bounded probes:thread_idoverobject_name $null, and the D5 pair probe.Ablations. Each negative pin was run red, then restored. Every mutation went through
scripts/ablation-replace.mjs, and every restore was proven blob == HEAD withgit diff HEADempty. Unless noted, the pins are the two plugin-audit suites, run fromsrc.dist: marker present (ablation-dist-preflightexit 0), 4 of 5 red. Restore rebuilt and the marker proven absentobject_name IS NULLconjunctcanReadObjectplanParentReads, not committed)dist: marker present (ablation-dist-preflightexit 0), 2 red. Restore rebuilt and the marker proven absentVoid legs, declared:
Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 103 commands ate499adaf6: 23 paths, 2,276 changed lines, under the 3,000 threshold. All 103 exit 0.--ranreports 「103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN」.pnpm check:adr-symbol-anchorsexits 0. ADR-0139's anchorscomment-access-hooks.ts#READ_SCAN_LIMITand#installCommentReadVisibilitystill resolve.check:engine-double-contractandcheck:objectql-double-limit, both on my unit stand-in. The stand-in was restructured; no ledger was touched.dist, a shallow clone) re-ran green after a build and a fetch.Acceptance notes
thread_id, which every reader of a row is already served and may already filter by, so it discloses nothing new. The pair gets no entry inparent-field-query-guard.ts; that guard covers value-bearing columns of the activity stream and the ledger, andsys_commenthas no such column.object_name IS NULL. After the backfill that probe finds nothing; until then it finds at most 2,000 rows.null/nullforever, so the unstamped probe always scans it. It stays hidden, as before.records-forms.jsonare about feeds and posting. So only the attachments item was revised, and no item was invented.parent-record-read-gate.ts'sno-object-admissionwarning still cites the activity stream's withheld-update re-read, and the comment gate now prints it too. The file is ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788's, so it is not touched here. Carrier: ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788.Seat edit ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T13:10Z: "Who can write the pair" bullets 3–4 and ablations A12–A13, per patch round 1 (addendum6216192076).Seat edit ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T13:56Z: bullets 3–4 again, ablation A14 and the Evidence scope line, per patch round 2 (addendum6217048956).Seat edit ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T13:58Z: the Evidence re-read at7d41db652and bullet 3's refusal source, per supplement6217097662.Generated by Claude Code