Repository navigation
feat(spec)!: refuse a user field whose reference names an object other than sys_user; every user field resolves to sys_user - #25074
Conversation
…er to sys_user Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…ished 18.x advisory's changeset Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…er-field-reference-refused
📓 Docs Drift CheckThis PR changes 2 package(s): 19 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 see
Coarse fallback — 144 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 78ac70748fed2ec9853aabb65ce179fd9adb3a7f && git checkout 78ac70748fed2ec9853aabb65ce179fd9adb3a7f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b6e870707b95d7b8ee9cedd68ad358b51de0da91 1a4eb0cf80a72df05c8bde66858344a65de3ceda && git checkout -B drift-repro b6e870707b95d7b8ee9cedd68ad358b51de0da91 && git merge --no-ff 1a4eb0cf80a72df05c8bde66858344a65de3ceda
node scripts/docs-audit/affected-docs.mjs --json b6e870707b95d7b8ee9cedd68ad358b51de0da91
|
…er-field-reference-refused
…he 24952 entry Restore .changeset/24808-user-field-foreign-reference-warning.md byte-identical to origin/main. The 24952 entry now declares Metadata-protocol: narrowed, names @objectstack/lint (minor) so the withdrawal of the advisory and of the USER_FIELD_FOREIGN_REFERENCE export lands in the CHANGELOG the 24808 entry writes to, and states that it supersedes that entry. Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 49 runs on |
Closes #24952
Clause-②: no (narrowing: a
userfield naming an object other thansys_useris refused; ships now under the amended 「甲」, #60156303865025)Metadata-protocol: narrowed
The refusal half of #24808 (ruled A on #24854), re-timed to 18.x by the amended 「甲」 ruling. BREAKING: the accept set of
FieldSchemanarrows. Shipped as aminorchangeset.What changed
FieldSchemarefuses the shape (packages/spec/src/data/field.zod.ts). A field oftype: 'user'whosereferencenames anything butsys_usergets onecustomissue atreference. The message names thelookupspelling ({ type: 'lookup', reference: 'TARGET' }/Field.lookup('TARGET', ...)) and says thatdeleteBehaviormust be chosen. Absent, blank and'sys_user'still parse. The check is onFieldSchemarather than as anObjectSchemasuperRefine (therefuseForeignTreeReferenceprecedent), because the verdict needs only the field. So objects, object extensions anddefineStackall refuse it through the same code.referenceTargetOfanswerssys_userfor everyuserfield, whatever the carrier says (field-value.zod.ts). A carrier that cannot be read still throws. A stored body saved before this release therefore resolves tosys_useras well.Field.user():referenceis now typed'sys_user'(still@deprecated) and is written after the config spread. The output keeps its key order and its literal types.user-field-foreign-reference-refused(packages/spec/src/migrations/entries/semantic/18.user-field-foreign-reference-refused.ts). It is not a conversion, because the rewrite to alookupis not lossless: alookupjoins the referential delete walk withdeleteBehavior: 'set_null', anddeleteBehaviorhas no value meaning "leave it".objectstack migrate metareports the entry.@objectstack/lintadvisoryuser-field-foreign-referenceis withdrawn. It was added for 18.x and never published, and the parse refusal replaces it. Removed: the rule, itsRULE_EXPLANATIONSentry, its export and its tests.field-value.test.ts(my_people) and theobject-graph.test.tsexplicit-carrier control, both now assert that the hop lands onsys_user. Thevalidate-object-referencesunknown-object control is now proven on alookup..changeset/24952-user-field-reference-refused.md(breaking, with FROM/TO, the fix and the ADR-0087 marker).The pending 24808 changeset (route B, seat decision 2026-10-22)
This PR no longer deletes
.changeset/24808-user-field-foreign-reference-warning.md, the pending release note of #24942. The file is restored byte-identical tomain, socheck-empty-changesetis green.Instead, this PR's own
.changeset/24952-user-field-reference-refused.mdnames@objectstack/lintas well as@objectstack/spec, carriesMetadata-protocol: narrowed, and has a Supersedes paragraph. That paragraph says theuser-field-foreign-referenceadvisory, theUSER_FIELD_FOREIGN_REFERENCEexport and itsos explainentry that 24808 announced are withdrawn before release, and that a foreignreferenceon auserfield is now refused byFieldSchema. Both entries land in@objectstack/lint's CHANGELOG together, so a reader can tell which one is current. The packages version in lockstep, so naming lint moves no version number.Producers
The first act from the re-timing comment was to measure who writes this shape. A source grep over the tree (tests excluded) found no
type: 'user'with a foreignreferencein any app, example or package.pnpm --filter @objectstack/spec test:repo, which parses the repo's example stacks, is green. So no producer needs fixing in this PR. Two test fixtures used the shape: theobject-graph.test.tsmodule fixture was already alookup, and the explicit-carrier control is rewritten here.Verification (head
bf738d1f99, merged withorigin/mainat63085c3806)pnpm --filter '@objectstack/lint^...' build(spec and the rest of lint's dependency closure): exit 0.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date. No regeneration was needed: no.describe(), authorable key or public export changed.pnpm --filter @objectstack/spec exec vitest run --project local: 706 files, 21433 tests passed.pnpm --filter @objectstack/spec test:repo: 72 files, 1237 tests passed.pnpm --filter @objectstack/spec typecheck: exit 0. This includescheck:test-typecheck, which compiles theexpectTypeOfpins in the new test file.pnpm --filter @objectstack/lint exec vitest run: 153 files, 7045 tests passed.pnpm --filter @objectstack/lint typecheck: exit 0.node scripts/pm/dispatch-gates.mjs --ran, round 2 at1a4eb0cf80, after mergingorigin/mainand restoring 24808: 91 derived, 87 run, 4 NOT MEASURED on the first pass (missing dist). Three of those were re-run green after the closure builds (check:doc-formula-expressions,check:docs-transcript-drift,check:lean-entry-closure), which leavescheck:dual-build-cjs-loadsto CI.check-empty-changeset,check-changeset-no-major --event,check:adr-0087-registration,check-changeset-fixedandcheck:nul-bytesall exit 0. spec: 21471 tests andtest:repo1247 passed; lint: 7069 passed.Acceptance notes
packages/fields/src/__tests__/userCell.readGate-10535.test.tsxat the pinned.objectui-sha(cf153eb2e5) still pins that the user cell's person object is the field'sreference(crm_member). The fixture casts its field and never parses it, so nothing here turns it red. But it now pins a reading the spec refuses. The renderer readsreferencedirectly rather than the type's constant, so for a stored body saved before this release it would gate by a different object than the runtime resolves. Retiring it is a change in objectui, outside this claim's file surface.objectmapping toObjectSchemainpackages/spec/src/kernel/metadata-type-schemas.ts. It was not measured over HTTP in this PR.CONTRACT_REVIEW_TIERcontract review.field.zod.ts,field-value.zod.ts,object.zod.tsor the lint files in this diff: none (all 11 open PRs were read).Generated by Claude Code