Skip to content

Commit 306ca50

Browse files
os-zhuangclaude
andauthored
fix(spec): 别名表必须是对 schema 的真断言 —— 修 12 条存量 + 全仓闸门 (#5013) (#5486)
* fix(spec): alias tables must be true claims about their schema (#5013) `ReportSchema` answered `filter` with "Did you mean `filter` -> `filters`?" and then rejected `filters` too, with no suggestion the second time — the strictness campaign's own fix pointing authors into the failure mode it exists to remove. Five more entries were filed under keys their schema already declares, so they could never run at all. Repointed the report's scope-filter aliases at `runtimeFilter` (matching `JoinedReportBlockSchema` verbatim), deleted the dead entries, and fixed the six further defects a repo-wide sweep found in `ai/skill.zod.ts` and `system/email-template.zod.ts`. `strictObject` now records each declaration so `alias-integrity.test.ts` can judge every table in the package against the runtime `.shape` it makes claims about. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * test(spec): pin the fix by parse, retire the superseded batch-14 section (#5013) The 批 14 reverse pin fired exactly as its comment promised — "this list cannot outlive its debt" — once all six defects it tracked were fixed. Its prescription-integrity section is deleted rather than emptied: what would remain is a second, weaker copy of a now-live package-wide check, built on the source-literal reading that cannot see spreads, assembled tables, or colliding surface strings. The parse-level assertion it carried, which no structural gate can make, stays. `report.test.ts` gains the author-visible half: all four scope-filter spellings name `runtimeFilter`, the prescribed key parses, and `columns` / `chart` still authoring cleanly proves those alias entries really were dead. The gate also pins, shrink-only, the 44 tables that reach `strictUnknownKeyError` directly and so sit outside it — measured clean, but a boundary that must not grow unnoticed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * docs(spec): name the two filed findings in the gate's coverage boundary (#5013) Refs #5481, #5483. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0285f7f commit 306ca50

10 files changed

Lines changed: 663 additions & 200 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): unknown-key suggestions no longer point authors at keys the schema rejects (#5013)
6+
7+
An `aliases` table on an authoring schema is a **claim about that schema**, in
8+
two halves: that the key it is filed under is one the shape *rejects* (an alias
9+
is consulted only from the `unrecognized_keys` path, so a key the shape declares
10+
can never reach it), and that the key it prescribes is one the shape *accepts*.
11+
Nothing checked either half, and both were false on `main`.
12+
13+
Writing `filter` on a report produced:
14+
15+
```
16+
Unrecognized key(s) on this report: `filter`. … Did you mean `filter` -> `filters`?
17+
```
18+
19+
and taking that advice produced a **second** rejection — `ReportSchema` declares
20+
neither `filter` nor `filters`, only `runtimeFilter` — this time with no
21+
suggestion at all. That is the exact failure the unknown-key strictness work
22+
exists to remove, shipped by its own fix, and it is worst for AI authors whose
23+
only signal is whether the parse complained.
24+
25+
**What changed** — no authorable key was added or removed, so nothing that
26+
parsed before stops parsing; only the guidance an author gets when a key is
27+
rejected:
28+
29+
- `ReportSchema` — `filter`, `filters`, `where` and `criteria` now all name
30+
`runtimeFilter`, matching `JoinedReportBlockSchema`'s table verbatim so a
31+
report and its sub-reports correct the author identically.
32+
- `EmailTemplateDefinitionSchema` — `html`/`content` name `bodyHtml`, `text`
33+
names `bodyText`, and `from`/`sender` name `fromOverride`. All five previously
34+
named `body` / `fromAddress`, neither of which the schema declares.
35+
- `SkillSchema` — `trigger` no longer renames onto `triggers` (never a key).
36+
It now carries a prescription instead, because the correct answer is a split:
37+
routing intent belongs in `triggerConditions`, natural-language intent in
38+
`description` / `instructions`. A rename would have landed a phrase in an
39+
array-of-conditions slot and been rejected on the value instead of the key.
40+
- Six entries filed under keys their own schema already declares — and which
41+
therefore could never run — are gone: `columns` and `chart` on `ReportSchema`,
42+
`measures` and `filter` on `DatasetSchema`, `body` on `ActionSchema`.
43+
44+
**What keeps it true** — `strictObject` now records each shape it builds, and
45+
`alias-integrity.test.ts` judges every one of the 235 authoring surfaces in the
46+
package against the runtime `.shape` it makes claims about. Reading the runtime
47+
shape rather than the source is what makes it work: shapes spread
48+
(`...MetadataProtectionFields`), ten alias tables are assembled rather than
49+
written as literals, and two different schemas share the surface string
50+
`'this field group'` — a source-literal reader is wrong or blind on all three.
51+
An alias pointing at a **tombstone** is caught too, which the suggester's own
52+
`knownKeys` filter cannot do, since the alias table is consulted before that
53+
fallback runs.

‎packages/spec/src/ai/skill.zod.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,27 @@ export const SkillSchema = lazySchema(() => strictObject({
6767
surface: 'this skill',
6868
history:
6969
'Until #4001 closed this shape these were dropped silently — the item still registered, minus whatever the key was meant to configure.',
70-
aliases: { prompt: 'instructions', content: 'instructions', body: 'instructions', trigger: 'triggers', tool: 'tools' },
70+
aliases: { prompt: 'instructions', content: 'instructions', body: 'instructions', tool: 'tools' },
7171
guidance: {
72+
// #5013 — `trigger` used to be an ALIAS pointing at `triggers`, a key this
73+
// schema has never declared: the author was told to write it, wrote it, and
74+
// was rejected a second time with no suggestion left to give.
75+
//
76+
// It is not repointed at `triggerConditions`, because a rename is the wrong
77+
// instrument here. The prescription the `triggerPhrases` tombstone below
78+
// carries is a SPLIT — routing intent goes to `triggerConditions`, natural
79+
// language to `description` / `instructions` — so an author who wrote
80+
// `trigger: 'create a case'` and took a rename would land a phrase in an
81+
// array-of-conditions slot and be rejected on the value instead of the key.
82+
// That is ledger finding 7 exactly: this campaign's own fix signposting the
83+
// way back into the failure mode it exists to kill.
84+
trigger:
85+
'`trigger` is not a skill key, and skills have never been activated by a phrase. '
86+
+ 'Activation is `triggerConditions` (an AND of context field/operator/value) intersected '
87+
+ "with the agent's `skills[]` allowlist, plus explicit /skill-name pinning. If you meant a "
88+
+ 'programmatic condition, write `triggerConditions: [{ field: …, operator: …, value: … }]`; '
89+
+ 'if you meant natural-language intent for the LLM to route on, that belongs in '
90+
+ '`description` / `instructions`, which are the strings actually put in front of the model.',
7291
permissions:
7392
'`permissions` is not a skill key — skill invocation was never permission-gated, '
7493
+ 'so this was stripped in silence and the author believed they had a gate. Gate at '

0 commit comments

Comments
 (0)