Skip to content

Commit e0f300b

Browse files
qq9340100qq9340100claude
authored
feat(spec): 关闭 chart 最后两个 STRIP 站点,并把 44 个直调 alias 表迁到 strictObject (#5583, #5593) (#6595)
* feat(spec): close chart's last two STRIP sites and migrate the 44 direct alias tables to strictObject (#5583, #5593) Two issues from the #4001 unknown-key campaign, landed together because they share one four-step regeneration and one CI lap. ── #5583 · ChartAggregateSchema / ChartGroupBySchema ──────────────────────── Both object shapes are `strictObject` now, so an undeclared key inside `aggregate={{…}}` is a named rejection carrying the surface, the offending key and a rename, instead of being silently stripped by the parse. `groupby`, `fn` and `dateGranularty` each degraded a chart with `build`/`validate` green. The order is the record worth keeping: `.strict()` is a property of a PARSE, and until #5020 wired one nothing parsed these schemas. Closing them first would have shipped a precisely-validated door with nothing behind it (#4583). The zod-4 union collapse is load-bearing here and pinned on both sides: `groupBy` is a union, so the arm's `unrecognized_keys` never reaches `error.issues` — the named message reaches an author only through `packages/lint`'s `describeIssue` arm unpacking. The product question this pair carried is ANSWERED and it did not move the schema: `groupBy` stays REQUIRED. Measured — the example corpus authors one `ObjectChart aggregate` and it carries `groupBy`; the ungrouped single-value need is served by objectui's separate `object-metric` block (7 instances in the showcase); and the renderer's three `schema.aggregate?.groupBy || schema.xAxisKey` reads are optional-chained on `aggregate` itself, so they serve charts with no aggregate at all. #5020's warning-level tolerance stays a tolerance; its hint now states the ruling rather than an open question. Records synchronised in three places: the file header, the two `chart.test.ts` pins (INVERTED in place, not deleted), and the ledger — whose `chart.zod.ts` remaining-strip row is deleted on the reverse pin (0 strip) with a departure note. Counts regenerated: strip 185 → 183, authorable 43 → 41. ── #5593 · 44 direct `strictUnknownKeyError` call sites → strictObject ────── All 44 migrated; 16 hand-transcribed `*_KEYS` arrays and 9 drift-probe tests deleted; the shrink-only ratchet is a hard ZERO and now forbids the weaker instrument rather than measuring it; #5483's transitional registry (`shared/alias-table-registry.ts`) is deleted with its last call site, along with the suppression hook `strictObject` needed to stay out of it. The migration found what the transcriptions were hiding — the half route 1 could not close: * 11 suggestion targets were `retiredKey()` tombstones (`app` ×8, `flow` ×2, `flow node` ×1), so a near-miss was steered onto the one key guaranteed to be rejected next — ledger finding 12, live on main; * the nav `separator` answered with keys it rejects: it spreads nothing and declares `type`/`id`/`order`, while the shared transcription handed every variant the base keys, so `title` was answered "did you mean `label`?" and `label` was rejected too — finding 7; * three ADR-0010 protection envelopes were missing from their own suggestion pools (`datasource`, `hook`, `sharing rule`). Zero-behaviour-regression is proved exhaustively rather than sampled: every migrated surface's old array is compared against its new derived pool (36 of 42 byte-identical; the 6 differences are exactly the defects above), plus a real rejection message per surface and per nav variant. `data/object.zod.ts`'s error map was deferred to dodge a temporal dead zone; `strictObject` evaluates options at construction, so the deferral is replaced by declaration order and that order is now documented as load-bearing. Exemption disposition, with one correction to the migration's own forecast: `VARIANT_LEGAL_GUIDANCE` is DELETED by a fix (the `children` prescription is filed only on the seven variants that can fire it), while `PROSE_ALIAS_TARGETS` MOVED into the shape-backed half rather than dying — migrating the tables did not turn seven deliberate prose targets into key names, so claim 2 met the same 93 entries from the shape side. It is stronger where it now sits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * docs(spec): sync chart.zod.ts header opening + regenerate the ledger counts on the merged tree The header's first paragraph still said two sites were deliberately left open, which #5583 closed. It now records the ORDER instead (批 15 refused to close first, #5020 wired the parse, #5583 moved the posture), because a later sweep meeting a `no gate` verdict needs the refusal's reasoning, not just the result. The counts artifact carries merge=os-regen and was merged without a text merge, so it is recomputed from the merged tree rather than reconciled by hand — main's etl.zod.ts retirement (#6414) landed in the same window, so the totals move for two independent reasons and only a wholesale regeneration gets both right: 444 -> 434 sites, 183 -> 180 strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY * fix(spec): registry array survives the field.zod ↔ suggestions.zod ↔ strict-object import cycle under eager schemas DECLARATIONS moved from a module-level const (TDZ at import time when the cycle enters through the automation barrel — an ordering #5593's migration itself introduced) to a hoisted function-owned store, the same property flowNodeObject() relies on (#4415). Pinned from both sides; the eager reproduction path is gen:schema (OS_EAGER_SCHEMAS=1). Recovery commit: the dev agent was killed by a container restart during its final verification run; verification re-run to completion by the PM — gen:schema eager pass, targeted cycle tests 36/36, full spec suite 342 files / 8768 tests green. --------- Co-authored-by: qq9340100 <sales@objectstack.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 51d74ad commit e0f300b

43 files changed

Lines changed: 2108 additions & 2237 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": patch
4+
---
5+
6+
feat(spec): `ChartAggregateSchema` / `ChartGroupBySchema` reject unknown keys instead of dropping them (#5583, #4001 批 15's last two sites)
7+
8+
`<ObjectChart aggregate={{ … }}>` is the react tier's object-bound chart binding,
9+
and until now a key it did not declare was **silently stripped by the parse**.
10+
`groupby` for `groupBy` degraded the chart to a single ungrouped point, `fn` for
11+
`function` fell back to the default, `dateGranularty` for `dateGranularity`
12+
turned off date bucketing — each with `os build` / `os validate` fully green.
13+
That is #4001's founding failure mode, on the surface an AI page author is most
14+
likely to write.
15+
16+
Both object shapes are `strictObject` now, so an undeclared key is a named
17+
rejection carrying the surface, the offending key and a rename:
18+
19+
```
20+
Unrecognized key(s) on this chart aggregate: `groupby`.
21+
Did you mean `groupby` → `groupBy`? Until #5583 an undeclared aggregate key was
22+
dropped at parse — …
23+
```
24+
25+
Curated beyond edit distance where the near-miss is semantic rather than a typo:
26+
`fn` / `agg` / `aggregation``function`, `measure``field`, and the ADR-0021
27+
dataset vocabulary an author carries over from the other binding mode
28+
(`dimension` / `category``groupBy`). Wrong-LAYER keys get a prescription
29+
instead of a rename — `dateGranularity` written *beside* `groupBy` did nothing at
30+
all and now says where it belongs; `alias`, `filter`, `objectName` and a
31+
`measures` array are pointed at the surface that owns them.
32+
33+
**Why this took two issues.** `.strict()` is a property of a PARSE, and until
34+
#5020 nothing parsed these schemas: the react-page publish gate re-derived the
35+
vocabulary by hand. Closing them first would have shipped a precisely-validated
36+
door with nothing behind it (#4583). #5020 wired the parse; this is the posture.
37+
38+
**The zod-4 union collapse is load-bearing here.** `groupBy` is a union, so the
39+
`unrecognized_keys` its strict arm raises never reaches `error.issues` — zod
40+
reports one `invalid_union` whose own message is the bare string `"Invalid
41+
input"`. What carries the named rejection to the author is `packages/lint`'s
42+
`describeIssue` arm unpacking, pinned end to end on both sides.
43+
44+
**`groupBy` stays REQUIRED — the product question this pair raised is answered,
45+
and the answer does not move the schema.** An ungrouped single-value chart is
46+
not a supported `<ObjectChart>` shape: the single-value need is served by the
47+
separate `object-metric` block, the example corpus authors zero ungrouped
48+
`<ObjectChart>` aggregates, and objectui's `schema.aggregate?.groupBy ||
49+
schema.xAxisKey` reads are optional-chained on `aggregate` itself — they serve
50+
charts with **no aggregate at all**, not ungrouped ones. #5020's `warning`-level
51+
tolerance for an absent `groupBy` therefore stays a tolerance rather than
52+
becoming a blessing; its hint now states the ruling.
53+
54+
**Upgrading:** if a chart aggregate carried a key this schema does not declare,
55+
it was already being ignored — the rejection names it and prescribes the fix. No
56+
legal declaration changes meaning.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
refactor(spec): the last 44 hand-transcribed key lists are gone — every alias table is judged against its schema's real shape (#5593)
6+
7+
Forty-four authoring schemas predated `strictObject` and wired their unknown-key
8+
error by hand: a `const X_KEYS = [...] as const` transcription of the shape, a
9+
`strictUnknownKeyError({ knownKeys: X_KEYS, … })` call, and a drift-probe test
10+
whose only job was to catch the two copies disagreeing. All 44 now call
11+
`strictObject(options, shape)`, which reads the candidate list from the shape
12+
itself, and the 16 transcriptions plus their probe tests are deleted.
13+
14+
The point is not the line count — it is what the alias-integrity gate (#5013)
15+
can now assert about them. #5483 had put these tables under the gate through a
16+
transitional registry, but two of its three claims were answered against the
17+
*transcription*: an array that had drifted from its schema dragged both answers
18+
with it, and "this alias target is a tombstone" was invisible because a flat
19+
string array holds no schemas. Migrating closes that half, and the migration
20+
itself found what the transcriptions were hiding:
21+
22+
- **11 alias/suggestion targets were retired keys.** `app` (8: `apis`, `aria`,
23+
`embed`, `homePageId`, `mobileNavigation`, `objects`, `sharing`, `version`),
24+
`flow` (`active`, `template`) and `flow node` (`outputSchema`) are
25+
`retiredKey()` tombstones the arrays still listed, so a near-miss was steered
26+
onto the one key guaranteed to be rejected next — ledger finding 12, three
27+
files, live. `strictObject` excludes anything the shape cannot accept, so the
28+
author now gets the tombstone's own upgrade prescription instead.
29+
- **A nav `separator` was answering with keys it rejects.** The nine navigation
30+
variants shared one transcription that handed every variant the base nav keys —
31+
but `SeparatorNavItemSchema` spreads nothing and declares `type` / `id` /
32+
`order` alone. Writing `title` on a separator was answered *"did you mean
33+
`label`?"*, and `label` was rejected too: finding 7, from the campaign built to
34+
end it. The separator now carries the alias entries whose target it really has,
35+
and one prescription for the nine base keys it does not.
36+
- **Three ADR-0010 envelopes were missing from their own pools** (`datasource`,
37+
`hook`, `sharing rule`): the protection keys the shapes spread were never
38+
transcribed, so a typo of one got no suggestion at all.
39+
40+
Author-facing messages are otherwise unchanged — the surface name, the offending
41+
key, the rename and the curated prescriptions all survive verbatim, verified by
42+
comparing every migrated surface's old array against its new derived pool and by
43+
sampling a real rejection from each.
44+
45+
Two structural consequences:
46+
47+
- the shrink-only ratchet on direct `strictUnknownKeyError` call sites is a hard
48+
**zero**, and the assertion changed meaning with the number: it no longer
49+
measures how much of the gate runs on the weaker instrument, it forbids the
50+
weaker instrument. `strictUnknownKeyError` stays published for external
51+
callers; inside `packages/spec` the only caller is `strictObject`.
52+
- `shared/alias-table-registry.ts`#5483's transitional registry — is deleted
53+
with its last call site, along with the suppression hook `strictObject` needed
54+
to stay out of it.
55+
56+
`data/object.zod.ts`'s error map was built lazily to step around a temporal dead
57+
zone; `strictObject` evaluates its options at construction, so the deferral is
58+
replaced by declaration order (`UNKNOWN_KEY_GUIDANCE` moved above the shape) and
59+
that order is now load-bearing.

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ regenerate.
2222
|---|---|
2323
| Triaged directories | 5 |
2424
| Object sites in them | 434 |
25-
| Still-open (strip) sites | 182 |
26-
| Files carrying at least one | 28 |
25+
| Still-open (strip) sites | 180 |
26+
| Files carrying at least one | 27 |
2727

2828
Remaining strip sites by class:
2929

3030
| Bucket | Sites |
3131
|---|---|
32-
| authorable — the ruling's forced scope | 43 |
32+
| authorable — the ruling's forced scope | 41 |
3333
| unresolved — needs a per-schema verdict | 33 |
3434
| wire / open — out of forced scope | 104 |
3535
| no door — no carrier, ADR-0049 territory | 1 |
@@ -44,12 +44,12 @@ The `strict` column is the one the campaign schedules against; it counts both th
4444

4545
| Dir | Sites | strict | passthrough | catchall | strip |
4646
|---|---|---|---|---|---|
47-
| `ui/` | 160 | 116 | 5 | 0 | 39 |
47+
| `ui/` | 160 | 118 | 5 | 0 | 37 |
4848
| `data/` | 162 | 54 | 1 | 0 | 107 |
4949
| `automation/` | 65 | 42 | 0 | 0 | 23 |
5050
| `security/` | 20 | 7 | 0 | 0 | 13 |
5151
| `studio/` | 27 | 27 | 0 | 0 | 0 |
52-
| **total** | **434** | **246** | **6** | **0** | **182** |
52+
| **total** | **434** | **248** | **6** | **0** | **180** |
5353

5454
## File-level triage — site counts
5555

@@ -156,21 +156,20 @@ over it is here.
156156

157157
### `ui/` — open
158158

159-
**39 strip of 160**, in 6 file(s).
159+
**37 strip of 160**, in 5 file(s).
160160

161161
| File | Strip | Sites |
162162
|---|---|---|
163163
| `action-params.zod.ts` | 1 | 1 |
164164
| `app.zod.ts` | 1 | 18 |
165-
| `chart.zod.ts` | 2 | 8 |
166165
| `component.zod.ts` | 31 | 31 |
167166
| `view.zod.ts` | 3 | 53 |
168167
| `widget.zod.ts` | 1 | 1 |
169-
| **total** | **39** | **160** |
168+
| **total** | **37** | **160** |
170169

171170
| Bucket | Sites |
172171
|---|---|
173-
| authorable — the ruling's forced scope | 34 |
172+
| authorable — the ruling's forced scope | 32 |
174173
| unresolved — needs a per-schema verdict | 0 |
175174
| wire / open — out of forced scope | 3 |
176175
| no door — no carrier, ADR-0049 territory | 1 |

0 commit comments

Comments
 (0)