@@ -13,6 +13,18 @@ import Lint from '../src/commands/lint';
1313import Diff from '../src/commands/diff' ;
1414import Explain , { SCHEMAS } from '../src/commands/explain' ;
1515import { FlowSchema } from '@objectstack/spec/automation' ;
16+ // The catalog sweep below resolves each entry's schema BY NAME, so it needs the
17+ // name surface rather than one binding. `@objectstack/spec`'s root exports none
18+ // of these Zod schemas (measured: 129 root exports, no `ObjectSchema` /
19+ // `FieldSchema` / … among them) — every one lives on a subpath, so the four
20+ // metadata-authoring subpaths the catalog draws on are named here. The named
21+ // `FlowSchema` import above stays: a value import fails loudly on a broken
22+ // export where a namespace property read would degrade to `undefined`, and the
23+ // sweep pays for its namespace form with an explicit resolvability assertion.
24+ import * as specAi from '@objectstack/spec/ai' ;
25+ import * as specAutomation from '@objectstack/spec/automation' ;
26+ import * as specData from '@objectstack/spec/data' ;
27+ import * as specUi from '@objectstack/spec/ui' ;
1628
1729describe ( 'CLI Commands (oclif)' , ( ) => {
1830 it ( 'should have compile command' , ( ) => {
@@ -118,9 +130,13 @@ describe('os explain — schema catalog accuracy', () => {
118130 // drift — it re-derives the truth from the spec on every run, which is what
119131 // the hand-maintained catalog otherwise has no way to do.
120132 // The catalog's element shape, stated locally: `SchemaInfo` is not exported,
121- // and these tests must stay honest even where `SCHEMAS` widens to `any`
122- // (this file sits outside every tsc program — see the TEST_DEBT ledger — so
123- // an implicit `any` here would silently stop checking anything).
133+ // and these tests must stay honest even where `SCHEMAS` widens to `any`.
134+ // ⚠️ The reason recorded here has CHANGED and the discipline has not. This
135+ // file no longer sits outside every tsc program: #14710 landed
136+ // `packages/cli/tsconfig.test.json`, whose `include: ["test/**/*"]` puts this
137+ // file in the program (`tsc --noEmit --listFiles -p tsconfig.test.json`
138+ // resolves it), and it carries NO row in `test-typecheck-debt.json` — so any
139+ // diagnostic it gains is red on arrival rather than silently unchecked.
124140 type CatalogField = { name : string ; type : string } ;
125141 const flowFields = ( kind : 'required' | 'optional' ) : CatalogField [ ] => SCHEMAS . flow [ kind ] ;
126142
@@ -161,3 +177,132 @@ describe('os explain — schema catalog accuracy', () => {
161177 expect ( declared ) . toContain ( 'edges' ) ;
162178 } ) ;
163179} ) ;
180+
181+ // ── `os explain` — the WHOLE catalog, swept against the spec (#14811) ──────
182+ //
183+ // #14782 pinned one entry (`flow`) by parsing its `example` against the real
184+ // schema. This generalises that technique to every entry, and derives the entry
185+ // set from `SCHEMAS` itself: a hand-written list of entries is precisely the
186+ // place a future entry escapes through unnoticed, which is the same defect this
187+ // guard closes one level down. Add a catalog entry and this block goes RED
188+ // until the entry is classified.
189+ //
190+ // ⛔ It does NOT fix what it turns red. Rewriting a catalog entry rewrites
191+ // operator-facing output and is a separate change with a separate review
192+ // question, so entries whose example does not parse today land as `it.fails`
193+ // xfails naming the card filed for each. The day one is corrected its xfail
194+ // fails ("expected to fail but passed") — promote it to a plain `it` then.
195+ //
196+ // ⛔ And it does not skip silently. Two entries resolve to no schema at all;
197+ // they get tests that ASSERT that reason. A guard reporting green over the
198+ // entries it never looked at is this card's own defect, one layer up.
199+ describe ( 'os explain — every catalog entry swept against its spec schema (#14811)' , ( ) => {
200+ type CatalogEntry = { name : string ; example : string } ;
201+ const catalog = SCHEMAS as unknown as Record < string , CatalogEntry > ;
202+
203+ // The searched name surface, stated rather than assumed: absence below means
204+ // absent from exactly these four subpaths. (`grep` over `packages/spec/src`
205+ // finds no `export const TriggerSchema` or `WorkflowSchema` anywhere at all.)
206+ const specSurface : Record < string , unknown > = {
207+ ...specData ,
208+ ...specUi ,
209+ ...specAi ,
210+ ...specAutomation ,
211+ } ;
212+
213+ type ParseResult = { success : boolean ; error ?: { issues : unknown [ ] } } ;
214+ type ZodLike = { safeParse : ( value : unknown ) => ParseResult } ;
215+
216+ // The catalog stores examples as authored source, so evaluate the literal —
217+ // the same technique as the `flow` pin above.
218+ const evaluate = ( key : string ) : unknown =>
219+ new Function ( `return (${ catalog [ key ] . example } );` ) ( ) as unknown ;
220+
221+ // Entries with one schema to parse against. `card` marks a known-broken one
222+ // and names where its errors are recorded; its absence means "must parse".
223+ const BOUND : Record < string , { schema : string ; card ?: number } > = {
224+ object : { schema : 'ObjectSchema' , card : 15170 } ,
225+ field : { schema : 'FieldSchema' } ,
226+ view : { schema : 'ViewSchema' , card : 15171 } ,
227+ flow : { schema : 'FlowSchema' } ,
228+ agent : { schema : 'AgentSchema' , card : 15172 } ,
229+ app : { schema : 'AppSchema' , card : 15173 } ,
230+ query : { schema : 'QuerySchema' } ,
231+ dashboard : { schema : 'DashboardSchema' , card : 15174 } ,
232+ action : { schema : 'ActionSchema' , card : 15175 } ,
233+ } ;
234+
235+ // Entries with NO single schema to parse against, and the reason each of the
236+ // two tests at the bottom asserts rather than merely states.
237+ const UNBOUND : Record < string , string > = {
238+ workflow :
239+ 'there is no standalone Workflow authoring type (ADR-0019) — the entry is a '
240+ + 'redirect and its example is commentary, not a literal' ,
241+ trigger :
242+ 'no `TriggerSchema` exists in the spec, and the sample is not a Hook either (#15176)' ,
243+ } ;
244+
245+ it ( 'classifies every entry in SCHEMAS — none is silently unswept' , ( ) => {
246+ const classified = [ ...Object . keys ( BOUND ) , ...Object . keys ( UNBOUND ) ] . sort ( ) ;
247+ expect (
248+ classified ,
249+ 'a new `os explain` catalog entry must be classified here: bind it to a spec '
250+ + 'schema, or give it an UNBOUND reason plus a test that asserts that reason' ,
251+ ) . toEqual ( Object . keys ( catalog ) . sort ( ) ) ;
252+ } ) ;
253+
254+ // Harness health, asserted separately from the xfails: `it.fails` is green on
255+ // ANY failure, so a broken subpath export or an unevaluable example would
256+ // otherwise keep six xfails passing while measuring nothing at all.
257+ it ( 'resolves every bound entry to a real schema, and every bound example to an object' , ( ) => {
258+ for ( const [ key , bound ] of Object . entries ( BOUND ) ) {
259+ const schema = specSurface [ bound . schema ] as ZodLike | undefined ;
260+ expect ( typeof schema ?. safeParse , `${ bound . schema } (for os explain ${ key } )` ) . toBe ( 'function' ) ;
261+ expect ( typeof evaluate ( key ) , `os explain ${ key } example` ) . toBe ( 'object' ) ;
262+ }
263+ } ) ;
264+
265+ for ( const [ key , bound ] of Object . entries ( BOUND ) ) {
266+ const parses = ( ) : void => {
267+ const schema = specSurface [ bound . schema ] as ZodLike ;
268+ const result = schema . safeParse ( evaluate ( key ) ) ;
269+ expect (
270+ result . success ,
271+ `os explain ${ key } : its example must parse as ${ bound . schema } . Issues: ${
272+ result . success ? '' : JSON . stringify ( result . error ?. issues , null , 2 )
273+ } `,
274+ ) . toBe ( true ) ;
275+ } ;
276+
277+ if ( bound . card === undefined ) {
278+ it ( `os explain ${ key } — example parses as ${ bound . schema } ` , parses ) ;
279+ } else {
280+ it . fails (
281+ `os explain ${ key } — example does NOT parse as ${ bound . schema } `
282+ + `(known-broken, filed as #${ bound . card } ; promote to a plain assertion once fixed)` ,
283+ parses ,
284+ ) ;
285+ }
286+ }
287+
288+ it ( `os explain workflow — ${ UNBOUND . workflow } ` , ( ) => {
289+ expect ( 'WorkflowSchema' in specSurface ) . toBe ( false ) ;
290+ expect ( catalog . workflow . name ) . toContain ( 'no standalone type' ) ;
291+ // Its example is commentary about the live mechanisms, not a literal.
292+ // Asserted, so "nothing was parsed here" is a property of this file rather
293+ // than an omission a reader has to notice.
294+ expect ( ( ) => evaluate ( 'workflow' ) ) . toThrow ( ) ;
295+ } ) ;
296+
297+ it ( `os explain trigger — ${ UNBOUND . trigger } ` , ( ) => {
298+ expect ( 'TriggerSchema' in specSurface ) . toBe ( false ) ;
299+ // …and it is not `HookSchema` under another name. Ruling the one real
300+ // candidate out is what makes the unbound classification a measurement
301+ // instead of an assumption: `event` is a strict-object ALIAS of `events`
302+ // (the same alias-as-a-documented-key failure the `flow` entry had), and a
303+ // hook's code slot is `handler`, so the entry's `flow` key is unrecognised.
304+ const hook = specSurface . HookSchema as ZodLike | undefined ;
305+ expect ( typeof hook ?. safeParse , 'HookSchema — the candidate this rules out' ) . toBe ( 'function' ) ;
306+ expect ( hook ! . safeParse ( evaluate ( 'trigger' ) ) . success ) . toBe ( false ) ;
307+ } ) ;
308+ } ) ;
0 commit comments