Repository navigation
fix(lint)!: os build and the object_extension publish door refuse a formula cycle an object extension adds - #33442
objectstack-fleet[bot] wants to merge 6 commits into
Conversation
… and the object_extension door A formula field an objectExtensions entry adds that reads itself, or two that read each other, now get the formula-cycle refusal the object's own formulas get, located at the entry. A cycle closing through a declared formula keeps the declared member's location at the object. Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
…he check in the object extension paragraph Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
…refusal through saveMetaItem and publishMetaItem Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift Check5 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 172 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 5812b4d84a93fb7087a836a397f73afe5d87bd04 && git checkout 5812b4d84a93fb7087a836a397f73afe5d87bd04
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fdf25a43707b03f2120941acd6acefbdd0b034a bfa139bac599ca65a062b0486364ea25d7e57ce8 && git checkout -B drift-repro 8fdf25a43707b03f2120941acd6acefbdd0b034a && git merge --no-ff bfa139bac599ca65a062b0486364ea25d7e57ce8
node scripts/docs-audit/affected-docs.mjs --json 8fdf25a43707b03f2120941acd6acefbdd0b034a |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag Contract reviewServed-tier: Inputs read: card #33153 (body, comments 6865769873, 6883476966, 6884342925), PR #33442 (body, 7 files, net diff against Check-runs on the head when this record was written: 21 success, 10 skipped, 33 in progress, 0 failed. Concluded green among them: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint · PM dispatch-gates, PR-body guards, Dogfood Regression Gate. Not concluded: Test Core 1-8, Test UI 1-16, Build Core, Type Check (workspace, debt ledger, consumer gates, UI), three Lint groups, Live E2E UI, Build & E2E UI. This record judges the contract and its implementing code; it does not answer for a check-run that had not concluded. ① Derived judgments
② Semver level
Clause-②: no (narrowing) — matches the diff. ③ Boundary flags
④ Design judgment
Attempts to break the implementing code.
Findings.
No must-fix. Finding 1 earns a delta review of its fix or of its carrier. Implemented-by: VERDICT: PASS |
…ber under a name the target already serves On the door's one pass the written entry is folded over the target as served before the write, so every name it adds is served from its own record: none is left out of the cycle report there. The build keeps leaving the authored target's declared names to the object walk. Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
…eck and the door's reading of a served name Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag Contract reviewServed-tier: Round 2. The round-1 record is comment 6884435411 on PR #33442, at head Inputs read: card #33153 (body, comments 6865769873, 6883476966, 6884342925, 6885198948), PR #33442 (body, 7 files, comments, net diff against Check-runs on the head: 72 runs, 60 success, 12 skipped, 0 failed, 0 pending. Concluded green: Test Core 1-8, Test UI 1-16, Build Core, TypeScript Type Check and the four Type Check groups, every Lint group, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint · PM dispatch-gates, PR-body guards, Dogfood Regression Gate, Build Docs, Live E2E UI, Build & E2E UI. ① Derived judgments
② Semver level
Clause-②: no (narrowing) — matches the diff. ③ Boundary flags
④ Design judgmentThe key attack: no name is left out of the report on the door's write.
Findings.
Observation, not charged to this PR: the same door snapshot gives a false negative when a higher-priority sibling's definition of a name the entry also adds closes a cycle through another field of the entry (the door folds the entry's definition over the sibling's and sees no cycle; the build reports the entry's member). That is a property of the #33072 snapshot, present at Round-1 findings (6884435411).
No must-fix. Finding 1 earns a delta review of its fix or of its carrier. Implemented-by: VERDICT: PASS |
…anked name, and state it beside the build's tie exception Claude-Session: https://claude.ai/code/session_01FEkQ5mAXtyZgMs9QwfQzXE Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag Contract reviewServed-tier: Round 3. Round 1 is comment 6884435411 (head Inputs read: card #33153 (body, comments 6865769873, 6883476966, 6884342925, 6885198948, 6885969777), PR #33442 (body, 7 files, comments, net diff against Check-runs on the head: 72 runs, 60 success, 12 skipped, 0 failed, 0 pending. Concluded green: Test Core 1-8, Test UI 1-16, Build Core, TypeScript Type Check and the four Type Check groups, every Lint group, Check Changeset (both runs on this head, 132613268973 and the rerun 132628462605 after the PR body was rewritten), Governed Surface Queue Guard, Spec property liveness, Lint · PM dispatch-gates, PR-body guards (both runs), Dogfood Regression Gate, Build Docs, Live E2E UI, Build & E2E UI. ① Derived judgments
② Semver level
Clause-②: no (narrowing) — matches the diff. ③ Boundary flags
④ Design judgmentEvery sentence the round added, against the code, for the build and for the door. What the code does, read at the head: the walk's
The new pin against the texts. Attempts to break the texts.
Findings.
No must-fix and no should-fix. Nits alone: the round ends here. Round-2 findings (6885259034).
Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag ACCEPT · PR #33442 (#33153) · head
|
Requested by Leehom · project thread
Fixes #33153
Clause-②: no (narrowing) —
os build,os validate,os lintand the runtimeobject_extensionpublish door (Studio,PUT /api/v1/meta/object_extension/TARGET, the MCP and AI authoring doors) now refuse one shape an object extension could carry before: aformulafield an extension adds that sits on a cycle of formulas (it reads itself, two added formulas read each other, or the cycle closes through a formula the target declares). No exported type or signature changes. Measured first-party exposure: 0objectExtensionsentries add aformulafield (examples/app-showcaseone entry, aselect, aurland anumberfield;packages/plugins/plugin-authone, asecretfield; HotCRMorigin/mainac4dd3d has noobjectExtensionsin code), so nothing first-party is newly refused.Metadata-protocol: narrowed
Skills-docs: none owed — this PR edits
content/docs/deployment/validating-metadata.mdx(the object extension paragraph names the cycle check);skills/objectstack-formulaandskills/objectstack-datateach formula fields anddefineObjectExtension({ extend, fields })without saying an extension is exempt from the cycle check, so the build and the door now give the verdict the object's own formulas already get.Precedent followed: #33216/#33217 (PR #33398), which widened the same extension walk and declared the same
no (narrowing)/narrowedpair.维护者速读
objectExtensions/ Studio 发布的object_extension)新增的公式字段,如果引用自己,或两个互相引用,或经由对象自身声明的公式绕回来,现在在os build和运行时发布口都会被拒(规则 idexpression-invalid,和对象自身公式循环同一句提示),位置为object '对象' · objectExtensions[0] · field '字段' expression。packages/lint/src/validate-expressions.ts(扩展遍历末尾新增的一次formulaCycleIssues调用)、packages/lint/src/formula-cycle-issues.ts(extensionFormulaLoci与 map 形式的report)、packages/lint/src/runtime-gate.object-formula-cycle.test.ts。pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.object-formula-cycle.test.ts,预期 16 条全过:扩展自引用、互相引用、经声明公式绕回的循环在 build 与发布口都被拒且位置一致,链式公式和读对象公式的扩展公式通过;发布口按写入条目自身的定义判定同名字段(含与其他包同优先级或被更高优先级覆盖的情形,build 此时放行,后续卡 object_extension door: the gate's served snapshot carries a sibling package's extension field with no priority, so the door's formula-cycle verdict differs from os build on ties and outranked entries #33494)。What changes
packages/lint/src/validate-expressions.ts(the extension walk only; the gate-setup region #33246 edits is untouched). After each target's entries are judged, the walk runs the formula-cycle pass (#33100'sformulaCycleIssues) over thefieldDefsit already builds for that target: the target as served (its declared fields with every kept entry's fields folded in throughobjectsAsServed, a tie between two packages left out), its declared fields and injected columns behind it; for a target the stack does not declare, the entries' own fields. No second fold. Each cycle member an entry adds gets a finding located at that entry, and in the build a member the authored target declares is left to the object walk, so a cycle closing through a declared formula keeps that member's location at the object and names it once. On the door's one pass the object walk does not run and the written entry is folded over the target as served before the write, so every name the entry adds is served from the entry's own record and none is left out, a name the target already serves included (review round 1).packages/lint/src/formula-cycle-issues.ts.formulaCycleIssues'reportalso takes a map from field name to location. The newextensionFormulaLoci(target, entries, left)builds it: each name an entry adds, locatedobject 'T' · objectExtensions[i] · field 'F' expression(the location the field-formula pass gives the same field), a name two entries add located at the later entry in fold order (the definition the registry serves), the caller'sleftnames left out (the authored target's declared names in the build, none at the door). Kept in this module so the walk gains no new receiver for the #5017 key scan.Door.
validateStackExpressionsis already on theobject_extensiondoor (#33072), so the roster and its pins do not move. The door's one pass reads only the written entry, and only names the entry adds are reported members there, so a cycle the target or a sibling package already carries, and a formula that only reads a cycle, are not charged to the extension write.packages/spec/src/migrations/entries/semantic/18.object-extension-formula-cycle-refused.ts: the ADR-0087 D3 entry, its one exception stated (inos buildon a declared target, a field two packages add at the priority that decides it is not judged; the door judges the written entry's own definition, see #33494). No D2 conversion: which read breaks the cycle is a fact only the author holds.content/docs/deployment/validating-metadata.mdx: the object extension paragraph names the cycle check, its location in the build, and the door's reading of a name the target already serves..changeset/33153-extension-formula-cycle.md:@objectstack/lintminor and@objectstack/specminor, both declaration lines, a BREAKING banner (Before / Now / Unchanged / Fix) andadr-0087: registered object-extension-formula-cycle-refused.Why
The engine computes an object's formulas, an extension's included, in dependency order; a formula on a cycle has no order, is never evaluated and reads null on every row. The cycle check named the object's declared formulas only, so the card's probe (an extension adding
a__c = record.a__c, ora__candb__creading each other) got 0 findings at both the build and the door.Tests
packages/lint/src/runtime-gate.object-formula-cycle.test.ts, new top-level describe#33153(11 cases) in the validate_expression and the object publish gate accept a formula that reads another formula field, and that formula reads null on every row #33100 harness file, at the real gate (runRuntimeAuthoringRuleswithtype: 'object_extension') and the build (runAuthoringRules('build', …)):formulaCycleMessage); the build's finding equals the door's (where and message).packages[j].manifest.objectExtensions[0]).packages/runtime/src/domains/meta-object-extension-item.test.ts, new describe[#33153](3 cases) through the realsaveMetaItem+publishMetaItem: a self-reading formula and a two-formula cycle each get 422INVALID_METADATAwith the build's rule id,whereandpathon each member, the field is not served and the row stays a draft; a chain publishes and is served.Red at base: the round-0 head tests over the base sources (
git restore --source=fc283d6dof the two lint files): 5 of 12 red (the file then held 12 cases), every LIT plus the sibling and undeclared-target cases; controls and the differential green. Restored withgit checkout HEAD --,git status --porcelainempty.Ablations (one-shot, through
node scripts/ablation-replace.mjson the committed head; the tool's restore verdict each time: blob equals HEAD,git diff HEADempty):formulaCycleIssues(!report.has(field)dropped, so every served member is reported): 5 of 12 red (round 0), including the differential case (the target's own cycle charged to the extension write) and the declared-member case (named twice). Lint tests readsrc/.base && !extensionWriteback tobase, the round-0 reading): 2 of 15 red, exactly shapes a and b; and the build-side exclusion (ownNamesemptied in the build too, behind an opaqueglobalThismarker): 1 red, shape a's build assertion (the member named twice). Both restored by the tool's verdict.bfa139ba: 3 of 16 red, shapes a and b and the tie/outranked pin; restored by the tool's verdict.globalThismarker), for the runtime pins that read lint throughdist/:pnpm --filter @objectstack/lint build,ablation-dist-preflightfound the marker in 4 built files, runtime-t 33153: 2 red (both LITs), the control green. Restore leg: lint rebuilt,--absentfound the marker in 0 of 20 built files, tree clean, 3/3 green.Acceptance notes
git grepofobjectExtensions/defineObjectExtensionoverexamples/,packages/(non-test) and HotCRMorigin/main: two entries, neither adds aformulafield.protocol-extension-redeclared-owner-field.tsdoes not judge an extension of an object its own package owns, so nothing else refused that shape).os buildon a declared target, is left out of the served body (objectsWithoutTiedFields) and so out of the cycle pass, as fix(lint)!: os build and the object_extension publish door judge the one-hop leaf through an extension-added lookup, and the regex and JSON-schema checks of an extension's validation rules #33398 leaves its hops unjudged: which definition is served is registration order. On an undeclared target the build takes the later entry's definition of a shared name and judges it.validate-expressions.ts(A flow or hook condition ordering record.created_at with no guard is refused at the publish door while os build accepts it #33246's),authoring-rules.tsand the door roster pins (the rule already runs on the door).Verification
Head
bfa139ba(basefc283d6d; review round 1 adds88b22a14and7c2eeb88over67ff848e, round 2 addsbfa139ba).origin/mainhas moved to811b976a;git diff --stat fc283d6d 811b976aover every file this PR touches is empty, so no merge was needed. #33246 (PR #33434) had not merged.Package tests
@objectstack/lint: 210/210 files, 8982/8982 tests;pnpm --filter @objectstack/lint typecheck(tsc + test typecheck) exit 0.@objectstack/runtimesrc/domains/meta-object-extension-item.test.ts: 46/46;pnpm --filter @objectstack/runtime typecheckexit 0.@objectstack/specsrc/migrations: 5 files, 198/198.Gates (dispatch-gates --commands, 109 lines)
The derivation at
bfa139bais 109 lines, the same list as at67ff848eand7c2eeb88. Every line ran after a whole-repopnpm buildofbfa139ba, its exit code captured before any pipe, and all 109 exited 0.dispatch-gates --ran:109 derived, 109 run, 0 NOT-MEASURED. The record ispm-orders/spec/33153-gates-ran-r2.txtin the project share (earlier rounds:33153-gates-ran.txt,33153-gates-ran-r1.txt).Dogfood (
Clause-②:carries the(narrowing)arm and the title!): full battery, the CI command (pnpm --filter @objectstack/dogfood testunder the verify lock), no shard, on headbfa139baafter a whole-repo build. Result:VERDICT command-exit 0, Test Files 465 passed and 3 skipped (468), Tests 3932 passed and 42 skipped (3974), no failures (the same counts as on67ff848eand7c2eeb88).Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code