Repository navigation
fix(lint)!: a lookup or master_detail into a stored-metadata-body table is refused at author time - #24800
Conversation
…ody table at author time The expression rule's object pass refuses a relationship field whose reference is in STORED_METADATA_BODY_OBJECTS, keyed on the hop refusal's own collector, so os validate, os build and the object save door refuse it alike. The engine never hydrates such a field. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…eference describe and the field-types page Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…rence into a stored-metadata-body table Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ed-metadata-body table Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ference-into-metadata-body-refused
…ot off the field The #5017 receiver meta-test pins the keys the field walk reads off its receiver; the type is already in the index the walk builds. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…refusal Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
… field, and pins the user case its collector admits A user field's authored reference is not refused at parse, so the hop collector already admits a user naming the family; the collector's docblock said otherwise. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 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 bb2a7bd6d4c8815e43d07d1b620b3a8b732f64c3 && git checkout bb2a7bd6d4c8815e43d07d1b620b3a8b732f64c3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a43405d596d4d670dc761e90c9fc20822dc806a9 5e54169edcbb57124019aa23efe3745edafb706e && git checkout -B drift-repro a43405d596d4d670dc761e90c9fc20822dc806a9 && git merge --no-ff 5e54169edcbb57124019aa23efe3745edafb706e
node scripts/docs-audit/affected-docs.mjs --json a43405d596d4d670dc761e90c9fc20822dc806a9
|
Contract reviewServed-tier: Read-only, on the narrowing face, adversarially against the dispatch and the dev report. Inputs: card #24677 (body and all six comments, the ruling 6282780489 verbatim 「都派」, the claim 6282819504, the dispatch 6282884996, the dev report 6285300787), #24358 (dev report 6253491408 and the recorded ruling 6279185009), #24316, the rider 6282830389 on #6367, PR #24800 (body, 11-file list, net diff against merge base Check-runs on the head, read at 2026-10-20T07:13Z: 39 runs; 27 ① Derived judgmentsEvery public-surface and behaviour change the diff implies, each judged against the code at base and head.
② Semver level
③ Boundary flagsEvery dev flag, deviation and finding, answered.
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
Fixes #24677
Clause-②: no (narrowing)
The authoring doors refuse a
lookupormaster_detailfield whosereferenceis a stored-metadata-body table (STORED_METADATA_BODY_OBJECTS:sys_metadata,sys_metadata_history). This is the rule the maintainer ruled B on #24358 (「24358 同意」, then 「ok」 to its shape), pulled into 18.0 on 「都派」 (record 6282780489). Claim 6282819504.What changes
@objectstack/lint. The expression rule's per-object pass (runStackExpressionPassesinvalidate-expressions.ts) refuses the field. It sits beside the [security] Stored-metadata-body family: a formula field's one-hop read through a reference is an exit the family projection does not reach — reach not yet measured, detail withheld pending maintainer #24316 hop refusal, keyed on that refusal's own collector,storedMetadataBodyReferencesOf, and its per-object index, so the two refusals judge one set.object 'NAME' · field 'FIELD' reference.expression-invalidrule, so there is no new error code and no new registry entry. It rides the object crossing the rule already has (runtimeTypesis unchanged), soos validate,os buildand the object save door give the same finding.STORED_METADATA_BODY_PRESCRIPTION.masterDetailCount, so an unreadablereferencecarrier is still refused by the pass that owned that verdict first. It reads the field's type from the walk's field-type index rather than off the field (the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 receiver meta-test pins the keys read offf).@objectstack/spec. TheFieldSchema.referencedescribe and its TSDoc state the convention. This is the field's ownreferenceatfield.zod.ts:1476;:989is a grid cell column's. The reference pages were regenerated bygen:docsonly (content/docs/references/{data/field,data/object,system/migration}.mdx, the describe line and nothing else).content/docs/data-modeling/field-types.mdx: thereferencerows oflookup/master_detail, plus a short section saying why the reference is refused.protocol.tschange:packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, through the realsaveMetaItem/publishMetaItem;stored-metadata-body-family.pin.test.ts(28 to 29).apiEnabled, and error codes.Premises, re-measured
P1: the doors accepted it. Measured before the change, at base
4b114eda57. Probes: one object carryingmeta, alookupor amaster_detailinto each family table; the controls point tosys_user.4b114eda57)5e54169edc)os validate(CLI built from the tree)object 'probe_carrier' · field 'meta' referenceunderexpression-invalid; controls 2/2 exit 0os buildPUT /api/v1/meta/object/NAMEon a freshpnpm dev:crm -- --freshbackend, as the dev admin200; controls 2/2200; gate-live control (asqrt(record.amount)formula)422422 INVALID_METADATA, with the issue atobject 'probe_…' · field 'meta' referenceand the message ending on the prescription; controls 2/2200runRuntimeAuthoringRules)P2: where the hop refusal runs.
refuseStoredMetadataBodyHopand its collector index live inrunStackExpressionPasses(validate-expressions.ts, about:1934and:1953on base). It is registered asvalidateStackExpressionsinauthoring-rules.ts, withcommands: ALL,surfaces: CLI_AND_RUNTIMEandruntimeTypes: ['flow', 'action', 'hook', 'object']. On an object write, the gate'sruntimeWriteTyperuns the passes over the object's own body. The new refusal is in that same per-object loop, so it reaches the same doors. P1's "after" column is that reading.P3: exposure. A
git grepfor a family table as areference(string literal, JSON, theField.lookup/Field.masterDetailbuilders,SystemObjectName.METADATA):examples/**,packages/apps/**,packages/platform-objects/**,packages/create-objectstack/src/templates/**andpackages/qa/**at4b114eda57: 0. A positive control matched 3 of 3 synthetic lines.4b114eda57: 0. Onorigin/maina43405d596: 0.platform-objects, 4 inapp-showcase, 1 inapp-todo, 4 inqa).The family objects themselves (
packages/metadata-core) reference other tables (sys_user,sys_package_version,sys_environment), and that is unaffected. Neither premise failed, and the family set is unchanged.Tests
Run on head
5e54169edc, each underos-verify-lock.sh,--maxWorkers=2:@objectstack/linttest@objectstack/linttypecheck@objectstack/metadata-protocoltest@objectstack/metadata-protocoltypecheck@objectstack/spectest (--project local)@objectstack/spectypecheck@objectstack/objectql(--project local)@objectstack/platform-objects@objectstack/runtime(--project local)@objectstack/cli(--project unit; integration is declared to CI)os validateandos buildon every example app (app-crm,app-multi-package,app-showcase,app-todo)Consumer sweep (G2). No fixture outside
packages/lint/packages/spec/content/docswent red, so there is no fixture for a rider. The family-iterating engine tests in@objectstack/objectqlregister objects directly and run no authoring rule (they are the installed-artifact path), and they are green.The new pins:
packages/lint/src/validate-expressions.stored-metadata-body-reference.test.ts, 21 cases:lookup/master_detailis refused at the build, atos validate/os build(runAuthoringRules), and at the object door with the build's own finding;usernaming the family is refused too (the collector's type set);sys_userand an app object, aslookupand asmaster_detail) draw nothing;422/INVALID_METADATAenvelope and nothing landing; a draft's promotion refused; 2 controls saved; door finding equal to build finding.Ablation (G4)
Run through
scripts/ablation-replace.mjson6c2c70f3eb. The later commits change only the message text and add auserpin; the mutated line is unchanged.if (!target || !type) continue;becameif ('ablation-24677') continue;: anchor 1 to 0, blobe85a4327e28fto6564a909a8fd.src) went 15 red / 11 green of the 26 cases then on the branch (theuserpin came later). The 11 green are the 4 controls, the differential and the 6 hop pins.ablation-dist-preflightfound the marker in 4 dist files. The DTS pass failed on the mutation itself (TS2872 / TS2345), which does not touch the JS the suite reads.dist/) went 6 red / 2 green. The 2 green are the controls.e85a4327e28f(equal to HEAD),git diff HEADempty, whole-treegit status --porcelainclean.--absentfound the marker in 0 of 20 dist files.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, on5e54169edc: 111 commands. Each exit code was captured before any pipe.--ran:Run reconciliation — 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:type-check-debtexited 3 in the battery: a queue-timeout on the verify lock, so nothing was measured. It was re-run under the lock: exit 0,1 ledger entr(ies) re-measured … none above its recorded number.--shallow-since=2026-07-13) first.check:dev-prereqsexit 0 andcheck:skill-frame-freshnessexit 0.@objectstack/speccheck:generated:All 14 generated artifacts are up to dateaftergen:docs.check:adr-0087-registration: green,not-required (no-migration-prescription).check-changeset-no-major: green, nomajor.Changeset
.changeset/24677-reference-into-stored-metadata-body-refused.md:@objectstack/lintminor,@objectstack/specpatch(the describe text only).minorunder the launch-window convention. It carries the FROM / TO / fix mapping and the stored-row sentence..changeset/pre.jsononmainis{"mode": "exit", "tag": "next"}. So this changeset is consumed by the release that leaves thenextpre-release line, and nothing here changes that mode.Acceptance notes
expression-invalidfor a field declaration. The rule's id is the expression rule's, and that is where the ruling put the refusal ("where the hop refusal already runs"). The location (… field 'FIELD' reference) and the message carry the specifics.userfield. The collector's type set isREFERENCE_VALUE_TYPES, and the parse accepts an authoredreferenceon auserfield (measured:ObjectSchema.safeParseaccepts{ type: 'user', reference: 'sys_metadata' }). So ausernaming the family is refused the same way, and is pinned. The collector's docblock claimed otherwise and now says what was measured. Atreecannot name the family: the object schema refuses a foreigntreetarget.os validateaccepts auserfield whosereferencenames any other object (measured:{ type: 'user', reference: 'probe_account' }, exit 0).Field.user's contract says "target fixed to thesys_usersystem object", andreferenceTargetOfreturns the authored carrier first.origin/mainonce (37aa1d68d6).origin/mainhas since moved 10 commits (a43405d596), and none of them touches this diff's files.CONTRACT_REVIEW_TIERreview of record on the narrowing face is owed before the queue.Generated by Claude Code