@vibe-agent-toolkit/cli 0.2.0-rc.8 → 0.2.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/commands/audit/provenance.d.ts.map +1 -1
  2. package/dist/commands/audit/provenance.js +4 -3
  3. package/dist/commands/audit/provenance.js.map +1 -1
  4. package/dist/commands/build.d.ts.map +1 -1
  5. package/dist/commands/build.js +2 -1
  6. package/dist/commands/build.js.map +1 -1
  7. package/dist/commands/cache/clear.d.ts +17 -3
  8. package/dist/commands/cache/clear.d.ts.map +1 -1
  9. package/dist/commands/cache/clear.js +17 -3
  10. package/dist/commands/cache/clear.js.map +1 -1
  11. package/dist/commands/cache/index.d.ts.map +1 -1
  12. package/dist/commands/cache/index.js +19 -10
  13. package/dist/commands/cache/index.js.map +1 -1
  14. package/dist/commands/claude/context.d.ts +2 -2
  15. package/dist/commands/claude/context.js +4 -4
  16. package/dist/commands/claude/context.js.map +1 -1
  17. package/dist/commands/claude/index.d.ts +12 -11
  18. package/dist/commands/claude/index.d.ts.map +1 -1
  19. package/dist/commands/claude/index.js +14 -16
  20. package/dist/commands/claude/index.js.map +1 -1
  21. package/dist/commands/claude/marketplace/git-publish.d.ts.map +1 -1
  22. package/dist/commands/claude/marketplace/git-publish.js +19 -10
  23. package/dist/commands/claude/marketplace/git-publish.js.map +1 -1
  24. package/dist/commands/claude/marketplace/validate.js +2 -2
  25. package/dist/commands/claude/marketplace/validate.js.map +1 -1
  26. package/dist/commands/claude/org/skills.js +8 -8
  27. package/dist/commands/claude/org/skills.js.map +1 -1
  28. package/dist/commands/claude/plugin/build.d.ts.map +1 -1
  29. package/dist/commands/claude/plugin/build.js +11 -21
  30. package/dist/commands/claude/plugin/build.js.map +1 -1
  31. package/dist/commands/claude/plugin/plugin-files.js +3 -3
  32. package/dist/commands/claude/plugin/plugin-files.js.map +1 -1
  33. package/dist/commands/consistency-check.d.ts +11 -5
  34. package/dist/commands/consistency-check.d.ts.map +1 -1
  35. package/dist/commands/consistency-check.js +113 -78
  36. package/dist/commands/consistency-check.js.map +1 -1
  37. package/dist/commands/resources/check-progress.d.ts +6 -0
  38. package/dist/commands/resources/check-progress.d.ts.map +1 -1
  39. package/dist/commands/resources/check-progress.js +10 -2
  40. package/dist/commands/resources/check-progress.js.map +1 -1
  41. package/dist/commands/resources/check.d.ts +182 -203
  42. package/dist/commands/resources/check.d.ts.map +1 -1
  43. package/dist/commands/resources/check.js +272 -248
  44. package/dist/commands/resources/check.js.map +1 -1
  45. package/dist/commands/resources/index.d.ts.map +1 -1
  46. package/dist/commands/resources/index.js +140 -23
  47. package/dist/commands/resources/index.js.map +1 -1
  48. package/dist/commands/resources/query.d.ts +2 -2
  49. package/dist/commands/resources/query.d.ts.map +1 -1
  50. package/dist/commands/resources/query.js +20 -10
  51. package/dist/commands/resources/query.js.map +1 -1
  52. package/dist/commands/skill/test/run.js +3 -3
  53. package/dist/commands/skill/test/run.js.map +1 -1
  54. package/dist/commands/skills/build.d.ts +34 -21
  55. package/dist/commands/skills/build.d.ts.map +1 -1
  56. package/dist/commands/skills/build.js +99 -50
  57. package/dist/commands/skills/build.js.map +1 -1
  58. package/dist/commands/skills/skill-discovery.d.ts +15 -13
  59. package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
  60. package/dist/commands/skills/skill-discovery.js +23 -4
  61. package/dist/commands/skills/skill-discovery.js.map +1 -1
  62. package/dist/commands/validate.d.ts.map +1 -1
  63. package/dist/commands/validate.js +31 -8
  64. package/dist/commands/validate.js.map +1 -1
  65. package/dist/commands/verify.d.ts +10 -4
  66. package/dist/commands/verify.d.ts.map +1 -1
  67. package/dist/commands/verify.js +92 -65
  68. package/dist/commands/verify.js.map +1 -1
  69. package/dist/report-schemas.d.ts.map +1 -1
  70. package/dist/report-schemas.js +0 -2
  71. package/dist/report-schemas.js.map +1 -1
  72. package/dist/skill-resolution/classify.js +2 -2
  73. package/dist/skill-resolution/classify.js.map +1 -1
  74. package/dist/skill-resolution/resolve-skill-reference.d.ts.map +1 -1
  75. package/dist/skill-resolution/resolve-skill-reference.js +48 -52
  76. package/dist/skill-resolution/resolve-skill-reference.js.map +1 -1
  77. package/dist/utils/claude-context-lens.d.ts +34 -0
  78. package/dist/utils/claude-context-lens.d.ts.map +1 -0
  79. package/dist/utils/claude-context-lens.js +39 -0
  80. package/dist/utils/claude-context-lens.js.map +1 -0
  81. package/dist/utils/projection-lenses.d.ts +98 -0
  82. package/dist/utils/projection-lenses.d.ts.map +1 -0
  83. package/dist/utils/projection-lenses.js +203 -0
  84. package/dist/utils/projection-lenses.js.map +1 -0
  85. package/dist/utils/projection-query.d.ts +38 -14
  86. package/dist/utils/projection-query.d.ts.map +1 -1
  87. package/dist/utils/projection-query.js +78 -17
  88. package/dist/utils/projection-query.js.map +1 -1
  89. package/dist/utils/projection-store.d.ts +61 -42
  90. package/dist/utils/projection-store.d.ts.map +1 -1
  91. package/dist/utils/projection-store.js +155 -59
  92. package/dist/utils/projection-store.js.map +1 -1
  93. package/dist/utils/relation-limits.d.ts +50 -0
  94. package/dist/utils/relation-limits.d.ts.map +1 -0
  95. package/dist/utils/relation-limits.js +50 -0
  96. package/dist/utils/relation-limits.js.map +1 -0
  97. package/dist/utils/skill-discovery.d.ts.map +1 -1
  98. package/dist/utils/skill-discovery.js +2 -1
  99. package/dist/utils/skill-discovery.js.map +1 -1
  100. package/dist/utils/skill-packaging-config.d.ts +38 -3
  101. package/dist/utils/skill-packaging-config.d.ts.map +1 -1
  102. package/dist/utils/skill-packaging-config.js +28 -3
  103. package/dist/utils/skill-packaging-config.js.map +1 -1
  104. package/package.json +16 -16
  105. package/schemas/resources-check.json +43 -0
  106. package/dist/commands/claude/budget.d.ts +0 -269
  107. package/dist/commands/claude/budget.d.ts.map +0 -1
  108. package/dist/commands/claude/budget.js +0 -507
  109. package/dist/commands/claude/budget.js.map +0 -1
  110. package/dist/utils/context-budget-issues.d.ts +0 -108
  111. package/dist/utils/context-budget-issues.d.ts.map +0 -1
  112. package/dist/utils/context-budget-issues.js +0 -280
  113. package/dist/utils/context-budget-issues.js.map +0 -1
  114. package/schemas/claude-budget.json +0 -197
