Skip to content

docs(skills,spec): the fieldPatches example patches options only on a field that declares them, so HotCRM accepts it - #30995

Merged
os-zhuang merged 6 commits into
mainfrom
claude/project-thread-kv932h
Nov 6, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/project-thread-kv932h

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Irene · project thread

Fixes #30944

Clause-②: no
Metadata-protocol: none

dispatch-gates mandates the contract-review model tier for skills/** (clause ①). The maintainer's standing order in this project (2026-11-03) waives it; this PR was developed below that tier, and the dispatching seat runs the contract review.

维护者速读

  • 改了什么:fieldPatches 的唯一示例改为只改 industry 的 label,选项补丁移到自带 inline options 的 type(customer → Client);覆盖 objectstack-data 技能、spec 的 fieldPatchesDescription() 与 TSDoc、object-extensions.mdx,references/data/object.mdx 由 spec 重新生成。
  • 为什么改:HotCRM 的 crm_account.industry 绑定共享 picklist industry,fieldPatchTargetMisses 对其任何 options 键返回 picklist-option,照抄旧示例会在安装和 os validate 时被拒。
  • 风险与回滚:只改说明文字和示例,无 schema、键或行为变化;发布 @objectstack/skills 与 @objectstack/spec 各一个 patch;回滚即 revert。
  • Token 账:SKILL.md 示例多 10 字节,删掉同文件 .custom 段末重复的 "a base field changes only through fieldPatches" 付账;上限行不动(6068/6078)。
  • 先看:skills/objectstack-data/SKILL.md L364 与 packages/spec/src/data/object.zod.ts fieldPatchesDescription()。
  • 需要你做的:无;席位负责复审与落地。

What changes

  • skills/objectstack-data/SKILL.md L364: the example is now defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } }). extend: 'crm_account' and label: 'Sector' stay whole tokens (eval cases 5 and 7 of evals/objects-fields-relationships.json require them). The sentence before it still states that "a picklist-bound field's options are never patchable".
  • packages/spec/src/data/object.zod.ts, description text only: fieldPatchesDescription() now says an option of a field bound to a shared picklist is never patchable and only a field declaring its own options takes an option patch, and its example sets industry's label and inlineHelpText and patches type's customer option. The ObjectExtensionSchema TSDoc example carries the same patch; since that example extends contact, its comment is conditional: where industry is picklist-bound, as on HotCRM's crm_account, its options are not patchable (its display keys are). No schema change.
  • content/docs/references/data/object.mdx: regenerated with pnpm --filter @objectstack/spec gen:docs after pnpm --filter @objectstack/spec build; not hand-edited.
  • content/docs/data-modeling/object-extensions.mdx L104-118: the os:check example drops industry's options (a comment says it takes them from a shared picklist) and patches type: { options: { customer: { label: 'Client' } } }. Its existing bullet on picklist-bound fields (L152-153) now agrees with the example.
  • Two changesets: '@objectstack/skills': patch and '@objectstack/spec': patch, each with Clause-②: no and Metadata-protocol: none. No skip-changeset.

Read on HotCRM main at 6961ab1, src/sales/objects/account.object.ts:

  • L311-318: industry: Field.select({ label: 'Industry', group: 'basic', picklist: 'industry' }), no inline options (the values, technology among them, live in src/sales/picklists/industry.picklist.ts L23).
  • L300-309: type: Field.select({ label: 'Account Type', …, options: [...] }) declares its own inline options; L305 is { label: 'Customer', value: 'customer', … }, so customer is a stored value the patch can address.

Read on objectstack main: fieldPatchTargetMisses in packages/metadata-core/src/extension-field-patch-targets.ts pushes reason: 'picklist-option' for every key under a patch's options when the target field has a non-empty picklist, before it looks at option values; a field with inline options is checked by stored value (reason: 'option' when absent). The new examples pass both checks.

Out of scope and left as is: the options shape error message in object.zod.ts ('options: { technology: { label: \'Tech\' } }', near L5034) illustrates the map shape, not a HotCRM patch.

Token ratchet. skills/objectstack-data/SKILL.md: after merging main at acd190b2 the ceiling is 6078 with 1 byte of slack. The new example adds 10 bytes. Payment, by the seat's ruling: the .custom paragraph's closing clause "; a base field changes only through fieldPatches" (L372 on acd190b2) is deleted. Its home is L355-360 of the same file: "Relabel a field the owner declares with fieldPatches — never by redeclaring it in fields" and "the type, values and rules stay the owner's". The file is now 6068 tokens (ceiling 6078, headroom 10). No ratchet row moves, nothing was re-wrapped. Net lines in skills/**: 0.

Why

The one concrete fieldPatches example the platform taught, in the skill, the spec description and two docs pages, patched an option of HotCRM's picklist-bound industry. A verbatim copy was refused at install and by os validate, and the skill paragraph contradicted its own rule.

Verification

Head 5a6e8fad: the two edit commits, a merge of origin/main at acd190b2 (8920b06e, no conflict, no rebase), the ratchet-payment commit, and one commit for the contract review's two should-fixes (TSDoc comment made conditional; a skills-changeset claim the skill text does not make, dropped), and one commit for the delta review's must-fix (the comment said only the label was patchable; a picklist binding blocks only option patches).

Local gates on 7c661de (head 5a6e8fa changes one TSDoc comment only): 105 derived, 105 run with exit 0, 0 NOT MEASURED (`dispatch-gates --ran`)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 105 commands for the 6 changed files. All ran after pnpm install --frozen-lockfile, a full pnpm build of the merged tree and a @objectstack/spec rebuild on this head (each exit 0), each under a 900 s timeout with NODE_OPTIONS=--max-old-space-size=8192 and its exit code captured before any pipe. The larger heap is for this container: at the script's default 4096 MB, check:test-typecheck inside check:generated had tsc exit with no diagnostics; at 10240 MB it was green.
  • All 105 exited 0, among them check-skills-token-ratchet.mjs, check:generated, check:docs, check:skill-examples, check:skill-identifier-liveness, check:doc-authoring, check:dts-closure, check:dual-build-cjs-loads and check-comment-mask-corpus.mjs. Also run: pnpm --filter @objectstack/spec exec tsc --noEmit (exit 0) and pnpm exec eslint packages/spec/src/data/object.zod.ts (exit 0).
  • --ran printed: 105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3).
  • The first head ad4d96f6 (before the merge) also ran all 105 with exit 0; there check-plugin-teardown-shape.mjs --self-test first exited 3 because the shallow clone lacked its pinned fixture commit, and exited 0 after fetching it.
  • node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0 (6 commits) before each push. Heads 22db7aee and 7c661dec ran all 105 with exit 0; 5a6e8fad changes only those two TSDoc comment lines, and on it pnpm --filter @objectstack/spec build and check:generated exit 0 with nothing stale.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Description text only.)
  • 2. Every name I added uses the casing for its kind. (No names added.)
  • 3. No formatter ran. eslint on the touched TypeScript file exits 0.
  • 4. One issue, one branch off an up-to-date main, one pull request. (main merged in, no rebase.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed (check:generated exit 0).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is Chinese, per the lane template.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp

…ld that declares them

HotCRM's crm_account.industry takes its options from the shared industry
picklist, so the taught example's options patch was refused at install and
by os validate (picklist-option). The skill, the spec description and TSDoc,
and the object-extensions docs page now relabel industry only and patch an
option of type, which declares its own options.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp
…atement

The .custom paragraph's closing clause restated the relabel paragraph's
lead (a base field changes only through fieldPatches), so it goes, and the
example's longer options patch fits under the unchanged token ceiling.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 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
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 170 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 8ae0ab9b0d77d56473e89dae9fe437778172dda8 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8ae0ab9b0d77d56473e89dae9fe437778172dda8

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 22db7ae · 2026-11-06T16:07Z

The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent. No review seat (lanes/review.md) is seated.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 22db7aee6047702085db7002930a061ef55a421d
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, skills/objectstack-data/SKILL.md + packages/metadata-core/src/extension-field-patch-targets.ts (fieldPatchTargetMisses), packages/objectql/src/registry.ts (patchedField, applyFieldPatches), skills/objectstack-data/evals/objects-fields-relationships.json (cases 5 and 7), scripts/check-skills-token-ratchet.mjs (the skills/objectstack-data/SKILL.md row, 6078), HotCRM main 6961ab1 src/sales/objects/account.object.ts and src/sales/objects/contact.object.ts

Inputs read: card #30944 (body, the triage comment and the claim comment), PR #30995 (body, 6-file list, net diff against merge base acd190b22), the head's 42 check-runs, HotCRM origin/main at 6961ab1.

① Derived judgments

  • ObjectExtensionSchema.fieldPatches, fieldPatchesDescription(): description text only. The added sentence ("An option of a field bound to a shared picklist is never patchable … only a field declaring its own options takes an option patch") and the new example change the published description, which is emitted into the JSON Schema and into content/docs/references/data/object.mdx. No z. shape, key, alias, refusal or default changes, so the accept set is unchanged. Right.
  • ObjectExtensionSchema TSDoc @example: this changes a comment that ships in the .d.ts. No type changes. Right.
  • skills/objectstack-data/SKILL.md: changes the taught example and deletes one clause. This is published through @objectstack/skills, and it has no API surface. Right.
  • What the change claims about runtime matches the code. fieldPatchTargetMisses pushes picklist-option for every options key of a field with a non-empty picklist, before it looks at values (L112–126). A field without a picklist and with no inline options gets an option miss that says its values are "none" (L128–139), so "only a field declaring its own options takes an option patch" holds. patchedField skips options when picklistBound (registry.ts L650–651). "Its labels are the picklist's" matches the judge's own message and the finishServedObject docblock. Right.
  • No code in metadata-core or objectql is touched. The regenerated reference row matches the new description text exactly, and check:docs runs in Type Check · source gates, which concluded success.

② Semver level

  • '@objectstack/skills': patch: the diff publishes changed teaching text only. Matches.
  • '@objectstack/spec': patch: the diff publishes changed description and TSDoc text only, with no schema or type change. Matches. Both packages are in the fixed group of .changeset/config.json. Neither changeset is a skip-changeset.
  • Clause-②: no and Metadata-protocol: none appear in both changesets and in the PR body, and both are correct. No authorable key or accepted value is added or removed, and the protocol is not touched.
  • The skills changeset's prose does not match the tree (should-fix S1 below). It says "the sentence before it names industry as the picklist-bound field whose options no patch reaches". The sentence on the head (SKILL.md L363) says only "a picklist-bound field's options are never patchable" and does not name industry. This prose lands in the CHANGELOG. The level is unaffected.

③ Boundary flags

  • Tier waiver ("developed below that tier; the dispatching seat runs the contract review"): answered. This record is served at CONTRACT_REVIEW_TIER, and the face is schema plus a governed path (--paths gives both hits).
  • Out of scope, options shape error message near object.zod.ts L5034 (options: { technology: { label: 'Tech' } }): I concur that it stays. It shows the map-versus-list shape inside a refusal message and names no target field, so it is not a HotCRM patch an author copies.
  • Ratchet payment "by the seat's ruling": judged in ④. It holds.
  • Local heap note (check:test-typecheck at 4096 MB) and check-plugin-teardown-shape --self-test exit 3 on a shallow clone: both come from the dev's container, not from the tree. CI is the verdict. Of the type-check jobs, Type Check · workspace, · consumer gates and · debt ledger are still in progress on this head.
  • Serial constraint with PR docs(skills): the objectstack-data skill leads a hook author to the save notice #30947 (same SKILL.md): resolved. docs(skills): the objectstack-data skill leads a hook author to the save notice #30947 merged at 2026-11-06T15:26Z, its merge commit is an ancestor of this head, and SKILL.md on origin/main equals the merge base.
  • Check-runs on the head (42 in total): 14 completed success (Type Check · source gates, which carries the token ratchet and check:docs; Spec property liveness; Lint · ESLint; Lint · PM dispatch-gates; Check Changeset; Governed Surface Queue Guard; PR-body guards; Check Documentation Links; Dogfood Regression Gate; Check PR Size; Flag docs affected by code changes; Plan Test Core shards; Auto Label; filter). 7 were skipped (opt-in or path-filtered). 21 are in progress: Build Core, Build Docs, Build UI Deps, Test Core (1–8/8), Live E2E UI, Type Check · workspace, Type Check · consumer gates (carries check:skill-examples), Type Check · debt ledger, Lint · docs & code guards (carries check:skill-identifier-liveness), Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · scoped · tooling self-tests, Lint · scoped · slot lookup & corpus, Lint · scoped · query-options erasure. None has failed. This PASS does not stand in for them: the merge waits on every check being green.

④ Design judgment

skills/objectstack-data/SKILL.md: holds.

  • I copied the new example verbatim, defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } }), and checked it against HotCRM main 6961ab1. industry is declared at L311–318 (Field.select({ label: 'Industry', group: 'basic', picklist: 'industry' })), so fieldOf finds it, and the patch has no options, so it raises no miss. type is declared at L300–309 with inline options and no picklist, and L305 is { label: 'Customer', value: 'customer', … }. customer is therefore a stored value, values.includes('customer') is true, and it raises no miss. fieldPatchTargetMisses returns []. patchedField applies label to industry and relabels type's customer option. The copy passes the install door, os validate and the fold.

  • The card's constraint holds. extend: 'crm_account' and label: 'Sector' are whole tokens on L364, as eval case 5 (must_contain extend: 'crm_account') and case 7 (extend: 'crm_account', fieldPatches, label: 'Sector') need. The example no longer contradicts the rule stated just before it ("a picklist-bound field's options are never patchable"), and it agrees with case 7's expected output ("no options entry belongs on this patch").

  • Ratchet payment: no fact is lost. The deleted clause was "; a base field changes only through fieldPatches", which closed the .custom paragraph. It carries three facts, and each survives in the same file:

    • (a) A base field can be changed, and the channel is fieldPatches. L355: "Relabel a field the owner declares with fieldPatches".
    • (b) The change is display-only. L356–360: the whitelist keys only, "the type, values and rules stay the owner's".
    • (c) No other channel exists (the "only"). fields is closed by L355–357 ("never by redeclaring it in fields"), and in a .custom extension it is a hard refusal through L370–371: a base field is unsuffixed, because __c is reserved to the pair, and "an unsuffixed field in a .custom extension, is refused". overrides is closed by L368 ("it takes no overrides declaration"). Removal is closed by L349 ("can add but never remove").

    Eval case 5's expected-output sentence ("A change to one of HotCRM's own fields goes through fieldPatches (display properties only)") is carried by (a) and (b), and it is not a must_contain token. The clause was a restatement, so the deletion is a real payment and no rule is lost.

  • Token count: the head blob is 24291 bytes. Minus the 20-byte stamp line, ceil(24271/4) = 6068, against the row's ceiling of 6078. The base was 24331 bytes, which counts to 6078. The example costs 10 bytes and the payment frees 50, a net of −40 bytes. No row moves and no re-wrap is involved. The ratchet runs in Type Check · source gates, which concluded success. The 10-token headroom could be returned by lowering the row. The script says this is encouraged but optional, so it is not a finding.

  • S1 (should-fix): make the tree and the skills changeset agree. The changeset says the sentence before the example names industry as the picklist-bound field, but it does not. As things stand, the example patches type's options and not industry's without saying why, so the reader learns the rule but not which HotCRM field it applies to. Case 7 expects the agent to know "HotCRM's industry takes its options from the industry picklist". There are two fixes. One is a few words on L363, e.g. "(HotCRM's industry is)", which fits in the 10-token headroom. The other is to drop the claim from the changeset.

packages/spec/src/data/object.zod.ts: holds.

  • The new description sentence is true of the code it cites (see ①). I copied the new description example verbatim, fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into' }, type: { options: { customer: { label: 'Client' } } } }, under extend: 'crm_account'. inlineHelpText is a whitelisted field key, and the rest goes through the same path as the skill example, so it passes fieldPatchTargetMisses on HotCRM main with []. The rule is stated next to the example, as the card's Expected asks.
  • S2 (should-fix): the TSDoc @example targets extend: 'contact', and its new comment states "industry is picklist-bound, so only its label; type declares its own options". Those are facts about HotCRM's crm_account, not about any contact. HotCRM's crm_contact declares neither industry nor type, so a copy against it gets two field misses. Patching industry on contact predates this PR, and the block is plainly illustrative (Field.select([...])), so this does not refuse a copy on the card's scenario. But the comment the PR adds now asserts the shape of a target it does not name. Either word the comment as a condition ("where industry is picklist-bound, as on HotCRM's crm_account") or split the fieldPatches line out under an account target.

No must-fix. Both should-fixes are text-only and do not block this head.

Implemented-by: claude/project-thread-kv932h
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

…im the changeset claim

The ObjectExtensionSchema example extends `contact`, so the comment on why
`industry` takes no option patch now names the case it holds for (a
picklist-bound `industry`, as on HotCRM's crm_account). The skills changeset
no longer says the skill text names `industry` as picklist-bound.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 7c661de · 2026-11-06T16:37Z

Delta review against the PASS at head 22db7ae (comment 6689532077). The delta is one commit answering S1 (skills changeset prose) and S2 (the TSDoc example comment), plus the PR body. The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7c661dec13587d9909755807fa4caf90b5b2440d
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, skills/objectstack-data/SKILL.md + packages/spec/src/data/field-customization.ts (BASE_FIELD_DISPLAY_WHITELIST), packages/metadata-core/src/extension-field-patch-targets.ts (fieldPatchTargetMisses), packages/objectql/src/registry.ts (patchedField, applyFieldPatches), skills/objectstack-data/evals/objects-fields-relationships.json (cases 5 and 7), scripts/check-skills-token-ratchet.mjs (the skills/objectstack-data/SKILL.md row, 6078), HotCRM main 6961ab1 src/sales/objects/account.object.ts and src/sales/objects/contact.object.ts

This is a delta review against the PASS recorded on 22db7aee. The delta is one commit, 7c661dec1, whose parent is 22db7aee. It changes two files: .changeset/30944-skills-field-patches-example-hotcrm.md (one line) and packages/spec/src/data/object.zod.ts (the TSDoc @example comment, which goes from one line to two). The merge base with origin/main is still acd190b22. The net diff against it is the same six files as before. SKILL.md, both docs pages, the spec changeset and fieldPatchesDescription() are byte-identical to 22db7aee.

① Derived judgments

  • ObjectExtensionSchema.fieldPatches, fieldPatchesDescription(): unchanged since 22db7aee. The text is description-only and the accept set is unchanged. The sentence on picklist-bound options is true of fieldPatchTargetMisses (L112–126) and patchedField (registry.ts L650–651). Right.
  • ObjectExtensionSchema TSDoc @example: a comment only, shipped in the .d.ts. No type changes. Right as a surface change. Its new wording is false, though: see M1 in ④.
  • skills/objectstack-data/SKILL.md: unchanged since 22db7aee. It is teaching text published through @objectstack/skills and has no API surface. Right.
  • The regenerated content/docs/references/data/object.mdx row still matches the description, because the delta does not touch the description. check:docs runs in Type Check · source gates, which concluded success on this head.

② Semver level

  • '@objectstack/skills': patch and '@objectstack/spec': patch: the diff publishes teaching text, description text and TSDoc only. Both match. Both packages are in the changeset config's fixed group, and neither changeset is a skip-changeset.
  • Clause-②: no and Metadata-protocol: none appear in both changesets and in the PR body, and both are correct.
  • S1 from 22db7aee is resolved and true. The skills changeset no longer says the sentence before the example names industry. What it still says now matches the tree: the example sets industry's label only and patches type's customer option, and the deleted .custom clause is a restatement (see ④). The skill text was left unchanged, which was one of the two ways to resolve S1.

③ Boundary flags

  • Tier waiver: answered. This record is served at CONTRACT_REVIEW_TIER, on the schema face plus a governed path.
  • Out of scope, options shape error message near object.zod.ts L5034: I still concur that it stays. It illustrates the map shape and names no target field.
  • Ratchet payment by the seat's ruling: judged in ④. It holds. The SKILL.md blob is still 24291 bytes, which counts to 6068 tokens against the 6078 ceiling.
  • Serial constraint with PR docs(skills): the objectstack-data skill leads a hook author to the save notice #30947: resolved. It merged, and its merge commit is an ancestor of this head.
  • Check-runs on 7c661dec (67 in total):
    • 55 completed success. These include every Type Check job (source gates, workspace, consumer gates with check:skill-examples, debt ledger, TypeScript Type Check, Type Check UI), every Lint job (docs & code guards with check:skill-identifier-liveness, repo & tooling gates, CI, release & engine gates, the three scoped jobs, ESLint, PM dispatch-gates, Lint & Repo Gates), Build Core, Build Docs, Build UI Deps, Build & E2E UI, Live E2E UI, Test Core 1–3 and 5–8, Test UI 2–16, Governed Surface Queue Guard, both PR-body guards, one Check Changeset, and Spec property liveness.
    • 9 were skipped (opt-in or path-filtered).
    • 3 are in progress: Test Core (4/8), Test UI (1/16), and a second Check Changeset run.
    • None has failed.

④ Design judgment

skills/objectstack-data/SKILL.md: holds. The delta does not touch this file, and every judgment from 22db7aee stands on the tree.

  • I checked a verbatim copy of defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } }) against HotCRM main 6961ab1. industry (L311–318) is picklist-bound and the patch carries no options. type (L300–309) declares its own options, and L305 stores customer. fieldPatchTargetMisses returns [], and patchedField applies both patches.
  • extend: 'crm_account' and label: 'Sector' are whole tokens on L364, as eval cases 5 and 7 need.
  • Ratchet payment: no fact is lost. Each fact of the deleted clause "; a base field changes only through fieldPatches" survives in the same file:
    • the channel is fieldPatches: L355;
    • the change is display-only: L356–360;
    • no other channel exists: fields is ruled out at L355–357, and in a .custom extension it is refused through the unsuffixed-field rule at L370–371; overrides is ruled out at L368; removal is ruled out at L349.

packages/spec/src/data/object.zod.ts: does not hold.

  • S2 from 22db7aee: the scoping is resolved, but the new sentence is not true. The comment no longer states crm_account facts as if they belonged to the example's contact target; it scopes them to "as on HotCRM's crm_account". That part is right.
  • M1 (must-fix). L5133–5134 now read: "Where industry is picklist-bound, as on HotCRM's crm_account, only its label is patchable; an option patch goes on a field declaring its own options." That is a false rule.
    • A picklist binding blocks only the options part of a patch. fieldPatchTargetMisses judges picklist only inside the options loop (L110–127). patchedField applies every BASE_FIELD_DISPLAY_WHITELIST.field key (label, description, inlineHelpText, placeholder; field-customization.ts L77) whatever the binding, at registry.ts L643–647. Only the options branch checks !picklistBound.
    • The same file contradicts the comment 16 lines above it. The fieldPatchesDescription() example (L5117) patches industry's inlineHelpText beside its label. The docs page object-extensions.mdx patches industry's inlineHelpText too.
    • An author who reads the published .d.ts learns that HotCRM's industry takes nothing but a label. This card exists because one taught fact about this patch was wrong, and the delta adds another.
    • The fix is a few words on L5133–5134, e.g. "Where industry is picklist-bound, as on HotCRM's crm_account, its options are not patchable (its display keys are); an option patch goes on a field declaring its own options". The code block, the description and the changesets can stay as they are, and the fix needs no changeset change.
  • The rest of the file holds as recorded on 22db7aee. The description's new sentence is true of the code. A verbatim copy of the description example (industry: label plus inlineHelpText; type: customer option) under extend: 'crm_account' passes fieldPatchTargetMisses on HotCRM main with [].

One must-fix (M1). S1 is resolved. S2 is resolved in scope, but its new wording is the must-fix.

Implemented-by: claude/project-thread-kv932h
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: FAIL

…c comment

The display keys of a picklist-bound field stay patchable; only its options
are out of reach, as the description's own example (inlineHelpText) shows.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 5a6e8fa · 2026-11-06T16:46Z

Delta review against the FAIL at head 7c661de (comment on this PR, must-fix M1: the TSDoc example comment). The delta is one commit rewriting object.zod.ts L5133–5134, plus the PR body. The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a6e8fad3087ae745d71794bc9bc28db0b3153bb
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, skills/objectstack-data/SKILL.md + packages/spec/src/data/field-customization.ts (BASE_FIELD_DISPLAY_WHITELIST), packages/metadata-core/src/extension-field-patch-targets.ts (fieldPatchTargetMisses), packages/objectql/src/registry.ts (patchedField, applyFieldPatches, finishServedObject), packages/objectql/src/picklist-resolution.ts (the served shape), skills/objectstack-data/evals/objects-fields-relationships.json (cases 5 and 7), scripts/check-skills-token-ratchet.mjs (the skills/objectstack-data/SKILL.md row, 6078), HotCRM main 6961ab1 src/sales/objects/account.object.ts and src/sales/objects/contact.object.ts

This is the second delta review. It is judged against the FAIL recorded on 7c661dec, which had one must-fix, M1. GitHub's compare of 7c661dec...5a6e8fad reports ahead 1, behind 0. The one commit is 5a6e8fad, whose parent is 7c661dec. It changes one file: packages/spec/src/data/object.zod.ts, two comment lines (L5133–5134) inside the ObjectExtensionSchema TSDoc @example. The local refs/seat3/pr-30995 still named 7c661dec, so I read the delta through GitHub's compare API and did not write to the local checkout. Every other path on the head is the tree I judged on 7c661dec. The merge base with origin/main is still acd190b22.

① Derived judgments

  • ObjectExtensionSchema TSDoc @example: a comment only, shipped in the .d.ts. No type, key or accepted value changes. Right. It does not reach the generated content/docs/references/data/object.mdx, whose row is built from the fieldPatches .describe() text. That text has been unchanged since 22db7aee.
  • fieldPatchesDescription(), SKILL.md, both docs pages and both changesets have not changed since 7c661dec. The derived judgments recorded there stand: the change is description and teaching text only, and the accept set is unchanged.

② Semver level

  • '@objectstack/skills': patch and '@objectstack/spec': patch are untouched by the delta and still match what the diff publishes: teaching text, description text and TSDoc only. A TSDoc comment is a patch-level change.
  • Clause-②: no and Metadata-protocol: none appear in both changesets and in the PR body, and both are correct.
  • S1 (the skills changeset's claim about L363) was closed on 7c661dec and has not regressed.

③ Boundary flags

  • PR body (v3) against the head. Its account of the TSDoc matches the tree: "since that example extends contact, its comment is conditional: where industry is picklist-bound, as on HotCRM's crm_account, its options are not patchable (its display keys are)". That is the wording at L5133–5134. Its Verification section says 5a6e8fad changes "only those two TSDoc comment lines", which is true by the compare (1 file, 1 commit). It also lists 6 commits for the trailer check. The body says the TSDoc example "carries the same patch" as the description. The description's patch also sets inlineHelpText, so this is loose wording in the body, not a tree defect, and it is not a finding.
  • The dev's report that nothing regenerated. This is consistent with the tree, because the reference row carries the .describe() text and not the TSDoc. check:docs runs in Type Check · source gates, which is still in progress on this head (see below).
  • Main is 9 commits ahead and the PR was not merged with it. Between acd190b22 and origin/main 8ae0ab9b0, the only paths in this review's reach that changed are scripts/check-skills-token-ratchet.mjs and skills/objectstack-data/rules/field-types.md. The ratchet change only lowers the rules/field-types.md row, from 3207 to 3199; the SKILL.md row stays at 6078. This PR does not touch rules/field-types.md. Nothing changed in extension-field-patch-targets.ts, registry.ts, field-customization.ts, object.zod.ts, SKILL.md, object.mdx or object-extensions.mdx. So the claims are judged on the same code that main has now.
  • Check-runs on 5a6e8fad (46 so far). The set is incomplete:
    • 14 completed success: Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, both PR-body guards, Plan Test Core shards, Spec property liveness, Auto Label, filter.
    • 9 were skipped (opt-in or path-filtered).
    • 23 are in progress: Build Core, Build Docs, Build UI Deps, Test Core 1–8, Live E2E UI, Type Check · source gates (which carries the token ratchet and check:docs), Type Check · workspace, Type Check · consumer gates (check:skill-examples), Type Check · debt ledger, Lint · docs & code guards (check:skill-identifier-liveness), Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, and a second Check Changeset run.
    • None has failed.
    • The UI jobs that ran on 7c661dec (Test UI, Type Check UI, Build & E2E UI) have not been reported on this head yet.
    • This PASS does not stand in for any of them: the merge waits on every check being green.
  • The tier waiver, the out-of-scope options shape message near L5034, and the serial constraint with docs(skills): the objectstack-data skill leads a hook author to the save notice #30947 were answered on 22db7aee and 7c661dec and are unchanged.

④ Design judgment

packages/spec/src/data/object.zod.ts: holds. M1 is closed and true on main's code.

  • L5133–5134 now read: "Where industry is picklist-bound, as on HotCRM's crm_account, its options are not patchable (its display keys are); an option patch goes on a field declaring its own options." I checked each part on origin/main 8ae0ab9b0:
    • A picklist binding blocks option patches. fieldPatchTargetMisses (extension-field-patch-targets.ts L110–127) reads field.picklist only inside the loop over the patch's options keys, and it pushes picklist-option for each one. patchedField (registry.ts L650–651) skips the options branch when picklistBound.
    • The display keys stay patchable. patchedField applies every BASE_FIELD_DISPLAY_WHITELIST.field key (label, description, inlineHelpText, placeholder; field-customization.ts L77) at registry.ts L643–647, with no picklist test. finishServedObject resolves a picklist onto the field as { ...field, options: resolved } (picklist-resolution.ts L153), so it replaces options only and keeps the patched label, description, help text and placeholder.
    • "An option patch goes on a field declaring its own options". Without a picklist, fieldPatchTargetMisses admits an option key only when it is a stored value of the field's own options (L128–139).
  • The comment now agrees with the description 16 lines above it (L5117), which patches industry's label and inlineHelpText, and with object-extensions.mdx. It stays conditional, so it no longer asserts crm_account facts about the example's contact target (S2, closed).
  • The example line is unchanged: fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } }. Under HotCRM's crm_account (industry L311–318, picklist-bound with no inline options; type L300–309 with inline options, L305 storing customer), it passes fieldPatchTargetMisses with [].

skills/objectstack-data/SKILL.md: holds. The delta does not touch it, and the judgment on 7c661dec stands:

  • A verbatim copy of the L364 example passes fieldPatchTargetMisses on HotCRM main with [].
  • extend: 'crm_account' and label: 'Sector' are whole tokens, as eval cases 5 and 7 need.
  • The ratchet payment loses no fact. The deleted clause "; a base field changes only through fieldPatches" survives in the same file:
    • the channel is fieldPatches: L355;
    • the change is display-only: L356–360;
    • no other channel exists: fields is ruled out at L355–357, and in a .custom extension it is refused through the unsuffixed-field rule at L370–371; overrides is ruled out at L368; removal is ruled out at L349.
  • The blob counts 6068 tokens against the 6078 ceiling, which main has not moved.

No must-fix and no should-fix. M1 is closed, S1 and S2 stay closed, and nothing else moved.

Implemented-by: claude/project-thread-kv932h
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

维护者速读

  • 改了什么: fieldPatches 的唯一示例改为 industry 只改 label,选项补丁移到自带 inline options 的 type(customer → Client)。同步改了 objectstack-data 技能 L364、spec 的 fieldPatchesDescription() 与 ObjectExtensionSchema TSDoc、object-extensions.mdx;references/data/object.mdx 由 spec 重新生成。6 个文件,+34/−7。
  • 为什么改: HotCRM 的 crm_account.industry 绑定共享 picklist,fieldPatchTargetMisses 对其 options 键返回 picklist-option,照抄旧示例会在安装和 os validate 时被拒(skills+spec: the taught fieldPatches example patches crm_account.industry's options, but HotCRM's industry is picklist-bound, so a copy is refused at install #30944)。
  • 风险与回滚: 只改说明文字、TSDoc 与示例,无 schema、键或行为变化;@objectstack/skills、@objectstack/spec 各一个 patch。SKILL.md 6068/6078 token,示例多出的 10 字节由删掉同文件 L372 一句重复说法付账(其内容在 L355–360 仍在)。可整 PR revert。
  • 先看: skills/objectstack-data/SKILL.md L364;packages/spec/src/data/object.zod.ts 的 fieldPatchesDescription() 与 L5133–5134。
  • 两分钟验证: grep -n "industry: { options" skills/objectstack-data/SKILL.md packages/spec/src/data/object.zod.ts content/docs/data-modeling/object-extensions.mdx 无结果。
  • 需要你做的: skills/** 是受治理 Tier H 面,需 os-zhuang 或 hotlong 在 GitHub 上 Approve 一次。达档复核:全量 6689532077 PASS(S1/S2),增量 6690089620 FAIL(M1),增量 6690287840 PASS,Head-sha 5a6e8fad3087ae745d71794bc9bc28db0b3153bb。批准后本席落地。
  • 席位意见: ACCEPT。

席位核对 — ACCEPT · domain:skills#3 · head 5a6e8fad3087ae745d71794bc9bc28db0b3153bb

  1. 形态: draft → main,Fixes #30944;两个 changeset('@objectstack/skills': patch、'@objectstack/spec': patch),Clause-②: no,Metadata-protocol: none;作者 objectstack-fleet[bot];6 个文件,在认领面之内。
  2. 前提实测: 复核读 main 代码确认:picklist 绑定只挡 options 补丁(fieldPatchTargetMisses、registry patchedField、picklist 解析只替换 options),label / description / inlineHelpText / placeholder 照常可改;新示例、描述与 TSDoc 三处说法一致。S1、S2、M1 均已闭。
  3. 门禁(钉 head 5a6e8fad): dev 派生门禁 105 条全部 exit 0;check-runs 69 条全部完成,59 success、10 skip、0 failure(Test UI 5/16 首跑一个图表渲染测试超时,与本 diff 无关,重跑一次即绿)。check-governed-merges:Tier H(skills/**)。
  4. 接受的偏差: PR 正文速读「需要你做的:无」不对,本 PR 碰 skills/**,属 Tier H,以本评论为准;正文说 TSDoc 示例与描述「同一补丁」略宽(描述还设了 inlineHelpText),不影响内容。
  5. 范围外去向: 无。Skills-docs: self。
  6. 落地条款: Tier H。保持 draft,挂 pm:awaiting-merge,请 os-zhuang、hotlong 审批;获批后本席过落地前检、清标、ready、auto-merge;维护者直合亦可。

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 6, 2026 17:38
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 6, 2026
@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 6, 2026
Merged via the queue into main with commit 33bc8ae Nov 6, 2026
116 of 118 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-kv932h branch November 6, 2026 18:08
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 protocol:data size/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants