Skip to content

fix(spec): one homeAmount refusal names both faults of a home or rate field - #29533

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

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ik6sn2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #28205

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:homeAmount 的 home / rate 字段同时缺 readonly: true 又带 expression 时,一条拒收文案同时点名两个毛病和两个处方。
  • 为什么改:以前只报 readonly,作者改完再 parse 才撞上 expression,要来回两趟。
  • 风险与回滚:只改文案,接受集不变,单一毛病的文案一字不变;回滚即 revert 本 PR。
  • 先看:packages/spec/src/data/object.zod.ts 的 refuseUnlessPlatformWritten;currency-config-home-amount.test.ts 新增用例。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-home-amount.test.ts,预期 32/32 通过。
  • 需要你做的:无,席位落地。

What changes

refuseUnlessPlatformWritten (the currencyConfig.homeAmount judge in packages/spec/src/data/object.zod.ts) used to pick ONE fault: a target without readonly: true was told only that, even when it also declared an expression. It now computes both faults; when both hold, the one issue at fields.AMOUNT.currencyConfig.homeAmount.field (or .rateField) names both and both remedies: "Add readonly: true to TARGET and delete its expression." A target with one fault gets exactly the message it got before.

A patch changeset for @objectstack/spec carries the two declaration lines.

Why

Triage 6528487750: one refusal names both faults (p3). The author otherwise fixes the readonly fault and meets the expression fault on the next parse.

Tests

  • New it.each over amount_home / amount_rate beside the existing homeAmount platform-written refusals: one issue at the pointer, code: custom, both fault fragments and the joint remedy.
  • Ablation (committed fix first): scripts/ablation-replace.mjs replaced the both-faults condition with false, so the old single-fault branch answered; the anchor hit 1 → 0, blob 7a8be354 → 9df9b598; the file went 2 failed / 30 passed; restore put the blob back to 7a8be354 and git diff HEAD came back empty.

Acceptance notes

  • None. The message text is the only change; no export, schema key or accepted value moves.

Verification

All on head 29af940 (this branch merged with origin/main c88467d).

