Repository navigation
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
Conversation
…n adds The expression rule's hop checks (refuseUndeclaredHopField and the stored-metadata-body hop refusal) looked the reference up on the declared objects, so a misspelt leaf behind a lookup an objectExtensions entry adds passed os build and the object_extension publish door. They now read the object as served (typedObjects: extension fields folded in, a field two packages tie on left out), at both doors. The object walk's own field-reference verdict stays on the declared body. Adds the ADR-0087 semantic entry object-extension-added-lookup-hop-leaf-judged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5ny58hH1bcw97uPpgu
…JSON-schema checks validateRuleCompilability and validateRuleSchemaFormats share one rule walk, which read objects[].validations only. It now also walks each rule an objectExtensions entry adds, through the shared extension fold, located at the entry (object 'T' · objectExtensions[i] · validation 'N', path objectExtensions[i].validations.N.regex|schema), with the rule ids the object's own rules get. Both rules now run on the object_extension publish door; the gate's differential keeps the target's own stored rules off the extension write. Adds the ADR-0087 semantic entry object-extension-validation-rule-artifacts-judged, the one changeset for both fixes, and the validating-metadata docs row and paragraph this makes stale. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5ny58hH1bcw97uPpgu
📓 Docs Drift CheckThis PR changes 2 package(s): 3 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 — 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 17df209a0b03adf7d71ad6a35e2e3959e7a55eb8 && git checkout 17df209a0b03adf7d71ad6a35e2e3959e7a55eb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b63c894934b5e427b456b997774f48a220edb7e5 d190a7f0437f6d0d40f1a2357023b12ce5b04993 && git checkout -B drift-repro b63c894934b5e427b456b997774f48a220edb7e5 && git merge --no-ff d190a7f0437f6d0d40f1a2357023b12ce5b04993
node scripts/docs-audit/affected-docs.mjs --json b63c894934b5e427b456b997774f48a220edb7e5
|
Requested by Leehom · project thread
Fixes #33216
Fixes #33217
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 two shapes an object extension could carry before: a validation rule or formula readingrecord.REF.LEAFthrough a lookup an extension adds where the lookup's target serves noLEAF, and aformat/json_schemavalidation rule an extension adds whose regex or schema does not compile or names an unregistered format. No exported type or signature changes. Measured first-party exposure: 0objectExtensionsentries add a lookup or a validation rule (examples/app-showcaseone entry, three plain fields;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 rule-table row marks the extension door, and the object extension paragraph names both checks);skills/objectstack-datateaches one-hop reads,format/json_schemarules anddefineObjectExtension({ extend, fields })without saying an extension is exempt from either check, so the build and the door now give the verdict the object's own declarations already get.Precedent followed: #33154 (PR #33205), which built the extension walk these two cards extend and declared the same
no (narrowing)/narrowedpair.维护者速读
objectExtensions/ Studio 发布的object_extension)补上两项对象自身早已有的检查。① 通过“扩展新增的查找字段”做一跳读取(record.manager__c.字段)时,目标字段名现在按“对象实际提供的结构”判定,拼错即拒,和通过对象自身声明的查找字段一样(同一规则 idexpression-invalid、同一句提示)。② 扩展里format规则的正则、json_schema规则的 schema 及其 format 名,现在和对象自身规则一样检查(同三个规则 id),位置为object '对象' · objectExtensions[0] · validation '规则'。两项在os build与运行时发布口结论一致。packages/lint/src/validate-expressions.ts(refuseUndeclaredHopField与storedMetadataBodyReferencesFor改读typedObjects)、packages/lint/src/validate-rule-compilability.ts(walkObjectValidationRules新增的扩展循环)、packages/lint/src/authoring-rules.ts(两条规则的runtimeTypes加object_extension)。pnpm --filter @objectstack/lint exec vitest run src/runtime-gate.object-formula-writes.test.ts src/validate-rule-compilability.test.ts -t "33216|33217",预期 16 条全过:扩展查找字段后拼错的叶子字段、扩展的坏正则/坏 schema/拼错的 format 名在 build 与发布口都被拒且结论逐条相同,正确写法通过,对象自身的用例不变。What changes
Two commits, one per card.
#33216 —
packages/lint/src/validate-expressions.ts(hop-index region only).refuseUndeclaredHopFieldlooked the hopping reference up on the declaredobjects, so a lookup anobjectExtensionsentry adds was never found and its leaf never judged. It now readstypedObjects, the served fold lint: the field-name indexes outside indexObjectGraph (validate-expressions, indexObjectFields readers, hook-body-writes, sortable-fields, translation-references) still read declared objects only, refusing extension-added fields #30471 already builds (every extension field folded in, a field two packages tie on left out), the same foldfieldIndexreads for the leaf. A hop through a declared lookup reads the same definition as before, so the object's own verdicts and locations do not move.storedMetadataBodyReferencesFortakes aservedflag. The hop refusal (refuseStoredMetadataBodyHop) asks for the served reading, so a hop through an added lookup into a stored-metadata-body table is refused as through a declared one. The object walk's lint: a lookup or master_detail field whose reference is a stored-metadata-body table saves, builds and validates, but the engine never hydrates it — refuse it at authoring time #24677 field-reference verdict keeps the declared reading, so a field an extension adds is never charged to the object's own location. One map holds both readings (a served reading is keyedserved:NAME), so the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 receiver scan sees no new receiver.object_extensiondoor the written entry is folded into the door's live objects by the same fold, so the verdict is the build's.packages/spec/src/migrations/entries/semantic/18.object-extension-added-lookup-hop-leaf-judged.ts: the ADR-0087 D3 entry. No D2 conversion: which field the author meant is a fact only the author holds.#33217 —
packages/lint/src/validate-rule-compilability.ts,validate-rule-schema-formats.ts,authoring-rules.ts.walkObjectValidationRules(the one walk both rules share, so they cannot disagree about which rules exist) also walks each rule an extension adds, throughobjectExtensionsByTarget(the fold fix(lint)!: os build and the object_extension publish door judge each validation rule an object extension adds by the object's own rule pass #33205's extension walk reads: one entry per package per target, as the registry keeps them),conditionalbranches flattened in. Located at the entry:whereobject 'T' · objectExtensions[i] · validation 'N'(the locus fix(lint)!: os build and the object_extension publish door judge each validation rule an object extension adds by the object's own rule pass #33205 uses),pathobjectExtensions[i].validations.N.regex/.schema(the entry's path in place of the object'sobjects.T),packages[j].manifest.prefixed in a built artifact. The rule ids are unchanged. An undeclared target is judged all the same: a regex or schema needs no field of the target.validateRuleCompilabilityandvalidateRuleSchemaFormatscross onto theobject_extensiondoor (runtimeTypes: ['object', 'object_extension']), together, as their [finding] The fourviews[]visibility-predicate rules are CLI-only — a Studio/REST/MCPviewwrite bypasses all of them; if they move to runtime-publish, they must move together #7220 family discipline requires. On that write the snapshot carries the written entry as its sole extension; the target's own rules the door's live objects carry are judged in the gate's baseline as well, so the differential charges the write with its entry's findings only.packages/spec/src/migrations/entries/semantic/18.object-extension-validation-rule-artifacts-judged.ts: the ADR-0087 D3 entry.content/docs/deployment/validating-metadata.mdx: the "Declared enforcement that cannot run, declared on the object being written" row's runtime column now reads✓ᵒᵒˣ, and the object extension paragraph names both checks. Both statements were made false by this change (os-dev rule 4)..changeset/33216-extension-lookup-hop-and-rule-artifacts.md:@objectstack/lintminor and@objectstack/specminor, both declaration lines, a BREAKING banner (Before / Now / Unchanged / Fix) andadr-0087: registerednaming both entries.Tests
packages/lint/src/runtime-gate.object-formula-writes.test.ts, new describe#33216(7 cases), at the real gate (runRuntimeAuthoringRules) and the build (runAuthoringRules('build', …)):record.manager__c.daly_hoursthrough the added lookup is refused at the door atobjectExtensions[0].validations[0].then.condition, the build's findings deep-equal the door's, and the message equals the declared-lookup case's with the reference name taken out.sys_metadatais refused as a stored-metadata-body hop.packages/lint/src/validate-rule-compilability.test.ts, new describe#33217(9 cases): both rules on both doors; an invalid regex, an uncompilable schema and a misspelt format name an extension adds each refused at the build with rule id,whereandpath; each refused at the door with the build's finding byte for byte; aconditionalbranch located at the branch; the same rule ids as the object's own (whose locations are pinned unchanged); valid rules clean at both; a sibling package's entry located at its body and an undeclared target judged; the target's own broken rule not charged to the extension write; the object save door unchanged.packages/runtime/src/domains/meta-object-extension-item.test.ts, through the realsaveMetaItem+publishMetaItem:[#33216]4 cases (rule and formula: a misspelt leaf through the added lookup gets 422INVALID_METADATAwith the build's rule id, location and path, the field is not served and the row stays a draft; the correct leaf publishes and is served);[#33217]3 cases (an invalid regex and an uncompilable schema each get 422 with the build's rule id and locator, the rule is not served; valid ones publish and are served).object_extensiondoor's rules exactly): two inruntime-gate.object-formula-writes.test.ts(lint: the object_extension publish door runs no field-formula judgment, so a computed field os build refuses still publishes through PUT /meta/object_extension #33072), two invalidate-security-posture.runtime-surface.test.ts(security-role-word lint skips fields declared through objectExtensions, so a field named role passes validate, build and verify #31998; surface amendment 6881180427).Ablations (one-shot, nothing permanent; each through
node scripts/ablation-replace.mjson the committed head, thenpnpm --filter @objectstack/lint build, thenablation-dist-preflightfound the marker in 4 built files; runtime pins read lint throughdist/):for (const obj of typedObjects)→ declaredobjects): lint 3 of 94 red, runtime 2 of 40 red, exactly the five A one-hop read through a lookup an object extension adds is accepted with a misspelt target field, then refuses every save #33216 LIT cases (rule and formula at both doors, the object's own formula); every control green.storedMetadataBodyReferencesFor(objectName, true)→ declared): lint 1 red, the stored-metadata-body LIT; runtime 40/40.obj.validations ?? obj.validationRules别名读法(#5017 全包 grep 的第八处,第三个文件) #5096 receiver meta-test, which reds on the marker's ownentries.filterspelling, not on behaviour), runtime 2 red (the regex and schema door LITs); the controls, the differential case and the object door green.validateRuleCompilability'sruntimeTypesback to['object']): lint 7 red (the door LITs, the roster pins, therulesRuncontrols), runtime 2 red (the door LITs).git diff HEADis empty (the tool's verdict). Lint was rebuilt after the last leg; no marker remains inpackages/lint/dist(grep0 files for each of the four) andgit status --porcelainis empty.Acceptance notes
git grepofobjectExtensions/defineObjectExtensionoverexamples/,packages/(non-test) and HotCRMorigin/main: two entries, neither adds a lookup or a validation rule. Every test file inpackages/metadata-protocol(7 files, 251 tests) andpackages/runtime(7 files, 136 tests) that publishes or reads anobject_extensionpasses at the head.buildNullableFieldIndex) and theextensionWritestrip, which are 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; A formula field an object extension adds that reads itself, or two that read each other, is refused at neither the publish door nor os build #33153 (a formula cycle an extension adds) and Publish accepts an activityMilestones[].field or displayNameField that names no field; both are served and do nothing #33101 are not folded.reference) is now read as served at a hop site, which is what the engine plans. The object walk's own field-reference verdict still reads the declared field, so such a field is not charged to the object's location.defineStack({ objectExtensions })throughregisterApp) are not gated, as an object's own rules are not.Verification
Head
d190a7f0(base87330e89);git diff --stat 87330e89 origin/mainover every touched file is empty at hand-over time, so no merge was needed.Package tests
@objectstack/lint: 208/208 files, 8902/8902 tests; typecheck exit 0.meta-object-extension-item.test.ts+packages-disable-module-extension-fields.test.ts): 81/81.object_extension:@objectstack/metadata-protocol7 files 251/251;@objectstack/runtime7 files 136/136.Gates (dispatch-gates --commands, 109 lines)
The derivation at
d190a7f0is 109 lines. Every line ran with stdin closed and its exit code captured before any pipe. 107 exited 0 on the first pass. Two exited 3 (refused, nothing measured) on a missing build:pnpm --filter @objectstack/spec run check:skill-examples(no@objectstack/client-reactdist) andpnpm check:dual-build-cjs-loads(needs every package's dist). After a whole-repopnpm buildboth exited 0.dispatch-gates --ran:109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. The record ispm-orders/spec/33216-gates-ran.txtin the project share.Dogfood (the title carries
!andClause-②:the(narrowing)arm): full battery, the CI command (pnpm --filter @objectstack/dogfood testunder the verify lock), no shard, on headd190a7f0after a whole-repo build. Result:VERDICT command-exit 0, Test Files 464 passed and 3 skipped (467), Tests 3927 passed and 42 skipped (3969), no failures.Generated by Claude Code