@@ -1,168 +1,35 @@
1
1
  /**
2
- * `vat resources check` — run the project's own SQL assertions over its
3
- * resource projection.
4
- *
5
- * ## Why this verb exists when `vat resources query` already runs SQL
6
- *
7
- * A query answers a question once. A question worth asking twice is a rule, and
8
- * a rule that lives in someone's shell history is not enforced — it decays into
9
- * a comment. This verb runs the statements a project wrote into
10
- * `resources.checks`, turns each violating row into an ordinary validation
11
- * finding, and exits non-zero when any of them is an error. That is the whole
12
- * difference, and it is the difference between a tool and a gate.
13
- *
14
- * ## 🔑 A broken check is an ERROR, never a skip
15
- *
16
- * VAT ships no schema version, so a renamed column simply breaks a check's SQL.
17
- * The tempting handling — log it and carry on — is the one thing this must never
18
- * do: a check that stopped running looks exactly like a check that passed, and
19
- * the project keeps reporting green over an assertion nobody is making any more.
20
- * So a statement that will not run is reported as a finding at **error**
21
- * severity, with the columns the projection actually has, and it fails the run.
22
- *
23
- * That report carries its OWN code (`RESOURCE_CHECK_BROKEN`), which no
24
- * `validation.severity` entry can reach. A check's severity override is about how
25
- * bad a VIOLATION is; while the two shared the code `CUSTOM:<name>`, the
26
- * documented way to stand down an inherited check also silenced the news that it
27
- * had stopped checking. See the `catch` in `runChecks`.
28
- *
29
- * ## 🔑 An empty CORPUS is a refusal too, not a pass
30
- *
31
- * The same class one level further out, and the one that shipped. `checksRun` is
32
- * the denominator of RULES; nothing counted the ROWS, so a repository whose
33
- * enumeration came back empty — a broad `.gitignore`, a shallow or sparse CI
34
- * checkout, a root that resolved somewhere else — ran every declared check over
35
- * nothing and reported `status: success` on exit 0 with empty stderr. Population
36
- * declines ignored members rather than flagging them, so the tables held no
37
- * trace of it either.
38
- *
39
- * So the population's size is published (`membersEnumerated`), and a run whose
40
- * checks executed over zero members is an ERROR under the same non-overridable
41
- * code as a broken statement — see {@link emptyCorpusFinding}. A gate that
42
- * asserts nothing over nothing has not passed.
43
- *
44
- * ## 🔑 An unknown `--check` name is a refusal, not an empty pass
45
- *
46
- * The same failure class one flag over: a filter that matched nothing ran nothing
47
- * and exited 0. `requireDeclaredCheck` refuses it before the crawl, naming the
48
- * declared set. A gate that cannot fail is worse than no gate.
49
- *
50
- * ## 🔑 NO checks at all is the same refusal, and the one that shipped longest
51
- *
52
- * A project with a valid config and no `checks:` block reported `status:
53
- * success`, `checksRun: 0`, exit 0 — so DELETING the `checks:` block silently
54
- * deleted the gate and nothing anywhere said so. A gate that checked nothing is
55
- * indistinguishable from a gate that was removed, and the entire argument for
56
- * having these gates is that you never let those two look alike. It is now a
57
- * `RESOURCE_CHECK_BROKEN` error like the other three — see
58
- * {@link noCheckRanFinding}, and `RUN_INTEGRITY_CODE` for why one code
59
- * covers all four. Declaring no checks is a legitimate CHOICE; running a gate
60
- * that can only pass is not, so the remedy is to take the command out of the
61
- * pipeline.
62
- *
63
- * ## 🔑 SQL is the surface a user OPTED IN to, so no BUILT-IN check may be SQL
64
- *
65
- * Every check this verb runs is adopter-declared: {@link checkCommand} reads
66
- * `config?.resources?.checks` and nothing else, and warns loudly when a project
67
- * declares none. There are no built-ins, and the one way to end that quietly is
68
- * to author the first one as SQL. A default-on rule written as a statement makes
69
- * the engine, the projection and the population mandatory for everyone who
70
- * inherits it, without anybody deciding that. So: **built-ins are TypeScript
71
- * predicates over the projection's row model.** SQL stays the two places a user
72
- * asked for it — this verb's declared checks, and `vat resources query`.
73
- *
74
- * ⚠️ The engine today is `node:sqlite`, built into Node, so the thing at stake is
75
- * not an install — it is the POPULATION. On an ~11,700-member adopter tree
76
- * building the projection is **>99.9%** of a check run (16.5 s cold, ~1.3 s from
77
- * the store) against **0.0008–0.004 s** for the statements themselves; see
78
- * `vat-knowledge-resources.md`, which carries that measurement. That is exactly
79
- * the cost "What this does NOT do" below refuses to put on every adopter's
80
- * pre-commit path, and a default-on SQL check would put it there without the
81
- * decision. The INSTALL cost is deferred rather than gone: the open question
82
- * about a columnar engine is a 108 MB platform binding on a published toolkit,
83
- * and this invariant is what keeps that question still answerable either way.
84
- *
85
- * ## 🔑 The DEFAULT check set belongs to the pipeline, not to the config file
86
- *
87
- * A directory with no `vibe-agent-toolkit.config.yaml` must run exactly the same
88
- * default-on checks as one with a config; config only ADDS to that set or
89
- * OVERRIDES a severity in it. This is load-bearing rather than a convenience:
90
- * *"'default-on error' is meaningless as a category if being default-on requires
91
- * a config file to say so."*
92
- *
93
- * It is therefore a constraint on WHERE the defaults live in code, not only on
94
- * what they are. They cannot be defaulted into the parsed config object, because
95
- * an absent config is `undefined` and every default would vanish with it — for
96
- * the project that never wrote one, which is the population the category exists
97
- * for. The rest of VAT already behaves this way and is the shape to match:
98
- * `vat audit` completes its entire scan first and reads config only to apply
99
- * severity overrides, warning and CONTINUING when the file cannot be read; and
100
- * `loadResourcesWithConfig` hands `vat resources validate` a registry whose
101
- * built-in validators run whether `loadConfig` returned a config or nothing.
102
- * This verb has no defaults yet, so the invariant is a rule for the first one —
103
- * and the day it is written is the easiest day to break it.
104
- *
105
- * ## Keeping a TypeScript check and its SQL twin: two good reasons, one trap
106
- *
107
- * The proposal was "write the predicate twice, in TypeScript and in SQL, and run
108
- * whichever is faster". Two thirds of it are worth having.
109
- *
110
- * ✅ **As documentation.** An SQL twin is executable documentation of the row
111
- * model, and strictly better than prose about it: prose can describe a column
112
- * that no longer exists and never say so, while a statement stops compiling —
113
- * which this verb reports as `RESOURCE_CHECK_BROKEN` rather than as a skip.
114
- *
115
- * ✅ **As a differential oracle.** Run both over the frozen corpus and assert
116
- * IDENTICAL findings. That is the only thing which makes a dual implementation
117
- * better than a single one rather than worse: two implementations with no
118
- * mandatory differential test are simply two places for one rule to be wrong.
119
- * Apply it selectively, to genuinely relational checks — done by default, every
120
- * new rule costs three artifacts instead of one.
121
- *
122
- * ⛔ **As a runtime optimizer — rejected, on two INDEPENDENT grounds.** It would
123
- * essentially never choose SQL: `buildResourceProjection` hands the rows back in
124
- * memory before the ephemeral database exists at all, so the TypeScript arm is a
125
- * for-loop over data already in hand, and the SQL arm's whole measured share of a
126
- * run is the 0.0008–0.004 s above. A scheduler would be choosing between two
127
- * arms of the 0.1%. And it reintroduces the dependency through the back door: an
128
- * optimizer that MAY choose SQL means SQL MAY be needed at check time, which for
129
- * anyone shipping the thing is the same as needed. **Optionality is not a
130
- * property you can keep while also letting a scheduler reach for the optional
131
- * thing.**
132
- *
133
- * ## 🔑 Adopter SQL runs UNSANDBOXED on purpose — and contributed SQL is not adopter SQL
134
- *
135
- * What a statement may BE is gated hard, and documented where it is enforced:
136
- * `assertIsQuery` and `detachForeignSchemas` in
137
- * `packages/projection-sqlite/src/store.ts` refuse `ATTACH` and `PRAGMA`, with
138
- * the measured cross-repository leak that motivated both, and CHANGELOG.md's
139
- * `--budget` entry carries why nothing inside this process can interrupt a
140
- * runaway statement. Read those; do not restate them here.
141
- *
142
- * What is written down nowhere else is that everything PAST those gates is
143
- * unsandboxed deliberately. A `.sql` file the adopter points their own config at
144
- * is adopter code, at the same trust boundary as their eslint config: they wrote
145
- * it or they chose to inherit it, and VAT is not the thing standing between a
146
- * project and its own repository.
147
- *
148
- * 🚨 That reasoning covers exactly ONE trust domain, and the rule scoping it is
149
- * the other half: **contributed SQL is a reviewed EXAMPLE, never an
150
- * auto-executed check.** A contribution channel is third-party text, and a
151
- * statement arriving through one has none of the "they chose it" the paragraph
152
- * above rests on. Ship it as documentation an adopter copies into their own
153
- * `resources.checks` deliberately. One paragraph must not be stretched to cover
154
- * both domains.
155
- *
156
- * ## What this does NOT do
157
- *
158
- * It does not fold into `vat resources validate`. That command is on every
159
- * adopter's pre-commit path, and adding a store-backed population to it is a
160
- * cost and a risk that deserves its own decision with its own evidence. Wiring
161
- * this into CI is one line in a workflow; making it unavoidable is not this
162
- * change's call to make.
2
+ * `vat resources check` — run the project's own assertions over its resource
3
+ * projection, as a gate.
4
+ *
5
+ * The invariants this file implements are written down once, in
6
+ * `docs/architecture/cli.md` under "`vat resources check [path]`". Read that
7
+ * before changing anything here; the headlines, so a reader knows when to:
8
+ *
9
+ * - 🔑 **Four ways to check nothing are all refusals**, at error severity under
10
+ * the non-overridable `RESOURCE_CHECK_BROKEN`: a statement that will not run
11
+ * (the `catch` in `runUnits`), a corpus that enumerated empty
12
+ * ({@link emptyCorpusFinding}), a `--check` name nothing declares
13
+ * (`requireKnownCheck`), and not one check executed at all
14
+ * ({@link noCheckRanFinding}, derived in {@link buildCheckOutputData} so an
15
+ * emptied `BUILTIN_CHECKS` cannot publish a clean document).
16
+ * - ⚠️ That last one does NOT make the step always able to fail: the only
17
+ * built-in ships at `info`. Do not invent a second refusal for it — that
18
+ * would refuse the no-config run the default set exists to serve.
19
+ * - 🔑 **No built-in check may be SQL.** {@link checkCommand} reads
20
+ * `config?.resources?.checks` and nothing else for statements; built-ins are
21
+ * TypeScript predicates over the row model, and a built-in's `sqlTwin` is
22
+ * documentation an adopter copies, never executed here.
23
+ * - 🔑 **The default set belongs to the pipeline, not the config file.**
24
+ * `BUILTIN_CHECKS` reaches {@link runProjectChecks} as a REQUIRED `builtins`
25
+ * argument via {@link runOutcome}; nothing on that path reads `config`.
26
+ * - 🚨 **Adopter SQL runs unsandboxed on purpose, and contributed SQL is not
27
+ * adopter SQL** — a contributed statement ships as a reviewed example.
28
+ * - ⛔ A TypeScript/SQL twin is documentation and a differential oracle, never
29
+ * a runtime optimizer to schedule between.
163
30
  */