Local runs
  • pnpm exec turbo run build for the spec, formula, lint and objectql closures, then pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date", then pnpm --filter @objectstack/spec test: Test Files 792 passed (792), Tests 23524 passed, 1 todo. Lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec typecheck: exit 0 (round 1, on a3fcb30; the round-2 merge did not touch the changed file).
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 80 families derived. 79 run, every one exit 0. pnpm check:dual-build-cjs-loads is NOT MEASURED: it needs every package built, so it is left to CI. --ran reconciliation: 80 derived, 79 run, 1 UNRUN (that gate).
  • Full dogfood: n/a, no ! in the title and no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint was not run locally (a repo-wide scan, owned by CI's Lint & Repo Gates).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 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 (none stale).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Dev session: session_01JQwxbY25z4BbXRWy1LD8NX (card thread), dispatched by domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir.


Generated by Claude Code

A home or rate field that lacks `readonly: true` and declares an
`expression` was told only the readonly fault; the issue now names both
faults and both remedies. Single-fault messages are unchanged.

Claude-Session: https://claude.ai/code/session_01JQwxbY25z4BbXRWy1LD8NX
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 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 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 168 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 5651647d82dea5ce18ad537d629c18e3dd27f613 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5651647d82dea5ce18ad537d629c18e3dd27f613

⚠️ 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:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 29af940fde182fd480eb8c328f34b9e597e452a3
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts + packages/spec/src/data/currency-config-home-amount.test.ts, packages/spec/src/data/field.zod.ts (readonly / expression field keys, read), content/docs/data-modeling/field-types.mdx, content/docs/data-modeling/validation-rules.mdx, content/docs/references/data/field.mdx (the homeAmount prose, read for drift)

Inputs: card #28205 body and its five comments (triage p3, unblocked, pm:queue, the seat claim, the os-dev-report); PR #29533 body, file list (3 files, +32/-6) and git diff origin/main...29af940f; the check-runs on 29af940f. Judged against the live tree, not the PR body.

① Derived judgments

  • Accept set — unchanged, right. On main the early return is t?.readonly === true && t.expression === undefined. On the head it is !writable && !computed with writable = t?.readonly !== true and computed = t?.expression !== undefined; !writable is t?.readonly === true and !computed is t?.expression === undefined, so the return condition is the same predicate on the same inputs. FieldSchema.readonly is z.boolean().default(false) (field.zod.ts:2371), so a parsed field never has readonly: undefined; even if it did, both forms treat it as writable. Every object accepted on main is accepted on the head and vice versa. Clause-②: no holds.
  • Single-fault messages — byte-identical, right. Old readonly === true branch: 'declares an expression, so the engine would store a second, computed value in the same column. ' + 'Delete expressionfromT.'; new !writable branch: ${computedFault}. Delete expressionfromT. with computedFault the same text minus the trailing . — the concatenation is the same string. Old else branch and the new writable-only branch likewise. The existing two it.each refusals (without readonly: true, declaring an expression) still hold without edit.
  • Both-faults message — new text, right. The only observable change: a target with readonly !== true AND an expression now receives one issue, same code: 'custom', same path fields.AMOUNT.currencyConfig.homeAmount.field|rateField, whose message names both faults and both remedies ("Add readonly: true to T and delete its expression."). Issue count at that pointer stays one; no second pointer gains or loses an issue. The call sites (object.zod.ts:2094 'the home field', :2148 'the rate field') are unchanged, so the refusal still fires only after existence, type, mode and ownership checks pass for that target — exactly where it fired before.
  • Public surface — none moved. No export, schema key, accepted value, generated artifact or docs anchor changes; git grep of both message fragments on main outside object.zod.ts returns nothing, so no snapshot or docs page pins the old text.

② Semver level

.changeset/28205-home-amount-both-faults.md: '@objectstack/spec': patch, Clause-②: no, Metadata-protocol: none. A refusal-message text change with an unchanged accept set is a patch; Clause-②: no is correct per ① (nothing accepted on main is refused on the head, and nothing refused is accepted). The PR body carries the same two declaration lines. Not a (narrowing) arm; no ! in the title. Matches what the diff publishes.

③ Boundary flags

  • Dev report open_questions: [], security_findings: none, out_of_scope_findings: [] — nothing to answer.
  • Dev flag "Human Minimum 3 unchecked: pnpm lint not run locally" — left to CI; Lint · ESLint is in_progress at review time (see below). Not a code finding.
  • Dev flag "two merges of origin/main" — the head 29af940f is a merge commit; the three-dot diff against origin/main is exactly the three intended files, so the merges carried nothing of their own. main has moved since (unrelated changesets only); neither scoped code file changed on main after the merge-base, so no conflict and no stale premise.
  • Check-runs on 29af940f at review time: concluded — Governed Surface Queue Guard, PR-body guards, Check PR Size, Spec property liveness, Dogfood Regression Gate, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Flag docs affected by code changes, Auto Label, filter all success; the opt-in / schedule / UI-artifact jobs skipped. Still in_progress — Build Core, Build UI Deps, Plan Test Core shards, Live E2E UI, Check Changeset, Type Check · workspace / source gates / consumer gates / debt ledger, Lint · ESLint / CI, release & engine gates / repo & tooling gates / docs & code guards / scoped · slot lookup & corpus / scoped · query-options erasure. No concluded run failed. The verdict below is on the concluded runs plus the read of the tree; the spec test and typecheck legs have not yet concluded in CI (the dev reports 792 files / 23524 tests green and typecheck exit 0 locally on this head, which this record does not adopt as a gate verdict). If any in-progress run concludes red, the merge gate — not this record — holds it.

④ Design judgment

packages/spec/src/data/object.zod.ts — judged against the card's ruling (triage: "one refusal names both faults", p3), ADR-0144 D5 as the test file cites it, and the first consumer (os validate / any object save through ObjectSchema.safeParse).

  • Holds. The ruling wants both faults and both remedies in one issue at the existing pointer; the head does exactly that and nothing else. I tried to break it:
    • Could writable && computed fire where neither single fault fired before? No — it is reachable only past the same early return, so every refusal on the head is a refusal on main.
    • Could the both-faults branch shadow a single fault? No — writable alone and !writable (hence computed) alone fall through to the two old strings; the ternary is exhaustive over the three reachable states.
    • Does the longer sentence still lead with the pointer and end with the prescription, as the sibling refusals do? Yes: "amount.currencyConfig.homeAmount.field names amount_home, the home field only the engine writes, but it does not declare readonly: true, so … , and it declares an expression, so … . Add readonly: true to amount_home and delete its expression." Consistent with the refusal style of the rest of refuseDishonestHomeAmount.
    • Rate-field path: the rate holder is judged once (the first holder, holder === undefined branch) as before; a second amount sharing a consistent rate field is still not re-judged. Unchanged.
  • Test (currency-config-home-amount.test.ts): the new it.each over amount_home / amount_rate overrides BASE_FIELDS[name] with readonly: false, expression: 'record.amount', asserts one issue at the pointer, code: custom, both fault fragments and the joint remedy — the one pinned case the card asked for, placed beside the two single-fault cases it extends. expression on a non-formula currency / number field is already accepted by the field schema (the existing declaring an expression case relies on it), so the case exercises the refinement and not a field-level refusal. Assertions use toContain like their neighbours rather than the exact string; acceptable, and the joint-remedy fragment is specific enough that the single-fault branches cannot satisfy it (the dev's ablation — both-faults condition to false — reports exactly these 2 cases failing, which is the expected shape).
  • Docs (read, not reviewed): the three homeAmount prose rows describe the rule (readonly: true, engine-written) and do not quote the refusal text, so no drift.
  • Findings: no must-fix. No should-fix.

Implemented-by: claude/project-thread-ik6sn2
Reviewed-by: session_01JQwxbahe4v7GBD94QeSnir

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

维护者速读

  • 改了什么:homeAmount 的本币字段或汇率字段同时缺 readonly: true 又带 expression 时,拒收消息现在一次点出两个问题和合并处方;只犯一个错时的消息逐字不变;加一个测试用例。
  • 为什么改:以前只报 readonly 一项,作者改完再保存才遇到第二个错。
  • 风险与回滚:接受与拒收的范围完全不变,只改消息文字(Clause-②: no),@objectstack/spec patch;回滚即 revert。
  • 先看:packages/spec/src/data/object.zod.ts 约 2035 行 refuseUnlessPlatformWritten。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-home-amount.test.ts,预期 32 例全过。
  • 需要你做的:无,CI 全绿,席位落地。
  • 席位意见:提前返回的判定式改写前后等价(契约复核 6584399690 逐项核过),两个单错消息拼接后与 main 逐字相同。

席位核对

ACCEPT · domain:spec#3 · head 29af940fde182fd480eb8c328f34b9e597e452a3 · 2026-11-02T18:22Z

  1. Shape: three files: packages/spec/src/data/object.zod.ts (+12 / -6, refuseUnlessPlatformWritten only), packages/spec/src/data/currency-config-home-amount.test.ts (+10, one it.each row beside the two single-fault rows), .changeset/28205-home-amount-both-faults.md (@objectstack/spec patch, both declaration lines). Inside the claim's file surface (6582609101).
  2. Premise, measured: on 94d3e254 the seat read the old message choosing one fault by t?.readonly === true, so a field with neither readonly: true nor a missing expression heard only about readonly. The dev's ablation turned the new case red twice before restoring.
  3. Gates: local 80 derived / 79 exit 0 / 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3 on a prerequisite, left to CI); check:generated up to date. On this head 64 check-runs, every one success or skipped; mergeable_state clean.
  4. Accepted deviations: main merged twice into the branch (the seat's serial ask, then gate scripts that moved mid-run); every gate re-ran on the final head. Human Minimum item 3 unchecked: lint is CI's, green here.
  5. Out of scope: none found by the dev, the reviewer or the seat. Skills-docs: n/a (Metadata-protocol: none).
  6. Landing: not governed (check-governed-merges --pr 29533: NOT governed, 38 changed lines), Clause-②: no, contract review at CONTRACT_REVIEW_TIER PASS on this head (6584399690). Serial: PR feat(spec)!: refuse an object that declares titleFormat at save and os validate, with the nameField prescription #29499 touches object.zod.ts in a disjoint hunk; the later lander merges. Seat lands it: pr_ready + automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 18:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 8ce974c Nov 2, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ik6sn2 branch November 2, 2026 19:01
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-warren · session_01JQwxbahe4v7GBD94QeSnir

Landed · 2026-11-02T19:07Z

Reading Value
PR #29533 MERGED 2026-11-02 19:01:54Z via merge queue, merge commit 8ce974c6 on main
Card #28205 closed completed by Fixes #28205; no other closing keyword in the body

State change: pm:dispatched removed from #28205. Delivered: one homeAmount refusal names both faults of a home or rate field. Remaining: none on this card.


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 protocol:data size/s tests tooling

Projects

None yet

1 participant