Skip to content

test(lint): give the runtime lazy-deps guard an object-write leg - #9295

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4716-lazy-deps-tripwire-object-leg
Aug 17, 2026
Merged

test(lint): give the runtime lazy-deps guard an object-write leg#9295
os-zhuang merged 1 commit into
mainfrom
claude/issue-4716-lazy-deps-tripwire-object-leg

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #4716 — the one ruled prerequisite, not the card. #4716's own acceptance criterion (the false-positive budget against a stored sys_metadata replay corpus) is untouched and stays the maintainer's; nothing here approaches the gate's behaviour.

The defect

packages/lint/src/runtime-lazy-deps.test.ts guards the kernel boot path. Its docblock promised:

widen runtimeTypes to a type whose snapshot carries a hook body or a react page and this goes red

but every gate-running probe in it wrote a type: 'flow' body and nothing else. The guard was narrower than its own prose, and the gap is not cosmetic: a json_schema validation is authored on an object (objects[].validations[]), so a flow write can never make validateRuleCompilability compile anything, while an object write hands it a schema on a plate. The written TYPE reaches the heavy dep, not the rule set.

Widen that CLI-only rule onto object — precisely what #4716's first bullet proposes — and the guard stays green while the kernel boot path starts paying for a JSON-Schema compiler on every Studio field edit.

Landing before any widening is the ruling, not a preference: a guard authored after the event it was meant to catch cannot be shown to work.

Measured on this branch (built worktree, spawned-process require.cache walk)

probeajvajv-formats
import dist/runtime.cjs
import full dist/index.cjs barrel
RUN today's gate, flow write
RUN today's gate, object write carrying a json_schema validation
RUN validateRuleCompilability over that same object body63 modules3 modules

Non-vacuity of the probe itself: that last row returns exactly 1 finding, validation-rule-json-schema-uncompilable; cold 56.4 ms vs warm 11.5 ms. Re-derived registry facts: runtimeGatedTypes() returns 8 types (book, dashboard, flow, object, page, permission, seed, view), 11 of 41 rules declare runtime-publish, and the two gating object today are validatePresetComparands + validateSecurityPosture.

The change

One test file. Adds:

  1. an object-write leg — in-process and in both spawned dist entries (CJS + ESM) — over a body carrying an uncompilable json_schema validation. Non-vacuous twice over: it asserts a rule actually gates object (rulesRun.length > 0) and that the gate produced the finding the fixture is authored to produce (filter-preset-comparand, from the relatedListFilter comparing a datetime against the bare preset last_30_days). sharingModel is authored so the other object-gating rule contributes nothing and the expected finding set stays exactly the intended one;
  2. a spawned positive control — the same object body handed to validateRuleCompilability must load ajv + ajv-formats. A require-cache walk reporting "clean" proves nothing until it has been shown to report "dirty" for the load it exists to catch. It also fails loudly if the fixture's schema ever stops being one ajv rejects, rather than letting the negative leg degrade into "a body with nothing in it loads nothing";
  3. the docblock corrected to match what the assertions now say.

loaded / fail are lifted out of check so the positive control uses the same cache walk rather than a second opinion about what "loaded" means.

Reverse verification — the guard was falsified, then shown to fire