164
31
  import { performance } from 'node:perf_hooks';
165
- import { issuesFromCheckRows } from '@vibe-agent-toolkit/resources';
32
+ import { bindBuiltinChecks, BUILTIN_CHECK_NAMES, issuesFromCheckRows, LIMIT_DIRECTIONS, } from '@vibe-agent-toolkit/resources';
166
33
  import { buildReport, CUSTOM_CHECK_CODE_PREFIX, ExitCode, isCustomCheckCode, reportSchema, toFindings, } from '@vibe-agent-toolkit/schema';
167
34
  import { safePath } from '@vibe-agent-toolkit/utils';
168
35
  import { z } from 'zod';
@@ -174,6 +41,7 @@ import { createLogger } from '../../utils/logger.js';
174
41
  import { writeStdoutSync, writeStructuredOutput } from '../../utils/output.js';
175
42
  import { assertDirectoryArgument, projectRootOrLoudCwd, projectRootOrNull } from '../../utils/project-root-policy.js';
176
43
  import { withQueriedProjection, } from '../../utils/projection-query.js';
44
+ import { relationBoundsFor } from '../../utils/relation-limits.js';
177
45
  import { nothingCheckedFinding, runIntegrityFinding } from '../../utils/run-integrity.js';
178
46
  import { createProgressWriter, parseProgressLog, unitInFlight, } from './check-progress.js';
