Repository navigation
Commit a84da60
fix(spec): the dropped-refinement ledger refusal names
Fixes #18747
Clause-②: no
## Where the two defects actually are on `main`
The card's line numbers were taken on a branch head. Re-anchored by
symbol against
`origin/main` at the base of this branch (`034f5a3afd`), in
`packages/spec/scripts/lib/dropped-refinements.ts`:
| the card said | on `main` it is at | symbol |
|---|---|---|
| `:453` | **`:496`** | the first `throw new Error` in
`readDroppedRefinementsBaseline` |
| `:456-458` | **`:499-501`** | the `entry.sites` check and the second
`throw new Error` |
| module docblock `:68` | **`:66-70`** | the "Every entry carries a
`reason`" sentence |
| (not named) | **`:134-150`** | the `DroppedRefinementsEntry` docblock
that contradicts it |
## Which side is true — measured from the consumers, not chosen
The card's open question was whether the refusal should name `sites` or
the shape
should really carry `count`/`reason`, and which docblock governs. Every
consumer that
reads this ledger answers `sites`, and none of them reads `count` or
`reason` at all:
| consumer | reading |
|---|---|
| `DroppedRefinementsEntry` (the shipped interface) | one member:
`readonly sites: readonly string[]` |
| `packages/spec/dropped-refinements.baseline.json` | 243 entries;
**243** carry `sites`, **0** carry `count`, **0** carry `reason` |
| `checkDroppedRefinements` | reads `entry.sites` only — set difference
plus a length compare |
| the `unreasoned` refusal | fires on `entry.sites.length === 0`, and
the gate prints "carry an empty `sites` list" |
| `build-schemas.ts` remedy text (both the undeclared and the miscounted
arms) | prints the corrected entry as `"sites": [ ... ]` |
| the ledger's own `description` field | documents `sites` and nothing
else |
| `dropped-refinements.test.ts` "the committed ledger" | asserts
`entry.sites.length` per entry and that
`measured.droppedRefinementSites` equals their sum |
So `sites` is the contract and the `DroppedRefinementsEntry` docblock
governs. The two
wrong texts are both residue of the module this one was copied from: in
`packages/spec/scripts/lib/unemitted-schemas.ts` the entries really are
`{ cause, reason }`, its refusal really does say so, and its gate really
does require a
non-empty `reason` (`entry.reason.trim() === ''`). Copying the file
carried the
vocabulary across without the shape.
## LIT — the red leg, both messages verbatim
The trap is a sequence, so the probe walks it: take a structurally
broken ledger, read
what the reader says the shape is, write that shape, and hand it back to
the same
function.
**BEFORE** (the module restored to `origin/main`, the rest of the tree
unchanged):
```text
[step 1] input: {"entries": []}
REFUSED dropped-refinements.baseline.json: "entries" must be an object of key -> { count, reason }
[step 2] the ledger an author writes by FOLLOWING step 1
input: {"entries":{"system/TraceSamplingConfig":{"count":1,"reason":"zod projects no custom check"}}}
REFUSED dropped-refinements.baseline.json: entry "system/TraceSamplingConfig" needs a `sites` array of path strings
```
The repair written from the message is refused by the **same function**,
four lines
below the message that prescribed it.
**AFTER**:
```text
[step 1] input: {"entries": []}
REFUSED dropped-refinements.baseline.json: "entries" must be an object of key -> { sites: string[] }
[step 3] the ledger an author writes by FOLLOWING step 1
input: {"entries":{"system/TraceSamplingConfig":{"sites":["properties.rate"]}}}
ACCEPTED (1 entry/entries)
```
The mutation leg and the restore leg were each proved on disk (the
deleted text present
and the injected text absent, then the reverse), and the restore was
verified
byte-identical to `HEAD` by `git hash-object` (`9615f4e0c0…` both sides)
with an empty
`git diff HEAD` and an empty `git status --porcelain`. The probe itself
lives outside
the repository and nothing of it is committed.
## DARK — a legitimate ledger passes on both legs, and nothing else
moved
| reading | BEFORE | AFTER |
|---|---|---|
| the real committed ledger through `readDroppedRefinementsBaseline` |
ACCEPTED — 243 entries, 737 sites | ACCEPTED — 243 entries, 737 sites |
| `pnpm --filter @objectstack/spec test` | 487 files / **14051** passed,
0 failed | 487 files / **14055** passed, 0 failed |
| `dropped-refinements.test.ts` | 23 tests | 27 tests |
The `+4` is exactly the four tests this PR adds. The BEFORE row is a
real run, not
arithmetic: both files were checked out at the merge base, the suite was
run, and both
were restored and re-verified byte-identical to `HEAD`.
Nothing else in the repository pins either message —
`git grep "must be an object of key"` returns exactly two hits, this one
and
`unemitted-schemas.ts`'s own (which is correct for its own shape, and is
the live
control on that grep).
## The pin is a closed loop, not a wording match
Error prose is not pinned here on its spelling; what is pinned is the
**named subject**
and the property that makes this class of defect a trap: whatever the
refusal names has
to be what the reader then accepts. Four cases in
`packages/spec/scripts/dropped-refinements.test.ts`:
1. the shape diagnostic names `sites`;
2. a ledger written to that shape is then ACCEPTED — the loop closes;
3. **LIT CONTROL** — the shape the old diagnostic named is refused, and
that refusal
still says `sites` (without this leg the first two pass on a reader that
accepts
anything);
4. the shape diagnostic names no key the entry shape does not have.
The docblock half has no pin, deliberately: no consumer parses a
docblock, and a
source-text assertion over prose is a gate that fails on rewording
rather than on
regression.
## Also in this diff, declared
- The module docblock's **shrink-only bullet** said the ratchet
re-checks "a recorded
`count`" — the same contradiction as the `reason` sentence the card
names, in the
paragraph above it, against the same `DroppedRefinementsEntry` docblock
("The unit is
the SITE and not a count, deliberately"). Repaired in place under the
bounded
exemption: same defect class as this card, same file, mechanical, the
corrected form
already fixed by the entry docblock, no new verification surface, and no
other claim
holds any `dropped-refinements*` path (measured across all 26 open
`claude/issue-*`
PRs, with `proof-registry.mts` reading out for #18797 as the live
control).
- `packages/spec/scripts/dropped-refinements.test.ts` was listed
read-only on the claim.
It is written here, and only to carry this card's own regression pin.
## The card's second open question — is there a third site?
`unemitted-schemas.ts`, the sibling the docblock calls itself "Identical
to", was read:
it has the same defect **nowhere**. Its docblock claim, its refusal
text, its interface
and its ledger all agree on `{ cause, reason }`. It is the correct
template, not a
second instance.
## Changeset — measured, not inferred from the path
`npm pack` on `packages/spec` after a full build, then grep over the
packed bytes
(142,490,031 of them):
| reading | hits |
|---|---|
| tarball entries under `package/scripts/` | **0** |
| `DROPPED_REFINEMENTS_BASELINE_FILE` in the packed bytes | **0** |
| `readDroppedRefinementsBaseline` | **0** |
| `must be an object of key -> { sites: string[] }` | **0** |
| positive control — entries under `package/src/` | 203 |
| positive control — entries under `package/json-schema/` | 1530 |
| positive control — `x-dropped-refinements` in the packed bytes | 486 |
| positive control — `ObjectSchema` in the packed bytes | 965 |
`packages/spec/scripts/**` is absent from the package's `files[]`, and
the build after
this change leaves `git status` clean, so no `dist/` byte moves either.
Nothing this
diff changes publishes from any released package, so it carries
`skip-changeset` rather
than a changeset.
## Verification
- `pnpm --filter @objectstack/spec build` — exit 0 (34/34 declaration
files present).
- `pnpm --filter @objectstack/spec test` — exit 0, 487 files / 14055
tests.
- `pnpm --filter @objectstack/spec typecheck` — exit 0, including
`tsconfig.scripts.json` (which is what compiles the edited file) and the
test layer.
- `node scripts/pm/dispatch-gates.mjs --commands` derived **57**
families from the
change set; all 57 were run and reconciled with `--ran`: **54 exit 0**,
**3 exit 3 =
NOT MEASURED** (`check:dual-build-cjs-loads`,
`check:lean-entry-closure`,
`check:type-check-debt` — each refuses its own prerequisite because only
`packages/spec` is built in this worktree; all three read built output,
which this
diff cannot move, and CI builds the full closure).
- `pnpm check:nul-bytes` exit 0, plus a direct control-character scan of
both edited
files — no match, with a live non-zero control on a file that carries
one.
- `origin/main` was merged in before this PR was opened (clean, no
`os-regen` deferral).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
Co-authored-by: Claude <noreply@anthropic.com>sites, the key it requires (#18816)1 parent c993b7c commit a84da60
2 files changed
Lines changed: 69 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
292 | 293 | | |
293 | 294 | | |
294 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
295 | 350 | | |
296 | 351 | | |
297 | 352 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
71 | 76 | | |
72 | 77 | | |
73 | 78 | | |
| |||
493 | 498 | | |
494 | 499 | | |
495 | 500 | | |
496 | | - | |
| 501 | + | |
497 | 502 | | |
498 | 503 | | |
499 | 504 | | |
| |||
0 commit comments