Skip to content

feat(seed): declared pointer-pair resolution — seeds can address an ActivityPointer (referenceVia) - #11388

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11339-seed-activity-pointer
Aug 23, 2026
Merged

feat(seed): declared pointer-pair resolution — seeds can address an ActivityPointer (referenceVia)#11388
os-zhuang merged 2 commits into
mainfrom
claude/issue-11339-seed-activity-pointer

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11339

The measured gap

sys_activity rows are the only thing the record:activity timeline renders, and a packaged app could not ship a usable one: record_id is Field.text(), the seed loader resolved natural keys only for lookup/master_detail/user fields, 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 on origin/main before implementation: the resolution gate sits in buildDependencyGraph (fieldDef.type === 'lookup' | 'master_detail' | 'user'), and no declaration carrier for a polymorphic pointer existed anywhere in spec (xRef is 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 a sys_activity quirk — 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)

  1. Carrier (packages/spec/src/data/field.zod.ts): a text field may declare referenceVia: '<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 with reference. ObjectSchema.create refuses an undeclared or self-referential sibling (packages/spec/src/data/object.zod.ts, beside assertSystemDataIsWritable, create-only per the at-rest-tolerance convention).
  2. Loader (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, targetField per the target dataset's declared externalId, same rule as buildReferenceMap) 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.
  3. Adoption: sys_activity.record_id (via object_name) and source_id (via source_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

seed writesbeforeafter
resolvable natural key in a declared pairliteral stored, attaches to nothing, success: trueresolved to the target's internal id; console filter matches
unresolvable value in a declared pairliteral stored, success: trueloud failure: counted, reported (success: false), logged at error; record dropped when no pass 2 can heal it, never stored verbatim
internal-id-shaped value (UUID/ObjectId; nanoids via the existing id probe)storedunchanged — kept/resolved
same column spellings, no declarationstored verbatimunchanged — only the declared carrier opts a pair in

No in-repo seed targets sys_activity (checked examples/ and packages/), so no existing dataset changes behaviour.

Four-axis analysis

sys_activity's 14-day telemetry retention still applies to seeded demo history — an authoring concern (seed timestamps inside the window via cel temporals, 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 in seed-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 appended field.test.ts block 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).
  • Reverse verification (from the committed state, mutation and restore both grep-confirmed on disk, script carried a restore 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.js relatively through vitest's transform, not across a dist boundary.
  • Derived gate sweep (node scripts/pm/dispatch-gates.mjs, no paths passed; answer from this repo at 8c64eb591): all derived + convention-triggered families run locally and green — including check: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 needs mapfile, 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


Generated by Claude Code

os-zhuangand others added 2 commits August 23, 2026 22:27
…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/plugin-audit, @objectstack/spec, touching 12 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a872ce93ab967daeb017429220bcd5723c7682fa.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/field.json, packages/spec/liveness/state-counts.md) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectSchema (symbol, 35 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a872ce93ab967daeb017429220bcd5723c7682fapackageMentionDocs.

Which tree this was computed on

This run read content/docs from 4238208edb9a021a45db472074ce34869b131a10 — the merge of head b9af5410ab3bf99caa786a1f772b5851d996c3c9 into base a872ce93ab967daeb017429220bcd5723c7682fa, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a872ce93ab967daeb017429220bcd5723c7682fa → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 15:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 8d237b4Aug 23, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11339-seed-activity-pointer branch August 23, 2026 16:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Seeds cannot address an ActivityPointer: sys_activity.record_id is plain text, so a packaged app can load timeline rows that attach to nothing

1 participant

@os-zhuang