179
47
  import { parseBudgetSeconds, requireSupervisableFlags, runsInThisProcess, superviseCheck, withProgressLog, } from './check-supervisor.js';
@@ -185,6 +53,38 @@ const PublishedCheckCostSchema = z.object({
185
53
  /** Rows the statement returned. ABSENT, never 0, when the statement did not complete. */
186
54
  rows: z.number().int().nonnegative().optional(),
187
55
  broken: z.literal(true).optional(),
56
+ /** Present and `true` only for a rule VAT supplied — see {@link CheckCost.builtin}. */
57
+ builtin: z.literal(true).optional(),
58
+ }).strict();
59
+ /**
60
+ * The published copy of {@link relationBoundsFor}'s answer.
61
+ *
62
+ * The registry lists are `readonly` and this document is handed to a serializer,
63
+ * so the arrays are copied rather than referenced — the same rule
64
+ * `lensesEvaluated` follows one field above: a published document owns no
65
+ * reference into the run's own state.
66
+ *
67
+ * @param lensesEvaluated - The lenses this run evaluated
68
+ * @returns The bounds keys to spread, or nothing
69
+ */
70
+ function structuredBounds(lensesEvaluated) {
71
+ const bounds = relationBoundsFor(lensesEvaluated);
72
+ if (bounds.boundsStatement === undefined || bounds.limits === undefined)
73
+ return {};
74
+ return { boundsStatement: bounds.boundsStatement, limits: bounds.limits.map((limit) => ({ ...limit })) };
75
+ }
76
+ /**
77
+ * A signed bound on what the derived rows settle, as `@vibe-agent-toolkit/resources`
78
+ * states it.
79
+ *
80
+ * ⛔ The direction vocabulary is IMPORTED, never respelled: `LIMIT_DIRECTIONS`
81
+ * is the one list, tied to `StatedLimit['direction']` by `satisfies`, so a
82
+ * fifth direction cannot leave this schema quietly rejecting it.
83
+ */
84
+ const StatedLimitSchema = z.object({
85
+ id: z.string(),
86
+ direction: z.enum(LIMIT_DIRECTIONS),
87
+ statement: z.string(),
188
88
  }).strict();
189
89
  /**
190
90
  * What the check run reports beyond its findings.
@@ -200,8 +100,28 @@ export const CheckDataSchema = z.object({
200
100
  population: z.enum(['derived', 'store']),
201
101
  /** What the population cost — charged to no check, see `CheckCost`. */
202
102
  populationSecs: z.number().nonnegative(),
203
- /** What evaluating the lens cost, paid before the first statement ran. */
103
+ /** What evaluating the lenses cost, paid before the first statement ran. */
204
104
  lensSecs: z.number().nonnegative(),
105
+ /**
106
+ * Which lenses that covers — the derived relations this run's checks could
107
+ * actually read. 🔑 An empty list says "no check asked for a derived relation"
108
+ * rather than "a lens stopped running"; without it `lensSecs: 0` is the same
109
+ * document either way, and a gate whose rules silently read empty relations is
110
+ * a gate that cannot fail.
111
+ */
112
+ lensesEvaluated: z.array(z.string()),
113
+ /**
114
+ * The prose frame {@link CheckDataSchema.limits} is read under. Present
115
+ * exactly when a lens with stated bounds was evaluated.
116
+ */
117
+ boundsStatement: z.string().optional(),
118
+ /**
119
+ * What the derived rows this run could read do NOT settle — signed and
120
+ * directional, the same list `vat claude context` publishes beside its own
121
+ * answer. Absent when no bounded lens ran: an empty list would claim nothing
122
+ * bounds the answer, which is stronger than "the answer holds no such row".
123
+ */
124
+ limits: z.array(StatedLimitSchema).optional(),
205
125
  /** The number of rules that EXECUTED. Derived from `checks`, never carried beside it. */
206
126
  checksRun: z.number().int().nonnegative(),
207
127
  /** What each rule cost, directly under the denominator it is the breakdown of. */
@@ -242,22 +162,28 @@ export const CHECK_REPORT_SCHEMA = reportSchema(CheckDataSchema);
242
162
  * stand-down matters here for an interrupted run, which recovers zero costs when
243
163
  * the kill landed before the first statement finished and already carries
244
164
  * {@link interruptedRunFinding}, the more specific of the two claims. A `--check`
245
- * filter that matched nothing is refused earlier by {@link requireDeclaredCheck},
165
+ * filter that matched nothing is refused earlier by {@link requireKnownCheck},
246
166
  * and must stay refused even if that guard is ever widened.
247
167
  *
168
+ * 🔑 **An absent `checks:` block does not empty the denominator** — the built-in
169
+ * set is in the pipeline, so reaching zero here means not even a default ran and
170
+ * the remedy is not "declare some checks". The guard stays because it is the
171
+ * mechanism-level guarantee that `checksRun: 0` can never be published clean,
172
+ * whatever empties it.
173
+ *
248
174
  * @param costs - One record per check that EXECUTED; its length is `checksRun`
249
175
  * @param issues - What the run already found, so an existing run-integrity
250
176
  * report is not duplicated
251
177
  * @returns The finding, or nothing when at least one check ran
252
178
  */
