Repository navigation
fix(lint): field-path rules judge an object as served, so a field an objectExtensions entry adds is not refused (#30383) - #30432
Conversation
Twelve field-path rules indexed declared objects only, and two rules folded objectExtensions through private readers that differed from the registry. All now read the one shared fold in object-graph.ts (objectsAsServed / objectExtensionsByTarget): a view, report, dataset, page or widget naming a field a sibling package adds through objectExtensions resolves as it does at runtime. Adds indexServedObjectGraph, the graph over objectsAsServed. Claude-Session: https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG Co-authored-by: Claude <noreply@anthropic.com>
One artifact for all fourteen rules: a module package adds fields to the app package's object. Each existence rule passes the extension-added field, refuses it without the extension, and still refuses a field nothing adds under the same rule id; the type arms judge the served definition; the two retired readers keep one entry per package per target. Adds the changeset. Claude-Session: https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 fe33964b711725635d55d4612605a7ba8a5fd488 && git checkout fe33964b711725635d55d4612605a7ba8a5fd488
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c58008187fb14259cae52461aff9971a1b4b15e9 806dd06b0fe3f915f32a7b022d5f505166a0169d && git checkout -B drift-repro c58008187fb14259cae52461aff9971a1b4b15e9 && git merge --no-ff 806dd06b0fe3f915f32a7b022d5f505166a0169d
node scripts/docs-audit/affected-docs.mjs --json c58008187fb14259cae52461aff9971a1b4b15e9
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head e27387e · 2026-11-05T11:09Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe shape is right: one helper beside the fold, every field-path rule resolving through it, the per-rule table honest about what each judges, and the two security rules weighed rather than exempted. Must-fix, in order: (1) Implemented-by: VERDICT: FAIL |
…s skip tied fields Contract review r1 on the served-fold PR: - validate-related-list-refs no longer builds a child from extensions alone: on the per-package leg a child another body owns read as having only a sibling's extension fields, a false related-list-ref-unknown. - A field two packages write at the deciding priority is served, but which definition wins is registration order. The type arms (measure aggregates, preset comparands, sharing comparisons, widget field types, kanban option values) leave it unjudged; the thank-you panel fails closed on it. - Pins: the per-package leg for every existence rule, byte-equal refusals against a declared-only control, the related-list per-package case, ties. Claude-Session: https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG Co-authored-by: Claude <noreply@anthropic.com>
…field Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head a7e0819 · 2026-11-05T12:02Z Re-review at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe rework takes the first of the two fixes round 1 offered and documents it in the header; the per-package leg is now pinned for every moved rule and for related lists; the byte-equal pin exceeds what was asked; the tie handling is the membership-key rule's rule applied where a type is judged, with the one anonymous-facing surface failing closed, and every direction read is safe. No must-fix. Should-fix, in order: (1) make the preset and sharing tie assertions non-vacuous by listing the text writer first in Implemented-by: VERDICT: PASS |
Each tied pair now lists the definition the arm would refuse last, which is the one the fold keeps, so every arm that read it instead of dropping the tie goes red. Adds the widget field-type map and the kanban option map, and says in the RLS rule and above the helpers how a tie is read. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审三轮。首轮 FAIL:相关列表规则在单包视角下把兄弟包对第三个包对象的扩展当成子对象来判,产生误报。返工后复审 PASS,平局处理也核对过注册表的合并顺序。最后一轮补了三条建议:平局钉原来有两处空转(现在每个分支单独一个测试,消融 5 条全红),注释措辞,以及 RLS 注释。更宽的同类读取器另开 #30471。 席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Requested by Yibo Wang · project thread
Fixes #30383
Clause-②: yes (widening: field-path rules accept fields a sibling package adds through
objectExtensions, which the runtime already serves; any rule that deliberately stays on declared fields says why in its header)Metadata-protocol: none (lint judgements, no authorable key)
Skills-docs: none owed — no skill, eval or doc teaches that a view, report, dataset, page, widget, mapping, related list or row policy may not name an extension field (
git grep -c -E "dataset-field-unknown|list-view-field-unknown|rls-predicate-unknown-field|related-list-ref-unknown|mapping-target-field-unknown|page-field-unknown|widget-filter-field-unknown" -- skills content/docs ':!content/docs/releases': 1 hit,content/docs/deployment/validating-metadata.mdx:267, which names no extension;skills/objectstack-data/rules/security.md:156refuses a membership key on an extension, a rule this PR does not touch)Prior rulings read: objectextensions,indexobjectgraph,objectsasserved → 2 hits; ADR-0029 D3, ADR-0146 D10; thread: none; repo: objectstack-ai/objectstack
维护者速读
objectExtensions加的字段。packages/lint/src/object-graph.ts文件末尾新增的indexServedObjectGraph与objectsWithoutTiedFields;validate-related-list-refs.ts与validate-mapping-target-fields.ts两个私有读取器的删除;object-graph.test.ts末尾的[#30383]describe。pnpm --filter @objectstack/lint exec vitest run src/object-graph.test.ts,预期 89 例全绿(其中 42 例为本 PR 新增)。What changes
object-graph.ts, two helpers at the end of the file:indexServedObjectGraph(stack, { tied }),indexObjectGraphoverobjectsAsServed(stack).objects; andobjectsWithoutTiedFields(served), the served objects less every field inserved.tied. Neither is exported from the package entry. They sit below the fold's section on purpose:foldCode()invalidate-security-posture.test.ts(:1656-1665) appendsobject-graph.ts's text fromexport interface VisibleObjectExtensionto/** What a path resolved toto the posture rule's code, and holds every receiver read there to its tables; these two read only the fold's output. The shared fold (objectExtensionsByTarget,objectsAsServed) itself is unchanged.objectsAsServed(...).tied: two packages write one field at the deciding priority). The registry registers packages in dependency-topological order (resolveArtifactPackageOrder), notpackages[]order, so which definition is served cannot be read off the stack. Every rule still accepts the tied field's name. The type arms leave it unjudged: measure aggregates and preset comparands index{ tied: 'drop' }, the sharing rule's condition graph and the widget field-type map and kanban option map are built fromobjectsWithoutTiedFields. The thank-you rule fails closed instead: a tied field passes only when every writer at the deciding priority has an engine-generated type.validate-related-list-refs.ts: its fold read the stack's top-levelobjectExtensionsonly, and only the map form offields. It now readsobjectsAsServedfor the children this stack declares, every field read by name in either shape. An object this stack does not declare is not a child even when an extension targets it: on the per-package leg its declaration and foreign keys may live in a package the leg does not carry, so it falls to the unknown-child rule as before (header says so).validate-mapping-target-fields.ts:extensionFieldsByTarget(top level plus every body, every entry kept) is deleted; the object is judged as served. The ruled refusal of a present non-arraypackages(ruling A on finding(spec): the option-B readers disagree with@objectstack/coreabout whether a non-arraypackagesis a refusal #15293) stays, asked throughpackagesOfonce, since the fold reads such a value as absent.validate-field-consumers.tsheader: extension-added fields are still not judged themselves, but a dataset or cube path through an extension-added relationship now credits the declared field it reaches.object-graph.test.ts, the fold's harness (42 tests): for each existence rule an extension-added field passes, the same stack without the extension refuses, and a field nothing adds is refused with byte-equal severity,where, path and message to a declared-only control; the per-package leg (packageBodyAsStack's shape) for the existence rules and for related lists; a nested describe for a field two packages tie on, one test per tie-dropping arm, each tied pair listing last the definition the arm would refuse (the one the fold keeps), with a single-writer control of that definition refused;validate-mapping-target-fields.test.ts's extension fixture rewritten to the two shapes the registry reads (see Acceptance notes)..changeset/30383-field-path-rules-extension-fields.md:@objectstack/lintpatch.Per rule
All on one artifact: an app package declares
xs_deal, a module package extends it withregion,score,closed_on,ref_no,partner, and extendsxs_notewith a lookup toxs_deal.validate-dataset-references.tsindexServedObjectGraph)ref_noandpartner.tierpass; without the extensiondataset-field-unknown;nowhere,partner.nowherestilldataset-field-unknownvalidate-report-runtime-filter.tsref_nopasses, refused without the extension;nowherestillreport-filter-field-unknownvalidate-widget-bindings.tsref_nopasses, refused without;nowherestillwidget-filter-field-unknown; SUM over anumber/percenttie is notmeasure-aggregate-incoherent, overpercentalone it isvalidate-list-view-field-refs.tsref_nopasses, refused without;nowherestilllist-view-field-unknown; a lanebon a select tied between optionsa,banda,cis not judged, ona,calone it islist-view-kanban-lane-unknownvalidate-object-field-refs.tsref_nopasses, refused without;nowherestillobject-field-ref-unknownvalidate-page-field-bindings.tsref_nopasses, refused without;nowherestillpage-field-unknownvalidate-rls-predicate-enforceability.tsref_nopasses, refused without;nowherestillrls-predicate-unknown-fieldvalidate-thank-you-record-fields.tsautonumberpasses, refused without;nowherestillthank-you-record-field; a tie betweenautonumberandtextis refusedvalidate-mapping-target-fields.tsref_nopasses, refused without;nowherestillmapping-target-field-unknown; one entry per package per targetvalidate-related-list-refs.tsobject.field; refused without;xs_note.nowherestillrelated-list-ref-unknown; one entry per package per target; on the per-package leg a body extending another package's undeclared object raises nothingvalidate-dataset-measure-aggregates.tsscorepasses, over extensionregionismeasure-aggregate-field-type-refused;nowherestays the existence rule's; a tiedscoreis not judgedvalidate-preset-comparands.tsclosed_on: 'last_30_days'on an extension date isfilter-preset-comparand; on extension text it passesvalidate-sharing-rule-enforceability.tscontrolled_by_parentanchor's master is read as the security posture rule's merge (#23210) reads itrecord.region == record.namepasses;record.closed_on == record.nameissharing-rule-unlowerable-conditionvalidate-field-consumers.tspartner.tiercreditsxs_account.tier; without that pathtierisfield-no-consumers; no finding on any extension-added fieldNo rule stays on declared fields. The two security rules were weighed: the RLS rule's only verdict on an extension field was a false
rls-predicate-unknown-field, and the sharing rule's type arm judges what the driver judges at query time, so neither has a security reason to read the declared anchor.Why
The runtime serves each object with every body's
objectExtensionsfolded in, but these rules indexedstack.objectsonly, so on a multi-package artifact (HotCRM, hotcrm#2315: a module package adds fields to an app package's object) a view, report, dataset, page or widget naming an extension-added field was refused as unknown. #30303 (PR #30359) added the shared fold and moved the FLS-key check onto it; this PR moves the rest of the census onto the same fold.Verification
All at head
806dd06b, underos-verify-lock,NODE_OPTIONS=--max-old-space-size=4096:pnpm --filter @objectstack/lint test && pnpm --filter @objectstack/lint typecheck: Test Files 194 passed (194), Tests 8063 passed (8063);check:test-typecheck: OK; VERDICT command-exit 0.object-graph.test.ts: 89 passed (89).npx eslint --no-inline-configover the 17 changed.tsfiles: exit 0.examples/app-showcase, which carries top-levelobjectExtensions), measured ate27387e2: each of the 14 rules run on the loaded config with theorigin/maincopy and with that head. Every rule's finding set (rule, path, where, message) is identical, 49field-no-consumerswarnings and 0 of every other id on both sides. Probe files deleted. The rework narrows only what that head accepted (an undeclared related-list child, a tied field's type), and the showcase has neither.Reverse verification (restored to the HEAD blob each time)
scripts/ablation-replace.mjs:indexServedObjectGraph's body back toindexObjectGraph(stack)(anchor 1 → 0):object-graph.test.tswent 9 failed | 54 passed (63), exactly the pins of the eight rules routed through the helper. The other six rules readobjectsAsServeddirectly; each of their pins asserts the same stack without the extension still refuses, so none is vacuous.31eef372):validate-related-list-refs.tsset back to itse27387e2blob: the new per-package-leg pin goes red (1 failed | 84 passed (85), a falserelated-list-ref-unknownon the undeclared target); restored.objectsWithoutTiedFields's first line made to returnserved.objectsunconditionally: 5 failed | 84 passed (89), one per arm: measure aggregates, preset comparands, sharing-rule criteria, the widget field-type map, the kanban option map. The thank-you pin stays green, as it should: that rule readstieditself and fails closed. Restored,git diff HEADempty. (ablation-replace.mjsrefuses a replacement that keeps its anchor, so this one was applied and restored by hand, the blob checked equal to HEAD after.)src, so no dist sat between mutation and run.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat806dd06b: 59 derived families, the same set as ata7e08193. Each run serially withNODE_OPTIONS=--max-old-space-size=4096; record/mnt/project-files/issue-30383/gates-ran.list, every lineexit 0on the first pass,check-changeset-no-majorincluded.dispatch-gates --ran: "59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED", exit 0.!in the title and no(narrowing)arm: no full dogfood run. CI owns the rest of the farm.Acceptance notes
validate-mapping-target-fields.tsread the top-levelobjectExtensionsbesidepackages[]; the registry reads only the bodies when an artifact carriespackages[](its top level repeats them). The old test fixture putsla_tierat the top level only, beside apackages[], a shape no CLI leg produces (the per-package leg hands the body's own entries in itspackages[]entry too); it is rewritten to the two shapes the registry reads. (2) Both readers kept every entry; the fold keeps one per package per target, so a field only a package's earlier, replaced entry adds is now refused, as the registry serves it (pinned). (3) The related-list reader read only the map form offields; the array form is now read too. (4) The related-list reader made an undeclared object reached by a top-level extension a judged child; that is dropped, so such an object falls to the unknown-child rule, as it does for any undeclared object. On the showcase none of these changes a finding.objectsAsServedreturns; only the type arms named above drop ties. Narrowing those too would need the graph to carry a tie per field, a change to the shared index this card leaves alone.validate-translation-references.tskeeps a third privateobjectExtensionsByTarget(top level plus every body, every entry kept). It also folds validation rules, which the shared fold does not carry, so it was not moved.indexObjectGraphstill read declared objects only:validate-expressions.ts(buildFieldIndex, therecord.FIELDreads of validations, formulas, flows, hooks and sharing-rule conditions),indexObjectFieldsinvalidate-react-page-props.ts,validate-flow-node-writes.tsandvalidate-action-body-writes.ts, and the own copy invalidate-hook-body-writes.ts. Open PR fix(lint)!: refuse a stored page sort naming an unknown field, on every page sort slot (#30093) #30270 moves page sorts tovalidate-sortable-fields.ts, which at its head reads declared objects only (recordsOf(stack.objects), line 508); carried with the others.defineStack's twin,collectRelatedListNameErrorsinpackages/spec/src/stack.zod.ts, folds every entry additively; lint is now the stricter of the two on the replaced-entry case only.validate-page-field-bindings.tswas moved last; open draft PR fix(lint)!: refuse a stored page sort naming an unknown field, on every page sort slot (#30093) #30270 edits the same file in hunks this PR does not touch.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses (eslint over the changed files: exit 0; the repo-wide run is CI's).main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzg3UmwhpAtL969c7YG