Uh oh!
There was an error while loading. Please reload this page.
feat(objects): adopt the declared referenceVia pointer pair on four system objects, per object (#11386) - #11676
Conversation
…ystem objects (#11386) #11339 landed the polymorphic pointer-pair carrier (ADR-0052 §5 ActivityPointer) and adopted it on `sys_activity`. This card adopts it on the remaining system objects carrying the same `(object half, id half)` idiom — measured per object, not as a sweep: - sys_audit_log.record_id via object_name — declared - sys_approval_request.record_id via object_name — declared - sys_record_share.record_id via object_name — declared - sys_share_link.record_id via object_name — declared - sys_automation_run.trigger_record_id — deliberately NOT declared Each adoption re-verified that object's own pair semantics (who writes it, what reads it back) and its seed corpus before declaring, and each seed test asserts the query that makes the pair load-bearing on that object rather than a generic "the id resolved": the {object_name, record_id} index for the audit ledger, the pending-request lock for approvals, the grant lookup for record shares, and the fail-closed recordStillExists gate for share links. The fifth object records a deliberate "stays undeclared" verdict at the declaration site, with its reasons and what would have to change to flip it: a paused sys_automation_run row is a live continuation the engine rehydrates on boot, terminal rows are telemetry under a 30-day sweep, and the object has no natural key to address rows by — so declaring would advertise run rows as authorable seed content rather than make a real corpus resolvable. Measured while writing the tests, and recorded in them: unlike sys_activity, none of the four heals an out-of-order pointer in pass 2, because an engine-owned row has no natural key and pass 2 back-fills by externalId. The load fails loudly (the ruled family direction) rather than storing the literal; a positive control isolates the cause to keylessness rather than to the pointer pair. No new engine doubles, so no new rows in scripts/engine-double-contract.pinned.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
📓 Docs Drift CheckThis PR changes 4 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not seeCoarse fallback — 16 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 edc561255a31b9335cacde7893dfd220d0006ce1 && git checkout edc561255a31b9335cacde7893dfd220d0006ce1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8bcd054702f4078bedcd21a3734524d235ba4b9d 0245ee74216b2dd040375e4018bd40ef7dc3b861 && git checkout -B drift-repro 8bcd054702f4078bedcd21a3734524d235ba4b9d && git merge --no-ff 0245ee74216b2dd040375e4018bd40ef7dc3b861
node scripts/docs-audit/affected-docs.mjs --json 8bcd054702f4078bedcd21a3734524d235ba4b9d
|
os-sam
commented
Aug 24, 2026
PM: docs-drift advisory dispositioned — no action; and this PR is blocked on contract review
Docs drift — checked where it could actually biteThe advisory lists 26 hand-written pages (truncated above 15 rows). Rather than re-read all 26, the check was aimed at the only class that this diff can make actively wrong: a doc showing an authored seed whose Read with Result: no such example exists. Every
⛔ The 5 release-owned pages were read-only and untouched. ⛔ This PR cannot enqueue yet — |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11386
Adopts the declared
referenceViapolymorphic pointer pair (ADR-0052 §5 ActivityPointer, carrier landed by #11339) on the remaining system objects that carry the(object half, id half)idiom — measured per object, not as a sweep, which is the card's own discipline and the triage grading's instruction.sys_audit_logobject_name/record_idsys_approval_requestobject_name/record_idsys_record_shareobject_name/record_idsys_share_linkobject_name/record_idsys_automation_runtrigger_object/trigger_record_idNo
packages/spec/**change was needed: the key, its authoring refusals and its liveness entry all landed with the carrier. This PR is adoption plus tests.Per-object verification (five separate ones, not one and four assumptions)
Each object's pair was re-verified against its own writers and its own consuming query before declaring, and each seed test asserts that object's load-bearing query rather than a generic "the id resolved":
sys_audit_log— four writers stamp an object machine name beside a real record id (audit-writers.ts,read-audit.ts,auth-event-audit.ts, plugin-auth's admin user endpoints). Read back by the{object_name, record_id}index and therecord_viewsview: who touched THIS record. Both halves optional, so the un-addressable refusal is reachable here.sys_approval_request— the pair is the key the machinery queries on:approval-service.tsfinds the open request withwhere: { object_name, record_id, status: 'pending' }, andlifecycle-hooks.tsholds the record lock on the same pair. A verbatim natural key locked nothing while looking pending. Both halvesrequired: true.sys_record_share— dereferenced as a real address:record-orphan-cleanup.tsstates the invariant ("record gone ⇒ the row cannot describe any access at all") and sweeps rows by asking whether(object_name, record_id)still exists. An unresolved grant enforced nothing while displaying as a grant, and was then deleted by that sweep.sys_share_link— verified through a different consumer than its sibling table:share-link-routes.tscallsengine.find(link.object_name, …), andshare-link-service.tsresolves a token through the fail-closed gateif (!(await this.recordStillExists(...))) return null. An unresolved link was a permanently dead token returning the same null as revoked/expired — silent at seed time, indistinguishable from a revocation at use time.sys_automation_run— the shape fits (serialize()stampstrigger_object: ctx.objectbesidetrigger_record_id: ctx.record.id) but the authorability does not, and the declaration's only enforced consumer today is seed-time resolution. Apausedrow is a live continuation the store rehydrates from every{ status: 'paused' }row on boot; terminal rows are telemetry under this object's own 30-day sweep; and the object has no natural key to address rows by (nameField: 'id', nonamefield). Declaring would not make a real corpus resolvable — it would advertise run rows as authorable seed content. The verdict, its reasons, and what would have to change to flip it (a consumer that reads the pair for something other than seed authoring — [engine][设计卡] 多态弱引用挂靠表的平台级删除级联 —— sys_record_share/attachment/comment 一族的统一清理机制 #5180's delete-cascade carrier is the live candidate) are recorded on the field and pinned by a test, so a later sweep has to rule against it on purpose rather than by shape.Seed corpus, measured
No seed dataset in this repo seeds any of the five, and neither does the packaged HotCRM app: its
src/data/*.seed.tsdatasets are business objects only, andsrc/sharing/demo-staffing.tsstates outright that "no seed dataset and no flow node in this app targets …sys_record_share" (its grants are materialised by rule evaluation). So the accept/reject flip breaks no existing corpus; it is prospective, and the seed loader explicitly supports targetingsys_*tables (its writes run as a privileged system context for exactly that reason).Measured while testing, and not what was predicted
The first draft of the audit-log case asserted that an out-of-order pointer heals in pass 2, by analogy with
sys_activity. It does not, and the reason is a property of the dataset rather than of the pair: an engine-owned row has no natural key, so its dataset declares noexternalId, and pass 2 back-fills by looking the row up by externalId — it resolves the target and then has nowhere to write it. The test now asserts the measured behaviour (loud failure, never the verbatim store), and a positive control — the same seeds, same order, with anexternalIddeclared — isolates the cause to keylessness rather than to the pointer pair. Filed separately as #11674 rather than fixed here.Not asserted either way, deliberately: on the three objects whose id half is
required: true, a real engine enforcingrequiredwould also reject the deferred insert. The engine double in that suite does not validate, so it cannot measure this; #11674 carries it.Ablation — both legs, restored byte-identically
Predicted in writing first, then run; mutation confirmed on disk by anchored greps in both directions (removed-text count and injected-text count), restored via
git checkoutagainst an absolute path, and proven bygit hash-objectagainst the HEAD blob (non-empty and matching on both files).referenceViafrom the loader test'sADOPTER_SCHEMAS(4 anchored hits removed, 4 markers injected): 9 failed / 11 passed. The 9 are exactly the declaration-dependent cases — 4 accept, 3 refuse, the keyless case and its positive control. The 11 that stayed green are the Seeds cannot address an ActivityPointer: sys_activity.record_id is plain text, so a packaged app can load timeline rows that attach to nothing #11339sys_activitycases, the internal-id-verbatim case (an undeclared column also stores verbatim, so that assertion does not discriminate on its own) and thesys_automation_runundeclared case.referenceVialine from the realsys_audit_logdeclaration (1 hit removed, pure deletion): the intended assertion goes red withexpected undefined to be 'object_name', 3 others in that file stay green. A first attempt that injected a marker key instead was thrown out as not-measured: the strict field schema refused it at import, so the file reported "no tests" rather than a red assertion.No rebuild was needed for either leg: both mutated files are imported by relative path from tests inside their own package, so nothing resolved through
dist.Verification, all at
0245ee7421metadata-protocol1909 passed / 10 skipped ·plugin-audit318 ·plugin-approvals589 ·plugin-sharing652 ·service-automation1035typecheckgreen for the three packages that declare the script (6 script-name echoes in the output, so the filter matched non-zero);metadata-protocolandservice-automationdeclare notypecheckscript —check:type-check-coveragecovers them structurally and is greenscripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(path-matched + convention-triggered) all green, includingcheck:engine-double-contract— no new rows inscripts/engine-double-contract.pinned.json, because the new cases reuse the fake engine already pinned in that filecheck:i18nfirst returnedPREREQUISITE NOT MET — the workspace CLI is not built, which is not a pass; afterturbo run build --filter=@objectstack/cliit reportsOK (9 package(s) — all bundles in sync), withplugin-approvals,plugin-auditandplugin-sharingeach in syncpnpm lint(full repo,eslint . --no-inline-config) exit 0 — no narrowing claimedcheck:type-check-debt --re-measureon the built closure:check-type-check-coverage: OK — 65/78 workspace packages type-checked …, no ratchet driftGenerated by Claude Code