Skip to content

Commit 9573d47

Browse files
committed
feat(cli,docs): the build path reports undeclared keys too, and the gate table says so (#3786)
Follow-up to the lint's first commit, closing a gap the docs-drift check made visible: `validating-metadata.mdx` is a catalog of what `os validate`/`os build` report, and the new check was not in it. Writing the row surfaced the real omission underneath — the check ran in `validate.ts` only. `defineStack` already covers any config authored through it, whichever command loads it. What it does not cover is a config that SKIPS it — a plain object default-export, or `strict: false` — and on that path `os build` would emit an artifact with the key silently gone. `compile.ts` now runs the same pre-parse report, at the same seam as the ADR-0089 visibility rule beside it. Verified: a plain-object config with `pii` + `indexed` now warns on the build path. Not a parity-test violation either way — `validate-build-gate-parity.test.ts` enforces compile ⊆ validate, and validate already had it. This makes the two agree in the direction the test cannot see. Docs: a row in the two-entry-point table, and a §9 section showing what the finding looks like and why a retired key deliberately gets no "did you mean". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXGj3Z5TmwSV6RK2oGc3cb
1 parent acfaa14 commit 9573d47

3 files changed

Lines changed: 63 additions & 3 deletions

File tree

‎.changeset/unknown-authoring-key-lint.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,17 @@ in the protocol, so flipping them rejects metadata that parses today: a migratio
4242
event for every consumer, and one that deserves to be scheduled on evidence
4343
rather than guessed at. This produces that evidence and costs nobody a migration.
4444

45-
Wired into the two layers that perform the discard, both **pre-parse** (the parse
46-
is what eats the key, so after it there is nothing left to report):
45+
Wired into every layer that performs the discard, all **pre-parse** (the parse is
46+
what eats the key, so after it there is nothing left to report):
4747

4848
- **`defineStack`** — warns on the console, once per distinct path, in strict
4949
*and* non-strict mode, since the key is dropped either way.
5050
- **`os validate`** — a non-blocking warning, and included in `--json` output
5151
rather than computed and discarded.
52+
- **`os build` / `os compile`** — the same non-blocking warning. `defineStack`
53+
already covers configs authored through it; this catches the ones that skip it
54+
(a plain object default-export, `strict: false`), which would otherwise emit an
55+
artifact with the key quietly gone.
5256

5357
Verified against the three first-party example apps (`app-todo`, `app-crm`,
5458
`app-showcase`): all clean, no false positives.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,41 @@ literal (it comes from React state or a variable), a usage carrying a `{...sprea
209209
a chart given static `data` (its columns are the author's own), and objects
210210
another package defines.
211211

212+
### 9. Object and field keys the schema never declared
213+
214+
`ObjectSchema` and `FieldSchema` are deliberately not strict, so a key they do
215+
not declare **parses clean and is dropped** on the way to storage. Nothing fails;
216+
the setting simply is not there.
217+
218+
```ts
219+
fields: {
220+
ssn: { label: 'SSN', type: 'text', pii: true, indexed: true },
221+
// ↑ neither is a FieldSchema key → both dropped
222+
}
223+
```
224+
225+
Each one is reported with what to do about it — a rename where the concept
226+
survives under another key, or the reason it was retired where it does not:
227+
228+
```
229+
objects.employee.fields.ssn.pii: 'pii' is not a declared field key, so its value
230+
is dropped at load — the `dataQuality` governance family was pruned in 2026-06
231+
as dead in both layers — it enforced nothing.
232+
objects.employee.fields.ssn.indexed: 'indexed' is not a declared field key, so its
233+
value is dropped at load — never a FieldSchema key; a field-level index flag
234+
built no index (#2377). Declare the index in the object's `indexes[]`.
235+
```
236+
237+
Plain typos get a "did you mean" (`requred` → `required`); a retired key does
238+
not, because the nearest declared key by spelling would be noise rather than
239+
advice.
240+
241+
This is **advisory** — the stack still loads. Strict rejection is where these
242+
schemas are headed (ADR-0049 enforce-or-remove), but they are the two
243+
most-authored surfaces in the protocol, so the tightening is scheduled on what
244+
this check finds rather than assumed. `defineStack` reports the same findings at
245+
config-load time, so an author sees them without running the CLI at all.
246+
212247
## The one gate, two entry points
213248

214249
`os validate` and `os build` (alias of `os compile`) run the **same** validator:
@@ -228,6 +263,7 @@ another package defines.
228263
| View references — form targets, view-key collisions (#2554) | ✓ | ✓ |
229264
| Flow authoring anti-patterns (#1874) | ✓ | ✓ |
230265
| Liveness author-warnings | ✓ | ✓ |
266+
| Undeclared object/field keys (#3786) | ✓ | ✓ |
231267
| Emits `dist/objectstack.json` | — | ✓ |
232268

233269
So `os validate` is the fast inner-loop check (no artifact); `os build` is what

‎packages/cli/src/commands/compile.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import path from 'path';
55
import fs from 'fs';
66
import chalk from 'chalk';
77
import { ZodError } from 'zod';
8-
import { ObjectStackDefinitionSchema, normalizeStackInput, type ConversionNotice } from '@objectstack/spec';
8+
import {
9+
ObjectStackDefinitionSchema,
10+
normalizeStackInput,
11+
lintUnknownAuthoringKeys,
12+
formatUnknownAuthoringKey,
13+
type ConversionNotice,
14+
} from '@objectstack/spec';
915
import { loadConfig } from '../utils/config.js';
1016
import { lowerCallables } from '../utils/lower-callables.js';
1117
import { validateStackExpressions } from '@objectstack/lint';
@@ -251,6 +257,20 @@ export default class Compile extends Command {
251257
}
252258
}
253259

260+
// 3b-ter. [#3786] Keys `ObjectSchema` / `FieldSchema` do not declare, and
261+
// so drop silently on the way to storage. PRE-parse for the same
262+
// reason as the rule above. `defineStack` already warns for configs
263+
// authored through it; this covers the ones that skip it (a plain
264+
// object default-export, `strict: false`) and would otherwise emit an
265+
// artifact with the key quietly gone. Advisory, never fatal.
266+
const unknownKeyFindings = lintUnknownAuthoringKeys(normalized as Record<string, unknown>);
267+
if (unknownKeyFindings.length > 0 && !flags.json) {
268+
printWarning(`Undeclared authoring keys (${unknownKeyFindings.length}) — dropped at load (#3786)`);
269+
for (const f of unknownKeyFindings.slice(0, 50)) {
270+
console.log(` • ${formatUnknownAuthoringKey(f)}`);
271+
}
272+
}
273+
254274
// 3c. Widget-binding diagnostics (issues #1719/#1721) — semantic checks
255275
// that need the widget's `dataset` reference resolved to its dataset
256276
// and `dimensions`/`values` resolved to declared names. Errors are

0 commit comments

Comments
 (0)