Uh oh!
There was an error while loading. Please reload this page.
feat(seed): declared pointer-pair resolution — seeds can address an ActivityPointer (referenceVia) - #11388
Conversation
…ctivityPointer (#11339) A text field may declare `referenceVia: '<sibling>'`, marking it as the id half of a polymorphic pointer pair (ADR-0052 §5) whose target object the sibling column names per row. The seed loader derives a per-row reference from the pair and routes it through the same resolution machinery static lookups use — externalId probes, in-memory map, pass-2 deferral — and refuses an unresolvable or un-addressable pointer loudly instead of storing the literal natural key as a row that attaches to nothing. sys_activity adopts the carrier on both pairs (record_id via object_name, source_id via source_object), so a packaged app's seed can ship timeline rows the shipped console filter { object_name, record_id } actually finds. Authoring contradictions are refused at parse (text-only, exclusive with reference) and at ObjectSchema.create (sibling must be declared). Undeclared text columns keep today's verbatim behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uble-contract ledger The pointer-pair test's relative import gains its .js extension so the package's tsc debt count stays at its recorded 63 (the ratchet refuses +1), and check:engine-double-contract --write records the new pinned coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 129 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 4238208edb9a021a45db472074ce34869b131a10 && git checkout 4238208edb9a021a45db472074ce34869b131a10
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a872ce93ab967daeb017429220bcd5723c7682fa b9af5410ab3bf99caa786a1f772b5851d996c3c9 && git checkout -B drift-repro a872ce93ab967daeb017429220bcd5723c7682fa && git merge --no-ff b9af5410ab3bf99caa786a1f772b5851d996c3c9
node scripts/docs-audit/affected-docs.mjs --json a872ce93ab967daeb017429220bcd5723c7682fa
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11339
The measured gap
sys_activityrows are the only thing therecord:activitytimeline renders, and a packaged app could not ship a usable one:record_idisField.text(), the seed loader resolved natural keys only forlookup/master_detail/userfields, and so a seed's pointer value was stored verbatim at every gate green. Downstream measurement (objectstack-ai/hotcrm#1258, on 17.1.0): rows load 342→345, and the exact filter the shipped console issues —{ object_name, record_id }— finds the literal string ('Lisa Thompson'→ 3 rows) and not the real record id (tgIjpNhjlfmWU8YF→ 0 rows). The premise was re-verified onorigin/mainbefore implementation: the resolution gate sits inbuildDependencyGraph(fieldDef.type === 'lookup' | 'master_detail' | 'user'), and no declaration carrier for a polymorphic pointer existed anywhere in spec (xRefis a flow-config annotation, not a data-field concept).One premise from the issue was sharpened by survey: the
(object half, id half)pair is a genuine platform-wide idiom, not asys_activityquirk — six objects carry it (sys_activity×2 pairs,sys_audit_log,sys_approval_request,sys_record_share,sys_share_link,sys_automation_run.trigger_*), all as plain text. That is what justifies a declared carrier over a one-off.The shape shipped — declared pointer-pair resolution (issue option 1, carrying option 3's refusal half)
packages/spec/src/data/field.zod.ts): atextfield may declarereferenceVia: '<sibling>'— the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer) whose target object the sibling column names per row. Contradictions refused at parse: text-only, mutually exclusive withreference.ObjectSchema.createrefuses an undeclared or self-referential sibling (packages/spec/src/data/object.zod.ts, besideassertSystemDataIsWritable, create-only per the at-rest-tolerance convention).packages/metadata-protocol/src/seed-loader.ts): load step 4.5 collects declared pairs; each record derives its own reference entry (target object read from the sibling column,targetFieldper the target dataset's declared externalId, same rule asbuildReferenceMap) and flows through the same resolution loop as static references — same in-memory/DB probes, same pass-2 deferral (which is what makes ordering irrelevant: a pointer contributes no static graph edge by construction), same counters and error registers. An un-addressable pointer (id half authored, type half empty) is refused outright — no pass can ever resolve it.sys_activity.record_id(viaobject_name) andsource_id(viasource_object) declare the pair. Nothing else does, deliberately — see Adopt the referenceVia pointer-pair declaration on the remaining polymorphic pairs (sys_audit_log, sys_approval_request, sys_record_share, sys_share_link, sys_automation_run) — measured per object, not as a sweep #11386.The contract change, stated plainly
success: truesuccess: truesuccess: false), logged aterror; record dropped when no pass 2 can heal it, never stored verbatimidprobe)No in-repo seed targets
sys_activity(checkedexamples/andpackages/), so no existing dataset changes behaviour.Four-axis analysis
pm:blockedon this card by explicit choice (the maintainer ruled Route A: wait for seedability rather than work around), and the console timeline is a shipped surface no packaged app could populate from seed. The pair idiom's six-object survey shows the carrier has a platform-wide population, not a speculative one.record_labelmatching was explicitly rejected in the issue). Declared = enforced from day one; the carrier is also the natural substrate for the family's open delete-cascade design ([engine][设计卡] 多态弱引用挂靠表的平台级删除级联 —— sys_record_share/attachment/comment 一族的统一清理机制 #5180) without a second declaration mechanism later.referenceat parse, missing sibling atcreate, unresolvable value at load. The old failure mode — a success receipt for configuration that does nothing — is the one this closes.$expand/drill support, cascade semantics, and the five sibling objects' adoption (each changes that object's seed accept/reject contract with zero downstream measurement — filed as Adopt the referenceVia pointer-pair declaration on the remaining polymorphic pairs (sys_audit_log, sys_approval_request, sys_record_share, sys_share_link, sys_automation_run) — measured per object, not as a sweep #11386, per-object, not a sweep). [engine][设计卡] 多态弱引用挂靠表的平台级删除级联 —— sys_record_share/attachment/comment 一族的统一清理机制 #5180 remains open for the delete side.sys_activity's 14-day telemetry retention still applies to seeded demo history — an authoring concern (seed timestamps inside the window viaceltemporals, as the downstream seed already did), noted in the issue as adjacent, unchanged here.Verification
All at
b9af5410a(final head; earlier full runs at the parent tree differ from it only by this test file's import spelling and the double-contract ledger — both re-verified at head).pnpm --filter @objectstack/metadata-protocol test→ exit 0,Test Files 139 passed | 2 skipped (141)/Tests 1892 passed | 10 skipped (1902)— 9 new tests inseed-loader-pointer-pair.test.ts(resolution in-memory/DB/per-row/externalId, pass-2 order independence, both refusal modes, uuid passthrough, undeclared-pair regression guard, dry-run quiet reporting).pnpm --filter @objectstack/spec test(full suite ran) → 414 files passed after adding the liveness classification; the appendedfield.test.tsblock covers accept/refuse/create-assertion/.describe()prose.pnpm --filter @objectstack/plugin-audit test→ exit 0,Test Files 19 passed (19)/Tests 313 passed (313)with both pairs declared.pnpm --filter @objectstack/spec check:generated→ exit 0, all 14 artifact gates ✓ (authorable-surface, api-surface, docs, liveness regenerated and committed).trap): disabling pointer-pair registration (if (false)at load step 4.5) predicted 7 RED / 2 GREEN in the new file — observed exactly that (seed-loader-pointer-pair.test.ts (9 tests | 7 failed); the uuid-passthrough and undeclared-pair tests stayed green as predicted). One unrelated 5s-timeout flake (reference-sites.derivation.test.ts, a purity test) failed in the ablated run only; green in all three non-ablated runs. Restore leg: marker count 0, original line count 1, full suite re-run exit 0. No rebuild leg applies: the test imports./seed-loader.jsrelatively through vitest's transform, not across a dist boundary.node scripts/pm/dispatch-gates.mjs, no paths passed; answer from this repo at8c64eb591): all derived + convention-triggered families run locally and green — includingcheck:durability-log-level,check:cross-package-test-inputs,check:engine-double-contract(ledger learned the new pinned double via--write),check:where-matcher,check:i18n,check:type-check-debt(the new test file's import was spelled NodeNext-clean so the package's frozen debt count stays 63),check:liveness,check:nul-bytes. Two could not run on this macOS host, both host-bound and unrelated to the diff:check:objectui-changeset(self-test needsmapfile, absent from bash 3.2 — exit 127 inside the fixture) and the shared verify-lock entry point (VERDICT lock-unusable, flock absent) — heavy commands therefore ran unserialised, declared rather than hand-rolling a lock.Out of scope, recorded
mode: 'upsert'accepted againstmanagedBy: 'append-only'(noted in the issue): not filed as a defect — seed replay depends on upsert matching for idempotence (isNoOpReplay), andappend-onlygoverns user-facing API affordances (reconcileManagedApiMethodsderives reads-only), not system seed writes. If refusal is wanted there it needs its own measurement.Generated by Claude Code