253
179
  function noCheckRanFinding(costs, issues) {
254
- return nothingCheckedFinding(costs.length, issues, () => 'No check ran, so this document is not a verdict: a gate that checked nothing'
180
+ return nothingCheckedFinding(costs.length, issues, () => 'No check ran at all, so this document is not a verdict: a gate that checked nothing'
255
181
  + ' produces the same report as a gate that was removed, and this run cannot tell'
256
182
  + ' you which happened.'
257
- + ' Declare checks under `resources.checks` in vibe-agent-toolkit.config.yaml —'
258
- + ' each is a description plus one SQL statement selecting the rows that VIOLATE it —'
259
- + ' and if this project deliberately has none, take `vat resources check` out of the'
260
- + ' pipeline rather than leaving a step that can only pass.');
183
+ + ' This is not an absent `resources.checks` block — VAT\'s own built-in checks run'
184
+ + ' whether or not a project declares any, so reaching zero means not even those'
185
+ + ' executed. Re-run with --debug and report it; if the run was filtered, check that'
186
+ + ' --check named a rule this build still ships.');
261
187
  }
262
188
  /**
263
189
  * Build the check report.
@@ -317,6 +243,16 @@ export function buildCheckOutputData(input) {
317
243
  // lens's cost to whichever rule the reader happened to be looking at, which
318
244
  // is the same defect `populationSecs` exists to prevent.
319
245
  lensSecs: formatDurationSecs(input.lensMs),
246
+ // What that number covers — see `CheckDataSchema.lensesEvaluated`. Copied
247
+ // into a plain array so the published document owns no reference into the
248
+ // run's own state.
249
+ lensesEvaluated: [...input.lensesEvaluated],
250
+ // The bounds of the rows those lenses produced, stated ONCE and only when
251
+ // a bounded lens ran. This is the verb an adopter makes GATE, so it is the
252
+ // one that most owes them — `utils/relation-limits.ts` carries why.
253
+ // Copied out of the readonly registry lists: the published document owns
254
+ // no reference into module state a later render could mutate.
255
+ ...structuredBounds(input.lensesEvaluated),
320
256
  // The denominator of rules. Derived from `checks`, never carried beside it.
321
257
  checksRun: input.costs.length,
322
258
  // What each rule cost, directly under the denominator it is a breakdown
@@ -336,6 +272,9 @@ export function buildCheckOutputData(input) {
336
272
  durationSecs: formatDurationSecs(cost.durationMs),
337
273
  ...(cost.rows === undefined ? {} : { rows: cost.rows }),
338
274
  ...(cost.broken === undefined ? {} : { broken: cost.broken }),
275
+ // Absent, never `false`, on a declared rule — so the key's presence is
276
+ // the whole claim and a reader never has to tell `false` from unset.
277
+ ...(cost.builtin === undefined ? {} : { builtin: cost.builtin }),
339
278
  })),
340
279
  },
341
280
  });
@@ -399,7 +338,7 @@ function timed(now, run) {
399
338
  *
400
339
  * 🪤 Only `ask` reaches this. Turning the selected rows INTO findings is not a
401
340
  * statement failure, and reporting it as one told the operator a working rule
402
- * was broken — see the append in {@link runChecks}.
341
+ * was broken — see the append in {@link runUnits}.
403
342
  *
404
343
  * @param name - The check's key in `resources.checks`
405
344
  * @param error - Whatever the statement threw
@@ -410,48 +349,85 @@ function brokenCheckFinding(name, error) {
410
349
  + (error instanceof Error ? error.message : String(error)));
411
350
  }
412
351
  /**
413
- * Run every declared check, collect its findings, and price it.
414
- *
415
- * ## What the clock measures, and what it must not
416
- *
417
- * The span is `ask(check.sql)` and nothing else — structurally, because it is
418
- * the thunk handed to {@link timed}, which exists for that reason and documents
419
- * the mutation that proved an inline stopwatch unguardable. Everything before
420
- * the loop — the git tracker, `buildResourceProjection`, loading the ephemeral
421
- * database — is paid ONCE for all of them and reaches the document as
422
- * `populationSecs`; see {@link CheckCost} for why charging it here would be a
423
- * lie in N places.
424
- *
425
- * ⚠️ `performance.now()`, never `Date.now()`. A rule over a small projection is
426
- * routinely sub-millisecond, and a millisecond-granularity clock reports every
427
- * one of them as `0` — which reads as "not measured" and makes the whole
428
- * attribution worthless exactly where it is cheapest to get right.
429
- *
430
- * 🪤 Exactly one cost record per executed check, on both paths, because
431
- * `checksRun` is this list's length. The record is filed before the rows become
432
- * findings, so nothing between the two can leave a check unpriced.
433
- *
434
- * ## 🚨 Why the loop announces itself before it can be stopped
435
- *
436
- * {@link ProgressSink} is told a check is about to run, and told again when it
437
- * is priced. That ordering is not decoration: this verb runs adopter-authored
438
- * SQL unattended, an accidental cross join or an unterminated `WITH RECURSIVE`
439
- * runs forever, and NOTHING in-process can interrupt it — the query is
440
- * synchronous, it holds the event loop, and `node:sqlite` exposes no interrupt.
441
- * The supervisor's only lever is an external `SIGKILL`, which publishes nothing
442
- * of the killed process's memory. So the name of the rule that hangs has to be
443
- * on disk BEFORE it is entered, or it is not recoverable at all.
352
+ * One declared statement, as a unit.
444
353
  *
354
+ * @param name - The check's key in `resources.checks`
355
+ * @param check - Its declaration
356
+ * @param ask - Runs one statement against the populated projection
357
+ * @returns The runnable unit
358
+ */
359
+ function sqlCheckUnit(name, check, ask) {
360
+ return {
361
+ name,
362
+ select: () => {
363
+ const rows = ask(check.sql);
364
+ return { rows: rows.length, report: () => issuesFromCheckRows(name, check, rows) };
365
+ },
366
+ };
367
+ }
368
+ /**
369
+ * One built-in predicate, as a unit.
370
+ *
371
+ * 🪤 Its `rows` is the finding count, and that is the same quantity a statement
372
+ * reports: one finding per violating row. There is no conversion step to leave
373
+ * outside the clock, so the whole predicate is timed.
374
+ *
375
+ * @param builtin - The built-in, already bound to this run's projection
376
+ * @returns The runnable unit
377
+ */
378
+ function builtinCheckUnit(builtin) {
379
+ return {
380
+ name: builtin.name,
381
+ builtin: true,
382
+ select: () => {
383
+ const issues = builtin.run();
384
+ return { rows: issues.length, report: () => issues };
385
+ },
386
+ };
387
+ }
388
+ /**
389
+ * Every check this run will attempt, in the order it will attempt them.
390
+ *
391
+ * 🔑 **Built-ins FIRST, and never interleaved with the declared ones**, so the
392
+ * document's `checks[]` opens with the same rules in the same order for every
393
+ * project and a reader comparing two runs compares like with like.
394
+ *
395
+ * 🪤 A project MAY declare a check whose key equals a built-in's name. Both run,
396
+ * both are priced, `builtin` on the cost record tells them apart, and
397
+ * `--check <name>` selects both. Not refused: a new config error is a breaking
398
+ * change and nothing about the collision is unsafe.
399
+ *
400
+ * @param builtins - The default set, bound to this run's projection
445
401
  * @param checks - The project's `resources.checks`
446
- * @param only - A single check key to run, or undefined for all of them
447
402
  * @param ask - Runs one statement against the populated projection
403
+ * @returns The units, built-ins first
404
+ */
405
+ function checkUnits(builtins, checks, ask) {
406
+ return [
407
+ ...builtins.map((builtin) => builtinCheckUnit(builtin)),
408
+ ...Object.entries(checks).map(([name, check]) => sqlCheckUnit(name, check, ask)),
409
+ ];
410
+ }
411
+ /**
412
+ * Run every unit, collect its findings, and price it.
413
+ *
414
+ * 🚨 **The timed span is `unit.select()` and nothing else** — structurally,
415
+ * because it is the thunk handed to {@link timed}. 🚨 **And the loop announces a
416
+ * check BEFORE it can be stopped**, because a runaway statement can only be
417
+ * ended by an external `SIGKILL` that publishes nothing of the killed process's
418
+ * memory. Both, with the clock-granularity and one-record-per-check rules they
419
+ * rest on: `docs/architecture/cli.md`, "The population is charged to NOBODY" and
420
+ * "Why the loop announces a check before it can be stopped".
421
+ *
422
+ * @param units - Every check this run will attempt, built-ins first
423
+ * @param only - A single check name to run, or undefined for all of them
448
424
  * @param now - The clock, in milliseconds; injected so a test can assert an
449
425
  * exact duration rather than a range that passes on a timer that never started
450
426
  * @param onProgress - Told what is about to run and what it cost, or undefined
451
427
  * when nothing is watching (the in-process lane, and `--budget 0`)
452
428
  * @returns The findings, and one cost record per check that ran
453
429
  */
