Skip to content

Commit 8ce974c

Browse files
fix(spec): one homeAmount refusal names both faults of a home or rate field (#29533)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8VcRFhGUei2bW39W6pCoKQt)_ 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 7a8be35 → 9df9b598; the file went 2 failed / 30 passed; restore put the blob back to 7a8be35 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). <details><summary>Local runs</summary> - `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. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 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`). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed (none stale). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 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](https://claude.ai/code/session_01JQwxbY25z4BbXRWy1LD8NX)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 03797bd commit 8ce974c

3 files changed

Lines changed: 32 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): a `currencyConfig.homeAmount` home or rate field that lacks `readonly: true` and declares an `expression` is told both faults in one refusal
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An object whose `homeAmount.field` or `homeAmount.rateField` names a field that lacks `readonly: true` AND declares an `expression` was refused with only the `readonly` fault, so the author added `readonly: true` and met the `expression` fault on the next parse. The one issue at that pointer now names both faults and both remedies: add `readonly: true` to the field and delete its `expression`. The accept set is unchanged; a field with only one of the two faults is told exactly what it was told before.

‎packages/spec/src/data/currency-config-home-amount.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ describe('the object judges what `homeAmount` names', () => {
233233
expect(issues.map((i) => i.path)).toEqual([`${AT}.${key}`]);
234234
expect(issues[0]!.message).toContain('declares an `expression`');
235235
});
236+
237+
it.each(['amount_home', 'amount_rate'] as const)('names both faults in one issue when `%s` has neither mark', (name) => {
238+
const issues = objectIssues(invoice({ [name]: { ...BASE_FIELDS[name], readonly: false, expression: 'record.amount' } }));
239+
const key = name === 'amount_home' ? 'field' : 'rateField';
240+
expect(issues.map((i) => i.path)).toEqual([`${AT}.${key}`]);
241+
expect(issues[0]!.code).toBe('custom');
242+
expect(issues[0]!.message).toContain('does not declare `readonly: true`');
243+
expect(issues[0]!.message).toContain('declares an `expression`');
244+
expect(issues[0]!.message).toContain(`Add \`readonly: true\` to \`${name}\` and delete its \`expression\``);
245+
});
236246
});
237247

238248
it('refuses `homeAmount` on a `formula` amount, at `homeAmount`, judging no pointer', () => {

‎packages/spec/src/data/object.zod.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,16 +2034,22 @@ function refuseDishonestHomeAmount(
20342034
// non-system write of it, and no `expression` competes with the engine.
20352035
const refuseUnlessPlatformWritten = (key: 'field' | 'rateField', target: string, what: string): void => {
20362036
const t = map[target];
2037-
if (t?.readonly === true && t.expression === undefined) return;
2037+
const writable = t?.readonly !== true;
2038+
const computed = t?.expression !== undefined;
2039+
if (!writable && !computed) return;
2040+
// Both faults on one field are named in one issue, so the author fixes
2041+
// the field once instead of meeting the second fault on the next parse.
2042+
const writableFault = 'does not declare `readonly: true`, so a client write could overwrite what the engine stored';
2043+
const computedFault = 'declares an `expression`, so the engine would store a second, computed value in the same column';
20382044
ctx.addIssue({
20392045
code: 'custom',
20402046
path: [...base, key],
20412047
message: `${at}.${key}\` names \`${target}\`, ${what} only the engine writes, but it `
2042-
+ (t?.readonly === true
2043-
? 'declares an `expression`, so the engine would store a second, computed value in the same column. '
2044-
+ `Delete \`expression\` from \`${target}\`.`
2045-
: 'does not declare `readonly: true`, so a client write could overwrite what the engine stored. '
2046-
+ `Add \`readonly: true\` to \`${target}\`.`),
2048+
+ (writable && computed
2049+
? `${writableFault}, and it ${computedFault}. Add \`readonly: true\` to \`${target}\` and delete its \`expression\`.`
2050+
: writable
2051+
? `${writableFault}. Add \`readonly: true\` to \`${target}\`.`
2052+
: `${computedFault}. Delete \`expression\` from \`${target}\`.`),
20472053
});
20482054
};
20492055

0 commit comments

Comments
 (0)