Repository navigation
fix(lint): field-path rules judge an object as served, so a field an objectExtensions entry adds is not refused (#30383) - #30432
objectstack-fleet[bot] wants to merge 4 commits into
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 3d9d6eb0e7af39bff14f91ce993bda4d0043b02f && git checkout 3d9d6eb0e7af39bff14f91ce993bda4d0043b02f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f18adce2f65b9de8ce3b7de1c3ba870bebb53420 a7e081935144d36f0b0c4094061e04a8dd511ed5 && git checkout -B drift-repro f18adce2f65b9de8ce3b7de1c3ba870bebb53420 && git merge --no-ff a7e081935144d36f0b0c4094061e04a8dd511ed5
node scripts/docs-audit/affected-docs.mjs --json f18adce2f65b9de8ce3b7de1c3ba870bebb53420
|
|
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 |
Requested by Yibo Wang · project thread
domain:spec#3 · os-support-ai · session_01WGufzg3UmwhpAtL969c7YG
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;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,预期 63 例全绿(其中 16 例为本 PR 新增)。What changes
object-graph.ts:indexServedObjectGraph(stack),indexObjectGraphoverobjectsAsServed(stack).objects. Not exported from the package entry. The shared fold (objectExtensionsByTarget,objectsAsServed) itself is unchanged.validate-related-list-refs.ts: its fold read the stack's top-levelobjectExtensionsonly, and only the map form offields. It now readsobjectsAsServedfor the declared children andobjectExtensionsByTargetfor a child only an extension reaches (another package's object), every field read by name in either shape.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 (16 tests);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-unknownvalidate-list-view-field-refs.tsref_nopasses, refused without;nowherestilllist-view-field-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-fieldvalidate-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 targetvalidate-dataset-measure-aggregates.tsscorepasses, over extensionregionismeasure-aggregate-field-type-refused;nowherestays the existence rule'svalidate-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
e27387e2, 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 8037 passed (8037);check:test-typecheck: OK; VERDICT command-exit 0.npx eslint --no-inline-configover the 17 changed.tsfiles: exit 0.examples/app-showcase, which carries top-levelobjectExtensions): each of the 14 rules run on the loaded config with theorigin/maincopy and with this 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.Reverse verification (scripts/ablation-replace.mjs, restored to the HEAD blob)
indexServedObjectGraph's body back toindexObjectGraph(stack)(anchor 1 → 0, blob6701b63b281e→1c21f154c760):object-graph.test.tsgoes 9 failed | 54 passed (63). The nine red are exactly the pins of the eight rules routed through the helper (dataset references ×2, report filter, object field refs, RLS, thank-you, measure aggregates, preset comparands, field consumers). The other six rules readobjectsAsServeddirectly; each of their pins asserts the same stack without the extension still refuses, so none of them is vacuous. Restored: anchor count back to 1,git diff HEADempty. The lint tests importsrc, so no dist sat between mutation and run.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackate27387e2: 59 derived families. Each run serially withNODE_OPTIONS=--max-old-space-size=4096; record/mnt/project-files/issue-30383/gates-ran.list, every lineexit 0,check-changeset-no-majorincluded.dispatch-gates --ran: "59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED", exit 0.check-plugin-teardown-shape --self-test(shallow clone; its pinned fixture commit fetched),check:dual-build-cjs-loads(no dists; green after a fullturbo run build, VERDICT command-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. On the showcase none of these changes a finding.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, whose index was not measured.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