454
- function runChecks(checks, only, ask, now, onProgress) {
430
+ function runUnits(units, only, now, onProgress) {
455
431
  const issues = [];
456
432
  const costs = [];
457
433
  /** File the cost with the observer as well as with the caller — one place. */
@@ -459,23 +435,28 @@ function runChecks(checks, only, ask, now, onProgress) {
459
435
  costs.push(cost);
460
436
  onProgress?.({ kind: 'check', ...cost });
461
437
  };
462
- for (const [name, check] of Object.entries(checks)) {
438
+ for (const unit of units) {
439
+ const { name } = unit;
463
440
  if (only !== undefined && name !== only)
464
441
  continue;
442
+ // Spread onto every cost record this unit files, so the two arms below
443
+ // cannot disagree about which set the rule came from.
444
+ const origin = unit.builtin === undefined ? {} : { builtin: unit.builtin };
465
445
  // 🪤 BEFORE `timed`, and not one line later. `ask` is where a runaway
466
446
  // statement never returns from, so an announcement below it is filed by
467
447
  // every check except the one whose name the operator needs.
468
448
  onProgress?.({ kind: 'start', name });
469
- const outcome = timed(now, () => ask(check.sql));
449
+ const outcome = timed(now, unit.select);
470
450
  if (!outcome.ok) {
471
- // Priced up to the throw, and with NO row count: a statement that did not
451
+ // Priced up to the throw, and with NO row count: a check that did not
472
452
  // complete selected nothing, and `rows: 0` would say it selected nothing
473
- // and passed.
474
- price({ name, durationMs: outcome.durationMs, broken: true });
453
+ // and passed. A built-in reaches this arm too — a defect in VAT's own
454
+ // predicate must fail the gate rather than vanish.
455
+ price({ name, durationMs: outcome.durationMs, broken: true, ...origin });
475
456
  issues.push(brokenCheckFinding(name, outcome.error));
476
457
  continue;
477
458
  }
478
- price({ name, durationMs: outcome.durationMs, rows: outcome.value.length });
459
+ price({ name, durationMs: outcome.durationMs, rows: outcome.value.rows, ...origin });
479
460
  // 🪤 Appended one at a time, NEVER `issues.push(...findings)`. A spread
480
461
  // becomes an ARGUMENT LIST, which throws `RangeError: Maximum call stack
481
462
  // size exceeded` past roughly 125,000 elements on the main thread — and a
@@ -485,44 +466,49 @@ function runChecks(checks, only, ask, now, onProgress) {
485
466
  // the throw landed in the arm above: a rule that ran perfectly was reported
486
467
  // as one that "could not run, so it is asserting nothing". A loop does not
487
468
  // put the argument limit in play at all.
488
- for (const issue of issuesFromCheckRows(name, check, outcome.value))
469
+ for (const issue of outcome.value.report())
489
470
  issues.push(issue);
490
471
  }
491
472
  return { issues, costs };
492
473
  }
493
474
  /**
494
- * Refuse a `--check` name the project does not declare.
475
+ * Refuse a `--check` name that names neither a built-in nor a declared check.
495
476
  *
496
- * 🔑 **An unknown name used to be a silent green.** The filter was a bare
497
- * `if (only !== undefined && name !== only) continue;` and nothing compared the
498
- * flag against the declared keys, so `--check nope` filtered every check away,
499
- * ran none, and exited 0 with `checksRun: 0` and `issues: []` on empty stderr.
500
- * The loud "no checks are declared" warning never fired, because it asks whether
501
- * the `checks` MAP is empty, never whether the FILTER matched anything.
502
- *
503
- * That is not a hypothetical typo: `vat resources check --check orphan-skills`
504
- * is the example in this command's own help text. Rename or delete that check
505
- * and the CI step keeps passing forever while asserting nothing.
477
+ * 🔑 **An unknown name is otherwise a silent green.** With nothing comparing the
478
+ * flag against the known names, `--check nope` filters every check away, runs
479
+ * none, and exits 0 with `checksRun: 0` and `issues: []` on empty stderr — and
480
+ * the "no checks are declared" warning does not fire, because it asks whether
481
+ * the `checks` MAP is empty, never whether the FILTER matched anything. Not a
482
+ * hypothetical typo: rename or delete a check and the CI step keeps passing
483
+ * forever while asserting nothing.
506
484
  *
507
485
  * Thrown rather than reported as a finding, because it is an OPERATOR error and
508
486
  * not a content violation — the two have different exit codes in this command's
509
487
  * contract (2 versus 1) and different audiences. Thrown before the projection is
510
488
  * populated, so a mistyped flag costs no crawl.
511
489
  *
490
+ * 🔑 **A built-in is nameable here exactly like a declared one.** A guard that
491
+ * only knew the config would refuse the very rules that run when there is no
492
+ * config at all. The refusal names both sets.
493
+ *
494
+ * @param builtinNames - The default set's names — {@link BUILTIN_CHECK_NAMES} in
495
+ * production, and passed in rather than imported here so a test can prove the
496
+ * guard rather than restate the constant
512
497
  * @param checks - The project's `resources.checks`
513
498
  * @param only - The `--check` value, or undefined
514
- * @throws When `only` names no declared check
499
+ * @throws When `only` names no check this run would attempt
515
500
  */
516
- export function requireDeclaredCheck(checks, only) {
501
+ export function requireKnownCheck(builtinNames, checks, only) {
517
502
  // 🪤 `Object.hasOwn`, not `only in checks`. `in` walks the prototype chain, so
518
503
  // `--check toString` would have looked declared, matched nothing, and restored
519
504
  // the exact silent green this guard closes.
520
- if (only === undefined || Object.hasOwn(checks, only))
505
+ if (only === undefined || builtinNames.includes(only) || Object.hasOwn(checks, only))
521
506
  return;
522
507
  const declared = Object.keys(checks);
523
- throw new Error(`No check named "${only}" is declared. `
508
+ throw new Error(`No check named "${only}" exists. `
509
+ + `Built in: ${builtinNames.join(', ')}. `
524
510
  + (declared.length === 0
525
- ? 'This project declares none — add them under `resources.checks` in'
511
+ ? 'This project declares none of its own — add them under `resources.checks` in'
526
512
  + ' vibe-agent-toolkit.config.yaml.'
527
513
  : `Declared under \`resources.checks\`: ${declared.join(', ')}.`));
528
514
  }
@@ -530,7 +516,7 @@ export function requireDeclaredCheck(checks, only) {
530
516
  * Name every `CUSTOM:` severity override that overrides nothing.
531
517
  *
532
518
  * 🔑 **The same silent-green shape as an unknown `--check` name, one surface
533
- * over.** {@link requireDeclaredCheck} refuses `--check nope` because a filter
519
+ * over.** {@link requireKnownCheck} refuses `--check nope` because a filter
534
520
  * that matched nothing ran nothing and exited 0. A `validation.severity` entry
535
521
  * keyed `CUSTOM:<name>` for a check that is not declared is the mirror image: it
536
522
  * parses (the schema validates the KEY's shape and cannot see
@@ -563,7 +549,7 @@ export function requireDeclaredCheck(checks, only) {
563
549
  export function warnUndeclaredOverrides(checks, validation, logger) {
564
550
  const stale = Object.keys(validation?.severity ?? {})
565
551
  .filter((code) => isCustomCheckCode(code))
566
- // 🪤 `Object.hasOwn`, not `in` — same reason {@link requireDeclaredCheck}
552
+ // 🪤 `Object.hasOwn`, not `in` — same reason {@link requireKnownCheck}
567
553
  // uses it: `in` walks the prototype chain, so an override keyed
568
554
  // `CUSTOM:toString` would look declared and stay silent.
569
555
  .filter((code) => !Object.hasOwn(checks, code.slice(CUSTOM_CHECK_CODE_PREFIX.length)));
@@ -1007,6 +993,7 @@ export function buildInterruptedCheckInput(options) {
1007
993
  population: population.population,
1008
994
  populationMs: population.populationMs,
1009
995
  lensMs: population.lensMs,
996
+ lensesEvaluated: population.lensesEvaluated,
1010
997
  membersEnumerated: population.membersEnumerated,
1011
998
  issues: [interruptedRunFinding(unitInFlight(entries), ending)],
1012
999
  // 🪤 Rebuilt field by field rather than passed through. The log's check
@@ -1019,12 +1006,19 @@ export function buildInterruptedCheckInput(options) {
1019
1006
  durationMs: entry.durationMs,
1020
1007
  ...(entry.rows === undefined ? {} : { rows: entry.rows }),
1021
1008
  ...(entry.broken === undefined ? {} : { broken: entry.broken }),
1009
+ ...(entry.builtin === undefined ? {} : { builtin: entry.builtin }),
1022
1010
  })),
1023
1011
  };
1024
1012
  }
1025
1013
  /**
1026
- * Run the declared checks and resolve their severities — the whole of this
1027
- * verb's logic, with the database taken out.
1014
+ * Run this project's checks — VAT's default set plus whatever the project
1015
+ * declared — and resolve their severities: the whole of this verb's logic, with
1016
+ * the database taken out.
1017
+ *
1018
+ * 🔑 **`builtins` is a required argument beside `checks`, never a default inside
1019
+ * this function.** That is what makes "the default set belongs to the pipeline,
1020
+ * not to the config file" a property of the signature — nobody can get the
1021
+ * defaults by omitting an argument or lose them by supplying no config.
1028
1022
  *
1029
1023
  * Exported and pure so the loop is provable without a spawn. It was not: every
1030
1024
  * spawned case declared ONE check (the `--check` case declares two and filters
@@ -1037,8 +1031,11 @@ export function buildInterruptedCheckInput(options) {
1037
1031
  * trust.
1038
1032
  *
1039
1033
  * @param options - The run
1034
+ * @param options.builtins - The default set, already bound to this run's
1035
+ * projection. Required, and `[]` is a deliberate statement rather than an
1036
+ * omission
1040
1037
  * @param options.checks - The declared checks
1041
- * @param options.only - A single check key, or undefined for all
1038
+ * @param options.only - A single check name, or undefined for all
1042
1039
  * @param options.ask - Runs one statement against the populated projection
1043
1040
  * @param options.validation - The project's severity overrides, or undefined
1044
1041
  * @param options.membersEnumerated - How many members the population enumerated.
@@ -1053,9 +1050,9 @@ export function buildInterruptedCheckInput(options) {
1053
1050
  * unit case run with nobody watching
1054
1051
  * @returns The resolved findings, and one cost record per check that ran
1055
1052
  */
1056
- export function runDeclaredChecks(options) {
1053
+ export function runProjectChecks(options) {
1057
1054
  const now = options.now ?? (() => performance.now());
1058
- const { issues, costs } = runChecks(options.checks, options.only, options.ask, now, options.onProgress);
1055
+ const { issues, costs } = runUnits(checkUnits(options.builtins, options.checks, options.ask), options.only, now, options.onProgress);
1059
1056
  // 🚨 HERE, and this line moved to get here. `checks-complete` buys the phase
1060
1057
  // after the last statement a fresh watchdog window, and three places
1061
1058
  // documented that phase as "resolving severities and serialising". It was
@@ -1066,7 +1063,7 @@ export function runDeclaredChecks(options) {
1066
1063
  // which is the useful direction: the alternative was to weaken three docs.
1067
1064
  //
1068
1065
  // ⚠️ It does NOT cover the last check's row-to-issue conversion, which
1069
- // `runChecks` does after filing that check's cost — see {@link inFlightPhrase}
1066
+ // `runUnits` does after filing that check's cost — see {@link inFlightPhrase}
1070
1067
  // for why the `idle` sentence still has to hedge for that window.
1071
1068
  options.onProgress?.({ kind: 'checks-complete' });
1072
1069
  // The run-integrity report leads. An aggregate check selects a row whatever
@@ -1083,6 +1080,12 @@ export function runDeclaredChecks(options) {
1083
1080
  // It reaches a check's VIOLATIONS only. `RESOURCE_CHECK_BROKEN` is not an
1084
1081
  // overridable code, so the news that a check stopped checking survives every
1085
1082
  // override an adopter can write about the check itself.
1083
+ //
1084
+ // 🔑 A BUILT-IN's findings pass through the identical call, under their
1085
+ // ordinary registry code: `severity: { CLAUDE_RULE_GLOB_INERT: ignore }`
1086
+ // silences the default exactly as it silences any other code VAT emits, with
1087
+ // no `CUSTOM:` spelling and no second override surface. That the two lanes
1088
+ // share this one line is the reason there is nothing to keep in step.
1086
1089
  issues: resolveIssueSeverity(reported, options.validation),
1087
1090
  costs,
1088
1091
  };
@@ -1252,18 +1255,20 @@ export async function checkCommand(pathArg, options) {
1252
1255
  const config = loadConfigCached(projectRoot);
1253
1256
  const checks = config?.resources?.checks;
1254
1257
  if (checks === undefined || Object.keys(checks).length === 0) {
1255
- // 🪤 Stderr only, and it decides NOTHING. The refusal lives in
1256
- // {@link noCheckRanFinding}, on the document, because the document is
1257
- // what the exit code is computed from and what CI parses; this warning is
1258
- // the same statement addressed to the human, and the two are one statement
1259
- // per audience. It used to be the ONLY channel, and the document said the
1260
- // opposite — `status: success`, exit 0 — which is the half that matters.
1261
- logger.warn('No checks are declared. Add them under `resources.checks` in'
1262
- + ' vibe-agent-toolkit.config.yaml; each is a description plus one SQL'
1263
- + ' statement selecting the rows that VIOLATE it.');
1258
+ // 🪤 Stderr only, and it decides NOTHING — the document is what the exit
1259
+ // code is computed from and what CI parses. ⚠️ It does not stand beside a
1260
+ // refusal: the built-in set runs here, so this run DID assert something.
1261
+ // What the operator needs to know is that the project is relying entirely
1262
+ // on VAT's defaults, which is what a deleted `checks:` block looks like.
1263
+ logger.warn(`No checks of this project's own are declared, so this run asserted only VAT's`
1264
+ + ` built-in set (${BUILTIN_CHECK_NAMES.join(', ')}).`
1265
+ + ' Add your own under `resources.checks` in vibe-agent-toolkit.config.yaml;'
1266
+ + ' each is a description plus one SQL statement selecting the rows that VIOLATE it.'
1267
+ + ' `vat resources check --help` prints the SQL each built-in is equivalent to,'
1268
+ + ' as a starting point.');
1264
1269
  }
1265
1270
  // Before the crawl: a mistyped flag is an operator error and pays nothing.
1266
- requireDeclaredCheck(checks ?? {}, options.check);
1271
+ requireKnownCheck(BUILTIN_CHECK_NAMES, checks ?? {}, options.check);
1267
1272
  // Also before the crawl, and for the same reason: an override that overrides
1268
1273
  // nothing is knowable from the config alone, and the operator should read it
1269
1274
  // beside the other config news rather than after a full population.
@@ -1315,7 +1320,13 @@ async function runOutcome(options) {
1315
1320
  // bare throw, and the finding exists precisely so a renamed column cannot end a
1316
1321
  // gate quietly. The population is the price of an honest report, and it is
1317
1322
  // charged once for every check rather than per broken one.
1318
- return withQueriedProjection({ root, logger }, (ask, provenance, extent) => {
1323
+ // 🔑 `statements` IS passed, and it is a different question from `preflight`:
1324
+ // it only decides which derived relations are evaluated. Every declared
1325
+ // statement goes in, including one `--check` narrows away — the lens is per
1326
+ // RUN, and narrowing it would make `--check foo` and a full run disagree about
1327
+ // which relations exist.
1328
+ const statements = Object.values(checks).map((check) => check.sql);
1329
+ return withQueriedProjection({ root, logger, statements }, (ask, provenance, extent, projection) => {
1319
1330
  // 🔑 Emitted HERE, inside the callback, because this is the one place where
1320
1331
  // the provenance and the extent are both exactly in hand — and emitted the
1321
1332
  // INSTANT population completes, because its arrival is what tells a
@@ -1327,14 +1338,27 @@ async function runOutcome(options) {
1327
1338
  population: provenance.population,
1328
1339
  populationMs: provenance.populationMs,
1329
1340
  lensMs: provenance.lensMs,
1341
+ lensesEvaluated: [...provenance.lensesEvaluated],
1330
1342
  membersEnumerated: extent.membersEnumerated,
1331
1343
  });
1332
1344
  // 🪤 `checks-complete` is NOT emitted here. It used to be, and that put
1333
- // `resolveIssueSeverity` — which runs inside `runDeclaredChecks` — on the
1345
+ // `resolveIssueSeverity` — which runs inside `runProjectChecks` — on the
1334
1346
  // wrong side of the line, still charged to the last check's budget window
1335
1347
  // while three separate docs said otherwise. It is emitted by
1336
- // `runDeclaredChecks` itself now, the instant the last statement is done.
1337
- const ran = runDeclaredChecks({ checks, only, ask, validation, ...extent, onProgress });
1348
+ // `runProjectChecks` itself now, the instant the last statement is done.
1349
+ //
1350
+ // 🔑 The DEFAULT SET enters HERE, from a module constant bound to the
1351
+ // projection — never from `config`, which this line does not read. That is
1352
+ // the placement the "defaults belong to the pipeline" invariant is about.
1353
+ const ran = runProjectChecks({
1354
+ builtins: bindBuiltinChecks(projection),
1355
+ checks,
1356
+ only,
1357
+ ask,
1358
+ validation,
1359
+ ...extent,
1360
+ onProgress,
1361
+ });
1338
1362
  return { ...ran, ...provenance, ...extent };
1339
1363
  });
1340
1364
  }