Repository navigation
Commit 582d3e5
test(cli): os explain's key-retention sweep now judges the optional/required TABLE faces (#18538)
Fixes #17266
Clause-②: no
## What this is
A **test-only pin**. There is no defect in the tree today — the card
says so itself (⛔ *"Not a defect in what #16925 shipped — a declared gap
in the guard it built"*) — and that was re-measured here rather than
taken on trust: **every row on all nine bound entries names a real
key.** So this lands green.
The key-retention sweep in `packages/cli/test/commands.test.ts` reads
the **EXAMPLE** face only, because `evaluate(key)` is the one face it
reads. A catalog entry names keys on a **second** face — its `required`
/ `optional` tables — and no assertion in that file had ever read a
row's `name` against a schema. (The #3244 pin above it reads one row's
`type` STRING; it never asks whether the row's NAME is a key.) The trap
is the same one a step earlier: a reader copies a row, the schema
silently strips the key, and the query runs unfiltered under an ordinary
success. `os explain query` shipped `filters` / `sort` on BOTH faces;
only the example half was guarded.
**The taker was the file itself.** The gap was declared in the test's
own header, so that header is rewritten in the same diff — it no longer
announces a gap that is closed, and it now points at the block that
closes it.
## The row grammar — the actual design work
A row's `name` is prose until something decides it is a key name. The
declared rule:
> A row name is read as an **ALTERNATION** — split on the pipe
character, trim — and the row is judged **iff every part is a bare
identifier**.
That is deliberately wider than "one identifier", and the width is what
earns its keep: it makes `view`'s required row (`list | form | listViews
| formViews`, four slot names in the `name` position — the card's own
blocking example) judgeable as **four keys** instead of waved through as
prose. Measured: all four are real `ViewSchema` keys.
A row that does not fit the grammar is ⛔ **not silently skipped** — it
must be declared in `PROSE_ROWS`, and a declaration that stops being
needed fails too, so a stale one cannot go on un-judging a row.
`PROSE_ROWS` is **empty today**, measured: every row on all nine bound
entries fits.
## Two techniques, because one entry does not read
| technique | entries | reading |
|:---|:---|:---|
| `.shape` | 8 of 9 | the schema's own declared key set |
| behavioural probe | `action` | plant the row's key on the entry's
parsing example with a sentinel and read the verdict |
`ActionSchema` resolves to a **pipe** (`lazySchema(() =` the arrow
`actionObject().refine(...))`), and a pipe has no shape — the card
predicted this and it holds. The probe never asks whether the VALUE is
acceptable, only whether the schema KNOWS the name: an
`unrecognized_keys` issue naming it is ABSENT (a strict object refused
it), success with the key gone from the output is ABSENT (an open object
stripped it silently), anything else is DECLARED.
Measured across all nine entries: the two techniques **agree on every
row the shape one can see**, and the probe returns ABSENT for 7 control
names on all 9 entries — including `query`, whose open top level answers
by DROPPING rather than refusing, which is the exact silent-strip mode
this guard exists for.
⭐ Each entry's test carries an inline **control key** that must read
ABSENT. That is load-bearing rather than decorative: `action` has no
second opinion, so a technique that answered "declared" to everything
would report green over twelve rows and be indistinguishable from a real
pass.
## Premises verified before writing any code
1. **"The table faces are unguarded today."** ✅ Located from its SYMBOL,
never a line number. The only read of a table face in the whole file is
`SCHEMAS.object.optional.find(...)` in the #3244 pin, which asserts the
row's `type` string. Control words known present in the same file:
`evaluate(` × 6, `.example` × 4.
2. **"`packages/cli/test/commands.test.ts` is the right and only
place."** ✅ It is the **only** file in the repository that imports the
explain catalog at all (repo-wide grep for `commands/explain`, excluding
`node_modules` and `dist`). No sibling pin exists.
3. **"No table row is wrong today."** ✅ Confirmed by both techniques
independently, on all 9 bound entries, every row, every alternation
part. Zero disagreement, zero offenders. The two UNBOUND redirect
entries carry zero rows.
## Ablation — a pin that fails on the wrong fix
Two legs, both run from the **committed** state, both mutating the
source the pin reads (`packages/cli/src/commands/explain.ts`), each with
a trap restore on absolute paths.
`HEAD` blob for that file: `09ab4c720ef0a52ffccbf9c19100a307d16568ef`
**Leg 1 — `query`, the shape technique, silent-drop mode.** Table row
`where` rewritten to `filters` (the historical defect, on the table face
only — the example keeps `where`).
- occurrence counts on disk: before `from=1 to=0`, after `from=0 to=1`
- mutated blob `535ff0bf015b9a757d2a89dabb666cfc3fae7a2f` (not equal to
HEAD blob)
- vitest exit **1** — `os explain query — every QuerySchema table row
names a key the schema declares` ✗, `Tests 1 failed | 49 passed (50)`
- ⭐ **`os explain query — example survives QuerySchema with every
declared key intact` stayed GREEN.** That is the card's claim made
mechanical: the pre-existing retention assertion cannot see this face.
- restored blob `09ab4c72...` equals the HEAD blob; `git diff HEAD` on
the path EMPTY; `git status --porcelain` empty
**Leg 2 — `action`, the probe technique, refuse-by-name mode.** Required
row `target` rewritten to `flow` (the key the entry's own description
says does not exist).
- occurrence counts on disk: before `from=1 to=0`, after `from=0 to=1`
- mutated blob `279748e6d171a66640fd020e37a24a8934e97ab7` (not equal to
HEAD blob)
- vitest exit **1** — `os explain action — every ActionSchema table row
names a key the schema declares` ✗, `Tests 1 failed | 49 passed (50)`;
both the parse and retention assertions on `action` stayed green
- restored blob equals the HEAD blob; `git diff HEAD` on the path EMPTY;
porcelain empty
Final tree state after both legs: `git diff HEAD` exit **0** (clean). ⛔
No permanent ablation artefact is left behind.
**No dist leg is owed here**: the mutated subject is imported by the
test relatively (`from '../src/commands/explain'`), so it resolves from
**source**, not through a package `exports` entry — there is no built
artefact in the resolution path for it.
## Verification
Run on `4fb225c6e` (the tip this PR opens with).
**Gate derivation** — `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, derived from the actual changed path
(1 path, `packages/cli/test/commands.test.ts`), re-derived after the
final commit and **byte-identical**. Reconciled with `--ran` carrying
exit codes:
```
Run reconciliation — 49 derived, 48 run, 1 NOT-MEASURED, 0 UNRUN.
```
- **47 green.**
- 1 parent 2e0401a commit 582d3e5
1 file changed
Lines changed: 211 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
201 | 207 | | |
202 | 208 | | |
203 | 209 | | |
| |||
317 | 323 | | |
318 | 324 | | |
319 | 325 | | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
324 | | - | |
325 | | - | |
326 | | - | |
327 | | - | |
328 | | - | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
329 | 334 | | |
330 | 335 | | |
331 | 336 | | |
| |||
391 | 396 | | |
392 | 397 | | |
393 | 398 | | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
394 | 595 | | |
395 | 596 | | |
396 | 597 | | |
| |||
0 commit comments