Skip to content

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

Draft
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/project-thread-0m40gx
Draft

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/project-thread-0m40gx

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33216
Fixes #33217

Clause-②: no (narrowing) — os build, os validate, os lint and the runtime object_extension publish 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 reading record.REF.LEAF through a lookup an extension adds where the lookup's target serves no LEAF, and a format / json_schema validation 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: 0 objectExtensions entries add a lookup or a validation rule (examples/app-showcase one entry, three plain fields; packages/plugins/plugin-auth one, a secret field; HotCRM origin/main ac4dd3d has no objectExtensions in 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-data teaches one-hop reads, format / json_schema rules and defineObjectExtension({ 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) / narrowed pair.

维护者速读

  • 改了什么:对象扩展(objectExtensions / Studio 发布的 object_extension)补上两项对象自身早已有的检查。① 通过“扩展新增的查找字段”做一跳读取(record.manager__c.字段)时,目标字段名现在按“对象实际提供的结构”判定,拼错即拒,和通过对象自身声明的查找字段一样(同一规则 id expression-invalid、同一句提示)。② 扩展里 format 规则的正则、json_schema 规则的 schema 及其 format 名,现在和对象自身规则一样检查(同三个规则 id),位置为 object '对象' · objectExtensions[0] · validation '规则'。两项在 os build 与运行时发布口结论一致。
  • 为什么改:注册表把扩展字段和扩展校验规则合并到目标对象上,每次保存都会执行;此前拼错的一跳字段让公式恒为空、让校验规则拒绝每次保存,写错的正则或 schema 则在每次写入时被跳过,规则形同虚设,都要等最终用户保存时才暴露。
  • 风险与回滚:收窄;第一方实测 0 处扩展新增查找字段或校验规则,无既有元数据被新拒。对象自身的规则、公式与一跳读取的位置和结论不变;草稿保存不受影响,已存的行再次发布时才判。回滚即 revert。
  • 先看: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).

#33217 — packages/lint/src/validate-rule-compilability.ts, validate-rule-schema-formats.ts, authoring-rules.ts.

Tests

  1. 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', …)):
    • LIT: a rule reading record.manager__c.daly_hours through the added lookup is refused at the door at objectExtensions[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.
    • LIT: the same for a formula field the extension adds.
    • LIT: the object's OWN formula reading through a lookup a package's extension adds is refused at the build, at the object; its control (the declared leaf) is clean.
    • LIT: a hop through an added lookup to sys_metadata is refused as a stored-metadata-body hop.
    • CONTROL: the correct leaf through the added lookup is clean at both doors, rule and formula.
    • A lookup two packages add at the same priority is left unjudged.
    • CONTROL: the object's own hop through a declared lookup is refused at the object save door and the build, at its own location, as before.
  2. 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, where and path; each refused at the door with the build's finding byte for byte; a conditional branch 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.
  3. packages/runtime/src/domains/meta-object-extension-item.test.ts, through the real saveMetaItem + publishMetaItem: [#33216] 4 cases (rule and formula: a misspelt leaf through the added lookup gets 422 INVALID_METADATA with 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).
  4. Roster pins moved with An object extension's format and json_schema validation rules skip the regex and JSON-schema checks the object's own rules get #33217 (each lists the object_extension door's rules exactly): two in runtime-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 in validate-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.mjs on the committed head, then pnpm --filter @objectstack/lint build, then ablation-dist-preflight found the marker in 4 built files; runtime pins read lint through dist/):

Acceptance notes

Verification

Head d190a7f0 (base 87330e89); git diff --stat 87330e89 origin/main over 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.
  • Runtime door files (meta-object-extension-item.test.ts + packages-disable-module-extension-fields.test.ts): 81/81.
  • Every other test file that publishes an object_extension: @objectstack/metadata-protocol 7 files 251/251; @objectstack/runtime 7 files 136/136.
Gates (dispatch-gates --commands, 109 lines)

The derivation at d190a7f0 is 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-react dist) and pnpm check:dual-build-cjs-loads (needs every package's dist). After a whole-repo pnpm build both exited 0. dispatch-gates --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. The record is pm-orders/spec/33216-gates-ran.txt in the project share.

Dogfood (the title carries ! and Clause-②: the (narrowing) arm): full battery, the CI command (pnpm --filter @objectstack/dogfood test under the verify lock), no shard, on head d190a7f0 after 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 9 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/validate-rule-schema-formats.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/object-extensions.mdx (via object_extension (literal, a string literal in runtimeTypes))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES), object_extension (literal, a string literal in runtimeTypes))
  • content/docs/kernel/cluster.mdx (via object_extension (literal, a string literal in runtimeTypes))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-3.mdx (via object_extension (literal, a string literal in runtimeTypes))
  • content/docs/releases/v18/18-4.mdx (via object_extension (literal, a string literal in runtimeTypes))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/validate-rule-schema-formats.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 172 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b63c894934b5e427b456b997774f48a220edb7e5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 17df209a0b03adf7d71ad6a35e2e3959e7a55eb8 — the merge of head d190a7f0437f6d0d40f1a2357023b12ce5b04993 into base b63c894934b5e427b456b997774f48a220edb7e5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b63c894934b5e427b456b997774f48a220edb7e5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant