Skip to content

fix(lint)!: the remaining field indexes judge an object as served, and an object that declares no field keeps the list-view skip once extended (#30608) - #30659

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h8r47n
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h8r47n

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:spec#2 · os-sales · session_01L6bhgVSM5tJd2UYk3Aa1MM

Fixes #30608

Clause-②: yes (narrowing) — os validate / os build / os lint refuse shapes they accepted before, each a field an objectExtensions entry of the same artifact adds or re-declares, now judged on its served definition: a notify node's recipients.emailField naming an owner email field an extension re-declares as text is notify-email-field-not-email at error; a form open to anonymous intake whose section names an image / file field an extension adds, or an owner field an extension re-declares as one, is public-form-file-field at error; a list page leading allowedVisualizations with gallery over an owner image field an extension re-declares as text is page/visualization-without-binding at error; a searchableFields entry naming an owner text field an extension re-declares as formula is searchable-field-unsearchable at error; an autonumber field an extension adds whose format names a field the object lacks is autonumber-references-unknown-field at error, and a format naming an owner required: true field an extension re-declares without it is autonumber-references-optional-field at warning; a flow template {{ record.F.sub }} through an owner text field an extension re-declares as a lookup is flow-template-lookup-traversal at warning. The widening half: a form section, a notify email-field recipient, a flow template hole, a gallery or calendar binding, a searchableFields entry (an object's own set, a list view's narrowing, a react page's <ListView searchableFields>) or an autonumber format naming a field an objectExtensions entry adds is no longer refused as unknown, and a list view over an object that declares no field is no longer refused once an extension adds to it.
Metadata-protocol: narrowed
Skills-docs: none — measured at 2818af5b: git grep of the twelve moved rule ids over skills/ and content/docs/ hits public-form-file-field (content/docs/ui/forms.mdx:258, content/docs/ui/public-data-collection.mdx:55, skills/objectstack-api/SKILL.md:106), searchable-field-unknown / searchable-field-unsearchable (content/docs/data-modeling/schema-design.mdx:153,172, content/docs/ui/views.mdx:182-184, skills/objectstack-data/SKILL.md:128,154, skills/objectstack-ui/rules/list-views.md:207-208) and release pages only otherwise; none states a declared-only reading ("a file field", "a renamed / mistyped column"), so every sentence reads true. The patch round (887e5623) adds no rule id and changes no message.

维护者速读

  • 改了什么:lint 里剩下的七个字段索引(表单布局、通知邮件字段收件人、公开表单文件字段、流程模板、列表页可视化绑定、可搜索字段、自动编号格式)都改读“注册表实际提供的对象”,即声明字段加上同一制品里 objectExtensions 加的字段;React 页的 <ListView searchableFields> 也读同一份;列表视图规则对“一个字段都没声明的对象”恢复不判。
  • 为什么改:HotCRM 这类多包制品里,模块包给 app 包的对象加字段,运行时会提供这些字段,lint 却把引用它们报成“未知字段”;扩展重新声明的字段又按 owner 的旧定义判断,真正的问题被漏掉。
  • 风险与回滚:这是收窄。扩展重新声明或新加的字段按它的定义判断,会拒以前放行的几种写法:通知收件字段被改成 text、公开表单上出现图片/文件字段、画廊页的图片字段被改成 text、可搜字段被改成 formula、扩展自带的自动编号格式引用不存在的字段(以上为错误),以及自动编号引用的必填字段被改成非必填、流程模板穿过被改成 lookup 的字段(以上为警告)。两个包在同一优先级写同一字段时不判定义;一个字段都没声明的对象照旧不判。回滚 = revert 本 PR。
  • 先看:packages/lint/src/validate-searchable-fields.ts 的 indexServedSearchTargets(名字从带并列的 served 对象取、并列字段的定义置空占位、不声明字段对象的 null);packages/lint/src/validate-page-visualization-bindings.ts 的 skip 2 与 tiedOnObject;packages/lint/src/object-graph.test.ts 末尾的 [#30608] 一组。
  • 两分钟验证:运行 pnpm --filter @objectstack/lint exec vitest run src/object-graph.test.ts -t 30608,预期 42 例全绿;其中 moved 表 14 行各自断言 main 视角(无扩展)的旧结论和现在的新结论。
  • 需要你做的:无,席位落地。

What changes

One artifact, two legs (the union artifact and the per-package body, identical on every row): an app package declares xs_deal (name, plus the owner field a row names) and xs_account; a module package extends xs_deal.

Site Moved or kept Pin
validate-form-layout.ts field-name set moved to objectsAsServed; tie name kept (existence); an unextended object keeps its map-only read added passes, refused without, nowhere byte-for-byte
validate-notify-email-field-recipients.ts indexFieldTypes moved; a tied field is null (skipped); declares-nothing keeps its empty map existence row; two moved rows; tie; declares-nothing
validate-public-form-file-fields.ts indexFieldTypes moved, ties dropped; declares-nothing keeps its empty map three moved rows; tie; declares-nothing
validate-flow-template-paths.ts field map moved; a tied head is known with no type; root resolution and the provenance anchors stay on the declared object existence row; two moved rows; tie
validate-page-visualization-bindings.ts derivation moved, ties dropped, an object carrying a tie not reported on a miss; skip 2 asked of the declared object; own list views stay declared two moved rows; tie; declares-nothing
validate-searchable-fields.ts new indexServedSearchTargets: names read off the served objects with ties kept, then each tied field's meta blanked to a stub (so an object whose every served field is tied keeps a target and still refuses an unknown entry), declares-nothing kept null; validateSearchableFields reads it; indexObjectSearchTargets stays the declared primitive (docblock says why) two existence rows; moved row; tie; declares-nothing; the all-tied corner on the object and the list view, both registration orders, with its unextended and single-writer controls
validate-react-page-props.ts <ListView searchableFields> moved to indexServedSearchTargets (file surface amended by the seat) existence row; the PR #30533 "still declared" pin flipped in place
lint-autonumber-formats.ts walks and resolves the served fields, ties dropped (a tied reference is neither unknown nor optional); an object declaring no field is skipped existence row; four moved rows; tie; declares-nothing
validate-list-view-field-refs.ts (item 2) skip 2 asked of the declared graph: an object that declares no field stays null when extended item-2 pin with and without the extension, plus the declared control; declares-nothing row

Every finding that moves, against main

Measured with runAuthoringRules('validate', { normalized, parsed }) on origin/main a8390207 and on 2818af5b, union and per-package legs (identical on every case); 58 cases, every unlisted case unchanged (the no-extension controls and every nowhere control included). The last two rows were measured in the patch round on a8390207 and 887e5623 (the rule sources of each, through validateFlowTemplatePaths / validateFormLayout, both legs identical).

Stack (field added or re-declared by the extension) main this head
form section naming an added field (also on a tie) form-field-unknown warning nothing
notify emailField naming an added email field (also on a tie) notify-email-field-unknown error nothing
notify emailField naming an added text field notify-email-field-unknown error notify-email-field-not-email error
notify emailField naming an owner email field re-declared text nothing notify-email-field-not-email error
public form section naming an added image field (form-field-unknown warning) public-form-file-field error
public form section naming an owner text field re-declared file nothing public-form-file-field error
public form section naming an owner file field re-declared text public-form-file-field error nothing
flow template {{ record.region }}, added field (also {{ record.account.name }} on a tie) flow-template-unknown-field warning nothing
flow template {{ record.account.name }}, owner text re-declared lookup nothing flow-template-lookup-traversal warning
flow template {{ record.account.name }}, owner lookup re-declared text flow-template-lookup-traversal warning nothing
gallery page, added image field (also on a tie) page/visualization-without-binding error nothing
gallery page, owner image field re-declared text nothing page/visualization-without-binding error
searchableFields (object's own, list view's) naming an added field (also on a tie) searchable-field-unknown error nothing
object searchableFields naming an owner text field re-declared formula nothing searchable-field-unsearchable error
autonumber {region}{000}, added required field (also on a tie) autonumber-references-unknown-field error nothing
autonumber {region}{000}, added optional field autonumber-references-unknown-field error autonumber-references-optional-field warning
autonumber {plan}{000}, owner required field re-declared optional nothing autonumber-references-optional-field warning
an autonumber field the extension adds, format naming nothing nothing autonumber-references-unknown-field error
an owner autonumber re-declared text autonumber-references-unknown-field error nothing
list view over xs_empty (declares no field), extended (item 2) list-view-field-unknown error nothing
the same view over a declares-nothing object with a list searchableFields list-view-field-unknown error nothing
a form over a declares-nothing object, extended: the added field's entry form-field-unknown warning nothing (its other entries still warn, as on main)
a flow variable partner bound to an xs_account record ({{ partner.nowhere }}), where the extension adds a partner field to the trigger object flow-template-unknown-field warning nothing (the trigger scope now carries the served name, so partner is an ambiguous root, as a variable named after a declared trigger field is; a variable acct is still judged)
a form section naming legacy, an owner field declared in the legacy array fields shape, on an extended object form-field-unknown warning nothing (an extended object's names come off the served list, which reads both shapes; unextended, still form-field-unknown warning)

Verification

Patch round (head 887e5623)

The contract review (6670491743) asked for one code fix and two declarations. 887e5623: indexServedSearchTargets reads names off the served objects with ties kept and blanks the tied metas afterwards, so an object whose every served field is tied (the owner declares only title, two packages re-declare it) still refuses searchableFields: ['nowhere'] as main does; the changeset's Now / Not changed name the flow-variable root and the legacy array-shape moves (the last two rows of the table above). Same base a8390207, NODE_OPTIONS=--max-old-space-size=4096, heavy runs under os-verify-lock.

  • pnpm --filter @objectstack/lint test && pnpm --filter @objectstack/lint typecheck: Test Files 195 passed (195), Tests 8216 passed (8216); check:test-typecheck: OK; VERDICT command-exit 0. The [#30608] describe: 42 passed; object-graph.test.ts 200 passed.
  • Full dogfood battery: the closure build (VERDICT command-exit 0), then pnpm --filter @objectstack/dogfood test under the lock: VERDICT command-exit 0; Test Files 420 passed, 3 skipped (423); Tests 3504 passed, 36 skipped (3540).
  • npx eslint --no-inline-config over the 2 .ts files the round changes: exit 0.
  • dispatch-gates --commands at 887e5623: the same 57 commands as at 2818af5b, each run as printed, all 57 exit 0 (the seven dist/ from the first round still present). dispatch-gates --ran over <command> :: exit <code> lines: "57 derived, 57 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero, exit 0. check-changeset-no-major --base origin/main --event EVENT with this body as the pull_request payload: exit 0.
  • Ablation of the fix through scripts/ablation-replace.mjs, the target built from the tie-dropped objects with the tied names re-added (the 2818af5b code, inlined): 1 failed / 199 passed (the all-tied pin), restored, blob equal to HEAD, git diff HEAD empty.
  • origin/main is at b77c160a; not merged.

First round (head 2818af5b)

At head 2818af5b (base origin/main a8390207), NODE_OPTIONS=--max-old-space-size=4096, heavy runs under os-verify-lock.

  • pnpm --filter @objectstack/lint test && pnpm --filter @objectstack/lint typecheck: Test Files 195 passed (195), Tests 8210 passed, 5 skipped (8215); check:test-typecheck: OK; VERDICT command-exit 0. The [#30608] describe: 41 passed; object-graph.test.ts 199 passed.
  • Full dogfood battery (the (narrowing) arm owes it): the closure turbo run build --filter='@objectstack/dogfood^...' (VERDICT command-exit 0), then pnpm --filter @objectstack/dogfood test under the lock: VERDICT command-exit 0; Test Files 420 passed, 3 skipped (423); Tests 3504 passed, 36 skipped (3540).
  • npx eslint --no-inline-config over the 10 changed .ts files: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2818af5b: 57 derived commands, each run as printed with its exit code recorded. First pass: 56 exit 0 and pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET: seven packages with no dist/: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge); after building those seven under the lock it exits 0 (116 entry points across 71 packages load). check-plugin-teardown-shape.mjs --self-test ran after fetching its fixture commit 621a4876. dispatch-gates --ran: "57 derived, 57 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main and check-empty-changeset --base origin/main: exit 0.
  • origin/main moved to b8929f48 after the base (one spec docblock commit, no packages/lint file); not merged.
Ablations (each through scripts/ablation-replace.mjs: anchor hit once, object-graph.test.ts run, restored, blob equal to HEAD, git diff HEAD empty)

The lint tests import src (relative imports, no alias to dist), so no build sits between the mutation and the run. Back to the declared index, per site:

  • form-layout served = declared → 1 failed / 198 passed (the existence row).
  • notify objectsAsServed → declared objects, no ties → 4 failed (existence, two moved rows, tie).
  • public form served = declared → 4 failed (three moved rows, tie).
  • flow template field map over the declared object → 4 failed (existence, two moved rows, tie).
  • page visualization derivation over the declared object → 2 failed (two moved rows).
  • searchable indexServedSearchTargets → indexObjectSearchTargets → 4 failed (two existence rows, moved row, tie).
  • react page searchable index handed the declared objects → 2 failed (the existence row and the flipped PR fix(lint)!: the remaining field-name indexes judge an object as served, so a read or write of a field an objectExtensions entry adds is not refused (#30471) #30533 pin).
  • autonumber served = declared → 6 failed (existence, four moved rows, tie).
  • list-view declares-nothing restore removed → 2 failed (item 2 and the declares-nothing row).

Ties and declares-nothing guards, one leg each: page visualization tie guard off, notify / flow template / autonumber tie read as a definition, searchable tie stub dropped → 1 failed each (that site's tie row); notify, public form, page visualization (skip 2 on the served object), searchable (declared null dropped) and autonumber declares-nothing guards off → 1 failed each (that site's declares-nothing row). Two first attempts were refused by the tool (the replacement text already occurred in the file) and re-run with distinct replacements; both then went red.

Acceptance notes

  • The searchable index ruling. The ruling zone kept a react page's searchableFields refusal on an extension field unless the code shows the runtime search reads extension fields there. It does: the ingress gate's resolveQueryFields (packages/metadata-protocol/src/protocol-query-validation.ts:341) reads registry.getObject(object), which resolves through foldExtendersOntoDefinition (packages/objectql/src/registry.ts:6476), and the engine's search expansion reads the same schema. The seat re-read it on main and amended the file surface to validate-react-page-props.ts (6669203452); the PR fix(lint)!: the remaining field-name indexes judge an object as served, so a read or write of a field an objectExtensions entry adds is not refused (#30471) #30533 pin is flipped in place.
  • indexObjectSearchTargets stays declared. It is exported, the sort rule composes it (it hands it pre-served objects and reads its null off the declared ones), and out-of-repo callers read it; the served reading is a new function beside it.
  • Declares-nothing, two readings kept. The notify, public form, page visualization, searchable, autonumber and list-view rules skipped an object that declares no field on main and still do, extended or not, as the write, readonly and sort rules do after 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. The form-layout and flow template rules judged such an object against the empty set on main (every entry reported); they still do, with the extension's names now known. Moving them to a skip would be a further widening outside this card.
  • The public form rule now sees extension fields of image / file type on an object that declares fields, which is where its new refusals come from; on main such a field was the form-layout rule's form-field-unknown warning.
  • Other indexServedObjectGraph callers (sister card, carrier: seat). Measured in the patch round on built dist/ (pnpm --filter @objectstack/lint build of each commit's packages/lint/src, every other workspace package at this head), runAuthoringRules('validate', { normalized }) after normalizeStackInput, union and per-package legs identical: xs_empty declares fields: {}, an extension adds region, a record page's record:highlights binds ['region', 'nowhere'] and a dataset's dimensions name nowhere and region.
  • Visualization ties are per object, not per binding (should-fix 1, not adopted; the seat's ACCEPT records why).
  • rule-explanations.ts is not changed: none of the moved ids' explanations states a declared-only reading.
  • The runtime publish gates (a view write for form layout and public form, an object write for autonumber) hand the rules a snapshot; this PR measured the CLI legs only.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgVSM5tJd2UYk3Aa1MM

…d an object that declares no field keeps the list-view skip once extended (#30608)

The form-layout, notify email-field recipient, public form file-field, flow
template, page visualization binding, searchable-fields and autonumber
indexes read objectsAsServed: a field an objectExtensions entry adds is
known, a re-declared field is judged on the extension's definition, a tied
field is a served name judged on no definition, and an object that declares
no field stays unjudged where main skipped it. A react page's
<ListView searchableFields> reads the same served search index. The list-view
rule asks its declares-nothing skip of the declared object.

Claude-Session: https://claude.ai/code/session_01L6bhgVSM5tJd2UYk3Aa1MM
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/l 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 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 10 documentable anchor(s).

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

  • content/docs/releases/v12.mdx (via validateFormLayout (symbol, a top-level function))

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
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 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 — 4 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 a2437b560bd8872feece6ec8b57c6fdf21cff1f4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ea8d0fc9fd7f3f9dac1d29cd52930a75b36a4ab4 — the merge of head 887e562308b62646ad2d87b3b11a35050e88317b into base a2437b560bd8872feece6ec8b57c6fdf21cff1f4, 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 ea8d0fc9fd7f3f9dac1d29cd52930a75b36a4ab4 && git checkout ea8d0fc9fd7f3f9dac1d29cd52930a75b36a4ab4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a2437b560bd8872feece6ec8b57c6fdf21cff1f4 887e562308b62646ad2d87b3b11a35050e88317b && git checkout -B drift-repro a2437b560bd8872feece6ec8b57c6fdf21cff1f4 && git merge --no-ff 887e562308b62646ad2d87b3b11a35050e88317b

node scripts/docs-audit/affected-docs.mjs --json a2437b560bd8872feece6ec8b57c6fdf21cff1f4

⚠️ 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 a2437b560bd8872feece6ec8b57c6fdf21cff1f4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2818af5b8ca35de9b7b3fbef7ca90fc3a5aee134
Local-runs: none
Scope: packages/lint/src/lint-autonumber-formats.ts, packages/lint/src/validate-flow-template-paths.ts, packages/lint/src/validate-form-layout.ts, packages/lint/src/validate-list-view-field-refs.ts, packages/lint/src/validate-notify-email-field-recipients.ts, packages/lint/src/validate-page-visualization-bindings.ts, packages/lint/src/validate-public-form-file-fields.ts, packages/lint/src/validate-react-page-props.ts, packages/lint/src/validate-searchable-fields.ts, packages/lint/src/object-graph.test.ts (the narrowing face: reviewFace reads no face off the paths and the changeset's Clause-②: yes (narrowing) line decides it, so the floor is every path of the diff outside the prose face .changeset/30608-remaining-field-indexes-extension-fields.md, which is read in ② and not reviewed) + cited, read in the live tree at this head: packages/lint/src/object-graph.ts (recordsOf :215-221, graphObjectOf :329-352, indexObjectGraph :358-366, objectExtensionsByTarget :425-460, the fold's one-to-one contract :479-485, objectsAsServed :486-517, objectsWithoutTiedFields :828-836, indexServedObjectGraph :846-849); packages/lint/src/runtime-gate.ts :690-695 (CONTEXT_STACK_KEY_ORDER); packages/lint/src/index.ts :508-518, :789; packages/lint/src/rule-explanations.ts rows :2586, :2603, :3686, :4438, :4891; and at origin/main (unchanged since the merge-base a8390207): packages/objectql/src/registry.ts (mergeObjectDefinitions :876-889, the extend fold :5020-5066, reportRedeclaredFields :5111-5148), packages/metadata-protocol/src/protocol-query-validation.ts (resolveQueryFields :340-357); the Skills-docs: hits content/docs/ui/forms.mdx :252-262, content/docs/ui/public-data-collection.mdx :55, content/docs/data-modeling/schema-design.mdx :153, :172, content/docs/ui/views.mdx :182-184, skills/objectstack-api/SKILL.md :106, skills/objectstack-data/SKILL.md :128, :154, skills/objectstack-ui/rules/list-views.md :207-208. Inputs: card #30608 body and its 7 comments (6666495103, 6668866069, 6668915947, 6668965498, 6668993985, 6669203452, the dev report 6670038297); PR #30659 body, its 11-file list, its one comment (the docs-drift bot 6670048882); the net diff against merge-base a8390207 (11 files, +530 −58); the check-runs on this head via REST (filter=all). PR #30533's records 6665186290 and 6666860927 were read for the family contract, not adopted.

① Derived judgments

  1. The positional join holds at every one of the seven new sites. Form layout (:160-166), notify (:101-118), public form (:95-111), flow template (:508-515), page visualization (:418-429), autonumber (:71-87) and the searchable index (:374-392, by name) each pair recordsOf(stack.objects)[i] with objectsAsServed(stack).objects[i] or objectsWithoutTiedFields(...)[i]. The fold's contract (:479-485) is stated and true of the body: objects.map (:492), the early return hands back the same array (:490), and objectsWithoutTiedFields maps one to one (:830). The form-layout rule's identity test served[i] !== obj (:160) additionally rests on the fold returning the declared record itself for an object nothing extends (:495) and on the early return; both hold, so an unextended object keeps its map-only read byte for byte. Right.
  2. Every widening is right against the runtime. A field an objectExtensions entry adds is served: mergeObjectDefinitions folds { ...base.fields, ...extension.fields } (:888) and the fold mirrors it (fields.set(key, f) :510). The form renderer, the notify node's runtime half, the flow run's record, the page derivation, the autonumber mint and the search ingress gate (resolveQueryFields reads registry.getObject(object) :343, the folded object) all read that merged object, so each existence refusal that goes to nothing on the head was a refusal of a name the runtime serves. The react page's ListView searchableFields override reaches the same gate, so the PR fix(lint)!: the remaining field-name indexes judge an object as served, so a read or write of a field an objectExtensions entry adds is not refused (#30471) #30533 pin flipped in place (:1067-1077) is right, and the seat's file-surface amendment (6669203452) is honoured by the file list.
  3. Every narrowing judges the definition the runtime serves, and none refuses a shape the runtime would serve differently. On a re-declared owner field the registry serves the extension's definition — the fold replaces whole (:888, :510) and reportRedeclaredFields only warns, naming "the definition served is the one from" the last writer (:5141-5146); nothing refuses the registration. So a notify emailField on an owner email re-declared text, a public form over an owner text re-declared file, a gallery over an owner image re-declared text, a searchableFields entry on an owner text re-declared formula, an autonumber format over an owner required field re-declared without it, an extension-added autonumber whose format names nothing, and a template hop through an owner text re-declared lookup are each judged on the served definition, and the inverse re-declarations pass (three rows). All seven are in the Clause-② line, and each is a moved row with the declared-only control (:1294-1342) on both CLI legs. Right.
  4. Ties are existence-only at every site, as the 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 contract has it. Definition reads drop them (objectsWithoutTiedFields in public form :98, page visualization :423, searchable :379, autonumber :74; null in notify :113 and :196; '' in flow template :527, where RELATION_TYPES.has('') is false :682 so no traversal is asked; null in autonumber :87 and :133); existence keeps the name (notify's null is known, flow template types.has, form layout's set, autonumber's null, the searchable stub {} :385, which on a narrowing lands on the auto-default pass's "no readable type — unresolvable" continue :591 and on the declared branch survives the existence filter as a system column does). Pinned in both orders on six sites (:1349-1371). Right, with one over-broad guard (④, should-fix 1) and one corner where the stub is lost (④, should-fix 2).
  5. Declares-nothing is kept where main skipped it, and the two rules that judged against the empty set still do. Notify (:108, declaresAny), public form (:103), page visualization (skip 2 asked of the declared object :452; derivableFields returns undefined on zero names :262), searchable (declared null restored :388-390), autonumber (:78) and the list-view rule (item 2: declaredGraph.get(name) === null ? null : served :974-977, where graphObjectOf returns null on zero names :340) all skip an object that declares no field, extended or not; pinned on six legs (:1374-1392) and the item-2 pin with and without the extension plus the declared control (:1394-1409). Form layout (known is an empty set, every entry warned :242) and flow template (isKnown false → warning :633) judged such an object against the empty set on main and still do, with the extension's names now known — the dev's reading is right, and moving them to a skip would be a further widening outside the card. The list-view rule's option map (:1084) reads the served objects, but a declares-nothing object's graph entry is null and the view is left at :1336 before the options are read, so the restore is whole.
  6. The runtime publish doors do not move. CONTEXT_STACK_KEY_ORDER is objects, permissions, books, datasets (:690-695): the snapshot a view or object write hands these rules carries no objectExtensions and no packages[], so objectsAsServed takes its early return (:490) and every moved index equals the declared one at PUT /api/v1/meta/view and /object. The narrowing is the CLI's, as the body says; the dev's unmeasured-doors flag is answered by reading (③).
  7. No public surface moves. packages/lint/src/index.ts exports validateSearchableFields and the three ids (:508-518), neither indexObjectSearchTargets nor the new indexServedSearchTargets; indexObjectGraph (:789), lintAutonumberFormats and validateFormLayout keep their signatures. indexObjectSearchTargets is unchanged in behaviour, so the sort rule's composition (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) is untouched; its docblock's "out-of-repo callers of this exported core" reaches deep importers only (a note, not a finding).
  8. The card's fences hold. No packages/spec file, no runtime file, none of the indexes 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 moved (validate-expressions.ts, the write and readonly rules, validate-sortable-fields.ts, validate-translation-references.ts) is in the file list; object-graph.ts is not touched; rule-explanations.ts is not touched and none of the moved ids' rows states a declared-only reading (:2586 and :2603 speak of the object's declared searchableFields, a different noun; :4891 "declaration order" is the fold's order too). content/docs/releases/v12.mdx, which the drift bot names, is not edited.

② Semver level

@objectstack/lint minor with a BREAKING banner, ! in the title and the changeset summary — right, the pair PR #30533 (482e089a) set for this family. The changeset carries Clause-②: yes (narrowing) and Metadata-protocol: narrowed with the value first after each colon, the widening half written out, and one ADR-0087 marker, not-required (no-migration-prescription), whose reason reads true (no schema key moves; each refusal names a served definition the runtime already refuses or misrenders, and the remedy is the author's). Check Changeset and the PR-body guards concluded success on this head. Every refusal the head adds over main is declared (①.3: the seven shapes, and the generic "judged on its served definition" clause covers the same shape on the other visualizations and the hidden / credential arms of the searchable rule); no declared refusal is absent from the tree — the "Every finding that moves" table matches the moved, tied and unjudged pins row for row. Two widenings are not in the table (④, should-fix 3): the flow-template trigger-scope collision and the form-layout legacy-array read on an extended object. Neither moves the arm, the kind or the level. Metadata-protocol: narrowed with the runtime doors unmoved (①.6) is the family's reading (the protocol's rules at the CLI doors); kept as #30471 declared it. The Skills-docs: none line is true: each hit was read, and none states a declared-only reading ("a form … whose sections declare such a field", "a renamed / mistyped column", "a real column outside the allowed set").

③ Boundary flags

Dev report 6670038297: open_questions none; security_findings none. out_of_scope_findings:

Elsewhere in the report: the ablation tool's two refusals ("replacement already present", re-run) leave no trace in the diff; mcp_calls: 5, no write tool; the check:dual-build-cjs-loads exit 3 was a missing-dist prerequisite, exit 0 after building — not a verdict on the diff. The seat's two claim amendments (branch 6668993985, file surface 6669203452) are matched by the head's branch and file list; the withdrawn spec-seat-3 claim (6668915947, released 6668965498) has no effect on this record. The drift bot's one row is a release page, read-only and untouched. Added by this review: the sister gap in ③'s first bullet, and should-fix 1-3 below.

④ Design judgment

Judged against the scenario of record — an app package's object extended by a module package's objectExtensions, served merged — and the code each hunk cites:

  • validate-form-layout.ts — served names for an extended object, the map-only read kept for an unextended one. Holds, with one inconsistency (should-fix 3b): an owner whose fields is the legacy array shape is read as declaring nothing on main (map-only) and on the head when unextended, but as declaring its array once an extension touches it (:161, the fold's list), so the same section entry warns or not by whether a sibling extends the object.
  • validate-notify-email-field-recipients.ts — null for a tie, the empty map for declares-nothing, the runtime half's live object the merged one. Holds.
  • validate-public-form-file-fields.ts — ties dropped, declares-nothing kept; FILE_REFERENCE_TYPES judged on the served type. Holds.
  • validate-flow-template-paths.ts — roots and provenance on the declared object, the field map on the served one, a tied head typed ''. Holds. One consequence outside the table (should-fix 3a): triggerScope (:574-576) now carries the served names, so a flow variable named like an extension-added field is an ambiguous root (:481) and its holes go unjudged where main judged them — right, since the run flattens the merged record's fields to top level, but a widening the declaration does not name.
  • validate-page-visualization-bindings.ts — skip 2 of the declared object, the derivation over the served object without ties. The tie guard is per object, not per binding (should-fix 1): tiedOnObject (:454) silences every miss of every visualization on the page (:481) whenever any field of the object is tied, even when neither writer's candidate could bind — two packages tying on notes: text with no image field anywhere leaves a leading gallery unreported, and every visitor lands on its refusal screen. The changeset declares it ("an object carrying one is not reported"), so it is a declared widening, not a FAIL; it is coarser than the question, and the precise skip (a tied field whose candidate definition from either writer is of the predicate's types) needs the candidates, which objectsAsServed does not return.
  • validate-searchable-fields.ts — the declared primitive kept, the served index composed beside it, the walk on declared objects so paths hold. Holds, with one corner (should-fix 2): when the tie covers every served field (an owner declaring exactly title, two extensions re-declaring it), objectsWithoutTiedFields leaves no name, declaredFieldTarget answers null, the !target guard (:382) skips the stub, and the declared restore (:388-390) does not fire (the owner declares a field), so the object is null and a searchableFields entry naming nothing (nowhere) is unrefused where main refuses it. Contrived, in the accepting direction, unmeasured.
  • validate-react-page-props.ts — one import (:57) and one call (:1162) moved; the two search surfaces read one index. Holds.
  • lint-autonumber-formats.ts — an extension-added autonumber walked, a tied one not, an owner autonumber re-declared text minting nothing, the declares-nothing skip. Holds.
  • validate-list-view-field-refs.ts — the served graph with the declared null restored per object; the control (a declared object's unknown column still refused at the same path) pinned. Holds.
  • object-graph.test.ts — 41 cases on both legs, every moved row carrying the declared-only control. Holds; the react pin flipped with its reason.

Must-fix: none.

Should-fix: (1) packages/lint/src/validate-page-visualization-bindings.ts:454,481 — narrow the tie guard to a tied field whose candidate definition (either writer's) is of the predicate's types, so a tie that cannot bind does not silence a real miss; this PR, or the family's next card with the fold widened to return the tied candidates. (2) packages/lint/src/validate-searchable-fields.ts:379-390 — seed the target's names from the served objects before dropping the tied metas (or build a target when the served-without-ties answer is null and the tie set is non-empty), and pin the all-tied corner; this PR. (3) declaration — one clause each in the changeset's "Now" / "Not changed" lists and the moved-findings table for (a) packages/lint/src/validate-flow-template-paths.ts:574-576, a flow variable colliding with an extension-added field is now an ambiguous root, and (b) packages/lint/src/validate-form-layout.ts:160-163, an extended owner's legacy array-shaped fields are now known (and still not when unextended — or read the array shape on both arms); this PR or the seat's amendment. (4) the sister card of ③ (the declares-nothing restore for the other indexServedObjectGraph callers) — filed by the seat, not this PR.

Gate verdicts on this head, read at 2026-11-05T21:50Z after every run had completed (the last landed one minute before the read): 63 check-runs (filter=all), 63 distinct names; 54 success, 9 skipped, 0 failure, 0 pending. The required contexts Lint & Repo Gates, Type Check · workspace, Test Core (8 shards), Build Core, Governed Surface Queue Guard, Type Check UI, Test UI (16 shards), Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Check Changeset, Check PR Size and the PR-body guards are success; the nine skipped are path-filtered or opt-in (Lint UI, Build Docs, UI Published Artifacts, Dogfood Verify CLI, the dogfood matrix shard, Temporal Conformance, the packed-tarball smoke, the 18.x harness, the schedule gate). No governed surface in the file list; 588 changed lines. Not re-run locally.

Implemented-by: claude/project-thread-h8r47n
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

…n all-tied object still refuses an unknown entry (#30608)

A searchable target built from the tie-dropped objects was null when every
served field was tied, which turned the existence check off. Names now come
from the served objects with ties kept, and the tied metas are blanked
afterwards. The changeset also records two moves the review measured: a flow
variable named after an extension-added trigger field is an ambiguous root,
and an extended owner's legacy array fields are known names to form layout.

Claude-Session: https://claude.ai/code/session_01L6bhgVSM5tJd2UYk3Aa1MM
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review — delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 887e562308b62646ad2d87b3b11a35050e88317b
Local-runs: none
Scope: packages/lint/src/validate-searchable-fields.ts, packages/lint/src/object-graph.test.ts (the delta since the reviewed head 2818af5b, record 6670491743: one commit 887e5623, 3 files, +44 −9, the third being the prose face .changeset/30608-remaining-field-indexes-extension-fields.md, read in ② and not reviewed) + cited, read in the live tree at this head: packages/lint/src/validate-searchable-fields.ts (declaredFieldTarget :256-275, resolveAllowedSet :300-320, indexObjectSearchTargets :340-350, indexServedSearchTargets :374-392, checkSearchableFieldList :410-632 with the auto-default continue :591); packages/lint/src/object-graph.ts (objectsAsServed :486-517, suggestName :761-764), packages/lint/src/validate-flow-template-paths.ts :481, :574-576 and packages/lint/src/validate-form-layout.ts :160-163, all three byte-identical to 2818af5b (git diff --quiet). Every other file of the PR is unchanged since 2818af5b (git diff --stat 2818af5b 887e5623: the three above and nothing else) and carries the judgment of 6670491743. Inputs: the dev's patch-round report 6671358427 on card #30608; the PR body as patched by the seat to r2 (read from the PR); the delta diff; the check-runs on this head via REST (filter=all).

① Derived judgments

  1. The all-tied corner now refuses an unknown entry as main does. indexServedSearchTargets builds the target over served.objects with ties kept (:382), so an owner declaring only title with two packages re-declaring it has names = {title} and a target, where 2818af5b built it from the tie-dropped objects and answered null; searchableFields: ['nowhere'] is searchable-field-unknown at error on the object's own set and a list view's narrowing, both registration orders, both legs, with the unextended control (:1375-1402). Right.
  2. A tied name stays existence-only. Each tied field's meta is blanked to {} afterwards (:383-386): the virtual and credential checks read no type, and on a narrowing over an auto-default object the admissibility pass leaves on "no readable type — unresolvable" (:591); on a declared set the stub survives the existence filter as a system column's does (resolveAllowedSet). The pin's title entry answers nothing in both orders, and the single-writer control (formula alone) still draws searchable-field-unsearchable (:1398-1400). Right.
  3. Nothing else moves against 2818af5b. For every object that is not all-tied, the name set is the same set (non-tied names plus the tied ones the old loop re-added) and the tied metas are the same {}; the declared-null restore is unchanged (:387-389), so a declares-nothing object stays unjudged, extended or not (the unjudged pin). objectsWithoutTiedFields is no longer imported here and has its other callers untouched. The one byte-level difference is the insertion order of names (fold order rather than non-tied-then-tied), which only suggestName (:761-764, a single nearest name) could read; no verdict, path or severity depends on it.
  4. The two declaration clauses are true against the tree. (a) validate-flow-template-paths.ts :574-576 seeds triggerScope from the served field map, and :481 drops a variable root in it, so a flow variable named after a field an extension adds to the trigger object is ambiguous and its holes unjudged, while a non-colliding variable is judged as before — the changeset's "Now" bullet and the "Not changed" sentence say exactly that, and the moved-table row ({{ partner.nowhere }}, control acct) is what the code gives. (b) validate-form-layout.ts :160-163 reads an extended object's names off the served list (both shapes) and an unextended one's off map-shaped fields only, so an owner's legacy array fields are known once extended and still reported form-field-unknown when not — the second bullet, the "Not changed" sentence and the table row match. Neither clause has a pin; both were measured by the dev on both legs (6671358427), and the first record asked for declarations only.

② Semver level

Unchanged: @objectstack/lint minor, ! in the title and the changeset summary, Clause-②: yes (narrowing) and Metadata-protocol: narrowed first after each colon, one ADR-0087 marker not-required (no-migration-prescription). The patch adds no rule id, changes no message and no finding path (the Skills-docs: none line's new sentence is true), and the two new "Now" bullets are widenings, so the arm, the kind and the level stand. The PR body r2 carries the same two rows and the rewritten searchable row; the three carriers agree with each other and with the tree.

③ Boundary flags

Dev report 6671358427: open_questions none; security_findings none. out_of_scope_findings: validate-dataset-references judges an extended declares-nothing object on the extension's fields alone — the dev's reachability probe for should-fix 4 (dataset-field-unknown at error when extended, nothing when not, identical on 887e5623, origin/main and 482e089a^), which confirms the gap predates PR #30533 and is the sister card's; validate-page-field-bindings is shown not to be an instance (no declares-nothing skip on either arm). The seat is dispositioning the card. Should-fix 1 of 6670491743 (the per-object visualization tie guard) was declined by the seat with a reason that holds — the widening is declared in the changeset's "Not judged", and the precise skip needs tied candidates from the shared fold, which is #30471's — so it is carried to the family's card, not this PR; the PR body's acceptance notes say so. Should-fix 2 and 3 are answered by this delta (①.1-4). The dev's ablation (the 2818af5b construction inlined → 1 failed, the new pin; the stub re-add dropped → the same 1) leaves no trace in the diff.

④ Design judgment

  • validate-searchable-fields.ts indexServedSearchTargets — names off the served objects first, tied metas blanked second, the declared null restored last. The order is the one that keeps every question answered by the reader that owns it: existence by the served names, definition by the single writer's meta or by nothing, judgeability by the declaration. Holds; the docblock's bullet is rewritten to match.
  • object-graph.test.ts — the new pin walks both surfaces, both registration orders, both legs, with the unextended and single-writer controls. Holds.
  • The changeset — the two clauses and the "Not changed" sentences are what :481/:574-576 and :160-163 do. Holds.

Must-fix: none.

Should-fix: none new. Carried: should-fix 1 of 6670491743 to the family's card (the seat's disposition); should-fix 4 (the sister card) with the dev's measurement attached.

Gate verdicts on this head as they stand, read at 2026-11-05T22:41Z (the push is minutes old): 63 check-runs (filter=all), 59 distinct names; 11 success, 11 skipped, 0 failure, 37 pending. Concluded success: Check Changeset (on the r2 body), Governed Surface Queue Guard, the PR-body guards, Dogfood Regression Gate, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Type Check · source gates, Build UI Deps, Plan Test Core shards, Flag docs affected by code changes, filter. Still pending at the read: Build Core, Test Core (8 shards), Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Lint · ESLint, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Type Check UI, Test UI (16 shards), Build & E2E UI, Live E2E UI. The skipped are the path-filtered and opt-in runs as on 2818af5b. The verdict below is on the delta's tree; the pending contexts are the merge gate's to conclude — on 2818af5b, whose tree differs from this head by the three files above, every one of them concluded success (6670491743), and the dev reports the package's full test and typecheck green at this head (195 files, 8216 tests). Not re-run locally.

Implemented-by: claude/project-thread-h8r47n
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

维护者速读

  • 改了什么:lint 里剩下的七个字段索引(表单布局、通知邮件收件人字段、公开表单文件字段、流程模板路径、页面可视化绑定、可搜索字段、自动编号格式)和 react 页的 searchableFields,改成按 registry 实际服务的对象判定,也就是声明字段加上同一产物里 objectExtensions 加的字段。不声明字段的对象被扩展后,列表视图规则仍然不判它。
  • 为什么改:多包产物(如 HotCRM)里,模块包给应用包的对象加字段,os validate 却按“字段不存在”拒掉引用它的表单、通知和搜索配置,运行时其实能服务这些字段。
  • 风险与回滚:两头都动。扩展加的字段不再被报未知(放宽);扩展重声明的字段改按服务时的定义判,例如把 email 重声明成 text 后,通知收件人会被拒(收窄)。@objectstack/lint 出 minor,标 !,七种新拒绝都写进了 changeset。两个扩展对同一字段打平时只判存在,不判定义。回滚即 revert 本 PR。
  • 先看:packages/lint/src/validate-searchable-fields.ts 的 indexServedSearchTargets → validate-page-visualization-bindings.ts 的 skip 2 与 tiedOnObject → object-graph.test.ts 末尾的 [#30608] 一组。
  • 两分钟验证:pnpm --filter @objectstack/lint exec vitest run src/object-graph.test.ts -t 30608,预期 42 例全绿;把这几个索引指回 recordsOf(stack.objects),新 pin 转红。
  • 需要你做的:无,席位落地。
  • 席位意见:接受。首轮契约复核 PASS(6670491743,Head-sha 2818af5b),四条 should-fix:采纳两条,在补丁轮落地;补丁轮后 delta 复核 PASS(6671443375,Head-sha 887e5623),没有新发现。另两条不采纳,理由见第 5 项。

席位核对

ACCEPT · head 887e562308b62646ad2d87b3b11a35050e88317b · 2026-11-05T22:43Z

  1. 形态:
  2. 前提实测:认领时在 origin/main ca37605c 上读到这七处都建在 recordsOf(stack.objects) 上(validate-form-layout.ts:143 等,见认领),objectsAsServed、indexServedObjectGraph 已在 main。dev 在 main a8390207 和本分支上用构建产物跑 runAuthoringRules('validate', …),联合产物和逐包两条腿结果一致,共 58 例,移动的结论逐行列在正文的 moved-findings 表里。复核在 registry.ts 5141-5146 读到,运行时对扩展重声明的定义只给警告、照样服务,所以按服务时定义判不会拒掉运行时会给出别的形态的东西。
  3. 门禁 check-runs:
    • head 887e5623 读时 23 success、11 skipped、30 仍在跑,0 failure。全部跑完且无红才 pr_ready + automerge_enable。
    • 首轮复核 6670491743 和 delta 复核 6671443375 都在 Served-tier: CONTRACT_REVIEW_TIER 下 PASS,范围是散文面外的全部 diff(收窄面)。delta 复核核到 2818af5b..887e5623 只动了 changeset、validate-searchable-fields.ts、object-graph.test.ts 三个文件,其余文件沿用首轮判断。
    • dev 本地补丁轮:lint 195 个文件 / 8216 条测试通过,typecheck OK;锁下 dogfood 420 个文件通过、3 个跳过((narrowing) 欠的这一项,command-exit 0);57 条派生门禁全部 exit 0,--ran 对账 0 NOT-MEASURED;check-changeset-no-major 用 r2 正文 exit 0;eslint exit 0。
    • 消融:首轮 21 条把索引指回声明对象,全部转红后复原;补丁轮把全 tie 修复内联回 2818af5b 的写法,恰好新 pin 1 条转红,复原后 blob 等于 HEAD。
  4. 席位自读(收窄面,没有余部;散文面逐句核):
    • changeset 逐句核过:@objectstack/lint minor,摘要带 !,Clause-②: yes (narrowing) / Metadata-protocol: narrowed,冒号后第一个就是取值,与认领一致;七种新拒绝逐条点名规则 id 和级别,与 moved 表一一对应;补丁轮加的两句(流程变量与扩展加的字段同名时成了歧义根、被扩展 owner 的旧数组形 fields 现在算已知)与 delta 复核读到的代码一致。
    • PR 正文已由席位按 dev 的 r2 经 relay 更新(回读一致),Skills-docs: none — … 一行属实:复核逐条读了命中的 skills/** 和 content/docs/** 句子,没有一句写的是“只看声明字段”。
  5. 接受的偏差:
  6. 范围外去向和 Skills-docs::Skills-docs: none — … 行在,属实(见第 4 项)。范围外只有第 5 项的两条,都 dropped。
  7. 落地条款:check-governed-merges.mjs --pr 30659 已在本 head 读过:11 个路径 0 个命中 governed 登记,623 行(+565 / -58)≤ 3000,属于 Tier S;落地前再读一次。CI 全绿后经 relay pr_ready + automerge_enable,跟到 MERGED,同一动作里摘 pm:dispatched。

Generated by Claude Code

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