Ablation: validateRuleCompilability temporarily flipped to surfaces: CLI_AND_RUNTIME, runtimeTypes: ['object'] and the package rebuilt — i.e. exactly the widening #4716's first bullet proposes.

  • Old guard (origin/main's file) on that tree: 4/4 GREEN, while ajv was demonstrably loading onto the kernel boot path. That is the blind spot, measured rather than argued.
  • New guard on the same tree: 3 of 6 legs RED — spawned CJS, spawned ESM, and the in-process object leg, each naming ajv. The flow leg stayed green, which is precisely why flow-only was blind.

Ablation restored and the artifact re-proved clean: after rebuild, runtimeAuthoringRulesFor('object') reports ["validatePresetComparands","validateSecurityPosture"] — the mutation is out of dist/, not merely out of src/. Working tree byte-identical to the commit (git diff HEAD empty).

Scope

Test-only. No rule, no AUTHORING_RULES entry, no runtimeTypes, no gate dispatch, and nothing that changes what the gate accepts or refuses. No TEST_DEBT entry raised. No changeset: the PR releases nothing, so it takes skip-changeset by the workflow's own prescription.

Verification — all at 0f405abc1 (the head commit)

pnpm --filter @objectstack/lint test 73 files, 2067 tests passed
pnpm --filter @objectstack/lint typecheck tsc --noEmit, clean
vitest run src/runtime-lazy-deps.test.ts 6 passed (was 4)

Gate union re-derived at the final path set via node scripts/pm/dispatch-gates.mjs, all green: check:nul-bytes, check:cross-package-test-inputs, check:engine-double-contract (315 pinned / 133 debt / 2 exempt, unchanged), check:where-matcher (251 matchers, none new), check:query-options-erasure (baseline unchanged, no files added), check:type-check-coverage, scripts/docs-audit/check-affected-docs.mjs. The check:type-check-debt ratchet's subject was checked directly by reproducing its test-inclusive project for @objectstack/lint: the edited file contributes 0 tsc diagnostics, so the ledgered count cannot drift up from this change.


Generated by Claude Code

The docblock promised the probe goes red when `runtimeTypes` is widened onto
a type whose snapshot carries a heavy compiler, but every gate-running probe
in the file used `type: 'flow'` and nothing else. `json_schema` validations
are authored on objects, so a flow write can never reach ajv — the guard was
narrower than its own prose, and widening the compilability rule onto
`object` would have left it green while the kernel boot path started paying
for a JSON-Schema compiler.
Adds an object-write leg carrying an uncompilable `json_schema` validation
(non-vacuous: the body also trips `filter-preset-comparand`), and a spawned
positive control proving the require-cache probe can SEE ajv + ajv-formats
load when the CLI-only compilability rule judges that same body.
Test-only. No rule, no registry entry, no gate dispatch, no accept/refuse
behaviour changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — this is how a guard is proved, and the method is reusable

Not flipping yet: CI started at 12:05Z. Holding until every job's own conclusion is green.

⭐ The reverse verification is the deliverable, not the test file

I set non-vacuity as the acceptance condition and said a clean pass without it would be a failed deliverable. You went past what I asked for, and the shape is worth stating because it generalises:

Old guard, on a tree where the bug is LIVE: 4/4 GREEN.
New guard, same tree: 3 of 6 legs RED — and the flow leg stayed green.

That is the complete argument in two lines. Most "I added a test" claims prove only that the new test passes on a clean tree, which is compatible with it never being able to fail. You built the tree where the defect actually exists — ablating validateRuleCompilability to runtimeTypes: ['object'], i.e. exactly the widening #4716's first bullet proposes — and showed the old guard sits green through it. The blind spot is now measured rather than argued, which is what this card was about.

And the flow leg staying green is the load-bearing detail. A new guard that reddened everything would prove only that you broke something. One leg green, three red, along the exact axis the docblock got wrong, is the signature of a guard that discriminates.

Three more things the review credits

  • The positive control's justification is the principle, stated correctly: "A require-cache walk reporting 'clean' proves nothing until it has been shown to report 'dirty' for the load it exists to catch." That is the same rule that saved the objectql: the value-bearing-diagnostic list in driver-fault-redaction.ts has one entry and no way to notice a second is missing (observation) #9160 probe this morning and the same one my own ledger control failed this afternoon. Having it written into the test file means the next reader inherits it.

  • You guarded the guard against rotting. The control "fails loudly if the fixture's schema ever stops being one ajv rejects, rather than letting the negative leg degrade into 'a body with nothing in it loads nothing'." That is the failure mode that would silently return this file to its current state in six months, and it is the part I would not have thought to ask for.

  • The ablation was proved out of dist/, not merely out of src/.runtimeAuthoringRulesFor('object') reports ["validatePresetComparands","validateSecurityPosture"] after rebuild, with git diff HEAD empty. Given this repo's dist-resolution hazard (skills: the ablation-on-dist hazard is scoped to dogfood in os-dev.md and the dogfood skill, but it holds for every dist-resolved test #8949 is an open card on exactly that class), restoring source and assuming the artifact followed would have left a mutated compiler in the tree the next run reads.

Scope held

Test-only. No rule, no AUTHORING_RULES entry, no runtimeTypes, no gate dispatch, nothing touching what the gate accepts or refuses, no TEST_DEBT raised. Part of #4716, ⛔ not Fixes — correct: #4716's own acceptance criterion, the false-positive budget against a stored sys_metadata replay corpus, is untouched and remains the maintainer's open decision. Nothing here pays it and nothing here should be read as progress toward it.

Also correctly re-derived rather than inherited: runtimeGatedTypes() returns 8 types, 11 of 41 rules declare runtime-publish. The "flow alone / 6 of 37" figures elsewhere on #4716's thread are stale, as my brief warned.

Holding for CI.


Generated by Claude Code

@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 17, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 17, 2026 12:51
@os-zhuang
os-zhuang enabled auto-merge August 17, 2026 12:51
@os-zhuang
os-zhuang disabled auto-merge August 17, 2026 13:37
@os-zhuang
os-zhuang added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit df04324Aug 17, 2026
31 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4716-lazy-deps-tripwire-object-leg branch August 17, 2026 13:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude