@event4u/agent-config 13.0.0 → 14.1.0
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +195 -315
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +336 -112
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2128 -1326
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1764 -899
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +5 -5
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gate_ledger.ts +5 -1
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/build_archive_index.ts +62 -3
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_gate_coverage.ts +93 -0
- package/src/scripts/check_pr_ci_current.ts +492 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/git_authorization_hook.ts +186 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/block_unauthorized_git.ts +62 -6
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
|
@@ -86,16 +86,27 @@ import {
|
|
|
86
86
|
parseHostBudgetEvent,
|
|
87
87
|
readObservationLog,
|
|
88
88
|
readProjectedCatalogue,
|
|
89
|
+
resolveSkillsRoot,
|
|
89
90
|
type HostProjectionRow,
|
|
90
91
|
type ProjectionMode,
|
|
91
92
|
type ProjectionModeCounts,
|
|
92
93
|
type SelectorReport,
|
|
93
94
|
} from './_lib/skill_catalogue.js';
|
|
95
|
+
import {
|
|
96
|
+
cadenceStatus,
|
|
97
|
+
formatCadenceStatus,
|
|
98
|
+
formatPointableBare,
|
|
99
|
+
joinPointableBare,
|
|
100
|
+
scopeFlagDecision,
|
|
101
|
+
} from './_lib/skill_catalogue_series.js';
|
|
94
102
|
import { scoped_projection_stats } from './_lib/scoped_projection.js';
|
|
95
103
|
import { iter_skills } from './update_counts.js';
|
|
96
104
|
|
|
97
|
-
// Re-exported so the CLI module stays the one public name for this tool
|
|
105
|
+
// Re-exported so the CLI module stays the one public name for this tool —
|
|
106
|
+
// both halves, so the split into a series layer is an internal seam and not a
|
|
107
|
+
// second import path every consumer has to learn.
|
|
98
108
|
export * from './_lib/skill_catalogue.js';
|
|
109
|
+
export * from './_lib/skill_catalogue_series.js';
|
|
99
110
|
|
|
100
111
|
const _HERE = fileURLToPath(import.meta.url);
|
|
101
112
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -151,6 +162,230 @@ function expandHome(p: string): string {
|
|
|
151
162
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
152
163
|
}
|
|
153
164
|
|
|
165
|
+
/**
|
|
166
|
+
* `--today <ISO>` for a deterministic run, else the machine's own date.
|
|
167
|
+
*
|
|
168
|
+
* Range-checked by round-trip, not by shape alone. `2026-13-45` matches the
|
|
169
|
+
* pattern and then parses to `NaN`, which reaches `cadenceStatus` and makes
|
|
170
|
+
* every host report `stamp unparseable (<the record's date>)` — a diagnostic
|
|
171
|
+
* blaming the corpus for an operator typo.
|
|
172
|
+
*/
|
|
173
|
+
function argToday(): string {
|
|
174
|
+
const explicit = argValue('--today');
|
|
175
|
+
if (explicit) {
|
|
176
|
+
const parsed = new Date(`${explicit}T00:00:00Z`);
|
|
177
|
+
if (
|
|
178
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(explicit) ||
|
|
179
|
+
Number.isNaN(parsed.getTime()) ||
|
|
180
|
+
parsed.toISOString().slice(0, 10) !== explicit
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(`--today must be a real ISO date (YYYY-MM-DD), got: ${explicit}`);
|
|
183
|
+
}
|
|
184
|
+
return explicit;
|
|
185
|
+
}
|
|
186
|
+
return new Date().toISOString().slice(0, 10);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `--cadence` — is an observation due on any host, and how far is the corpus?
|
|
191
|
+
*
|
|
192
|
+
* The recurring half of the capture. It reports and prints the next command; it
|
|
193
|
+
* never records anything itself. Two reasons, both deliberate: recording is what
|
|
194
|
+
* `--host-event --record` and `--observed --record` already do, and a mode that
|
|
195
|
+
* both decides freshness AND writes a record could refresh a series with a
|
|
196
|
+
* reading nobody took.
|
|
197
|
+
*
|
|
198
|
+
* The asymmetry between the hosts is printed rather than smoothed over. The
|
|
199
|
+
* codex round is one shell pipeline and can be scheduled; the claude round needs
|
|
200
|
+
* an agent to read its own delivered context, which no scheduler can do.
|
|
201
|
+
*/
|
|
202
|
+
function runCadenceMode(): number {
|
|
203
|
+
const today = argToday();
|
|
204
|
+
const records = readObservationLog(path.join(REPO, OBSERVATION_LOG));
|
|
205
|
+
const rows = cadenceStatus(records, today);
|
|
206
|
+
|
|
207
|
+
if (process.argv.includes('--json')) {
|
|
208
|
+
process.stdout.write(
|
|
209
|
+
`${JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
today,
|
|
212
|
+
// `daysSince` is NaN when a record's stamp is unparseable, and
|
|
213
|
+
// `JSON.stringify(NaN)` emits `null` — indistinguishable from an
|
|
214
|
+
// absent field to a machine consumer. This module states every
|
|
215
|
+
// other absence explicitly, so this one is stated too.
|
|
216
|
+
hosts: rows.map((row) => ({
|
|
217
|
+
...row,
|
|
218
|
+
daysSince: Number.isNaN(row.daysSince) ? null : row.daysSince,
|
|
219
|
+
daysSinceKnown: !Number.isNaN(row.daysSince),
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
null,
|
|
223
|
+
2,
|
|
224
|
+
)}\n`,
|
|
225
|
+
);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
process.stdout.write(`${formatCadenceStatus(rows, today)}\n`);
|
|
230
|
+
|
|
231
|
+
if (rows.length === 0) {
|
|
232
|
+
// The one state in which every host is maximally overdue is the state
|
|
233
|
+
// with no host to name: `cadenceStatus` only knows hosts it has already
|
|
234
|
+
// seen. Saying so beats printing a "next round" header over nothing.
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
'\nno host is known yet, so no next command can be named — the corpus has never been\n' +
|
|
237
|
+
'written to. Take a first observation on any host, then this mode has a series to keep current.\n',
|
|
238
|
+
);
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const due = rows.filter((row) => row.due);
|
|
243
|
+
if (due.length === 0) return 0;
|
|
244
|
+
|
|
245
|
+
// ONE walk, hoisted: `projectionModeCounts` parses the frontmatter of every
|
|
246
|
+
// skill in the tree, and the counts are a property of the package, not of
|
|
247
|
+
// the host being reported — calling it per due host walked the tree twice
|
|
248
|
+
// for a value that cannot differ, against its own docstring.
|
|
249
|
+
const counts = projectionModeCounts();
|
|
250
|
+
|
|
251
|
+
process.stdout.write('\nnext round — run what is due:\n\n');
|
|
252
|
+
for (const row of due) {
|
|
253
|
+
const scopeFlag = projectionModeFlagFor(row.host, counts);
|
|
254
|
+
// Keyed on the HOST, never on the latest record's source. Only codex is
|
|
255
|
+
// known to publish a parseable budget event, and `row.source` comes from
|
|
256
|
+
// a single headline record whose same-date tie `_supersedes` breaks on
|
|
257
|
+
// `dropped_count` — a self-report record has none, so one same-date
|
|
258
|
+
// host-event row could flip a self-report host onto the codex pipeline
|
|
259
|
+
// and record codex's truncation under another host's name.
|
|
260
|
+
if (row.host === 'codex') {
|
|
261
|
+
process.stdout.write(
|
|
262
|
+
` # ${row.host} — deterministic, read off the host's own JSON channel\n` +
|
|
263
|
+
` codex exec --json --skip-git-repo-check "reply with exactly: OK" \\\n` +
|
|
264
|
+
` | agent-config exec capture_skill_catalogue \\\n` +
|
|
265
|
+
` --host-event - --host ${row.host} --host-root ~/.${row.host} \\\n` +
|
|
266
|
+
` ${scopeFlag.flag}--record --observed-at ${today}\n` +
|
|
267
|
+
`${scopeFlag.note}\n`,
|
|
268
|
+
);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
process.stdout.write(
|
|
272
|
+
` # ${row.host} — self-report; only an agent reading its own context can produce it.\n` +
|
|
273
|
+
` # Write the entries seen bare and the ones seen described, one name per line, to\n` +
|
|
274
|
+
` # agents/evidence/metrics/skill-catalogue/${today}-${row.host}-{bare,described}.txt\n` +
|
|
275
|
+
` agent-config exec capture_skill_catalogue \\\n` +
|
|
276
|
+
` --observed agents/evidence/metrics/skill-catalogue/${today}-${row.host}-bare.txt \\\n` +
|
|
277
|
+
` --described agents/evidence/metrics/skill-catalogue/${today}-${row.host}-described.txt \\\n` +
|
|
278
|
+
` --host ${row.host} ${scopeFlag.flag}--record --observed-at ${today}\n` +
|
|
279
|
+
`${scopeFlag.note}\n`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The `--projection-mode` flag for one host's round, MEASURED off its installed
|
|
287
|
+
* root — or omitted, with the reason, when the root carries neither mode count.
|
|
288
|
+
*
|
|
289
|
+
* Printing `--projection-mode <scoped|legacy-all>` as a placeholder invites the
|
|
290
|
+
* operator to pick one, and on an `indeterminate` root either pick is a label
|
|
291
|
+
* nobody measured. Measured 2026-08-18 on this machine: `~/.codex` holds 297
|
|
292
|
+
* skills against a scoped projection of 219 and a legacy-all of 290 — it matches
|
|
293
|
+
* neither, because it is a stale install rather than a broken one. So the honest
|
|
294
|
+
* output for that root is no flag and a stated reason, which keeps the record's
|
|
295
|
+
* absent-scope field meaning "not determinable" instead of "not asked".
|
|
296
|
+
*/
|
|
297
|
+
function projectionModeFlagFor(host: string, counts: ProjectionModeCounts): { flag: string; note: string } {
|
|
298
|
+
const root = expandHome(`~/.${host}`);
|
|
299
|
+
const exists = fs.existsSync(root);
|
|
300
|
+
const decision = scopeFlagDecision(
|
|
301
|
+
root,
|
|
302
|
+
exists,
|
|
303
|
+
exists ? classifyHostProjection(measureCatalogueVolume(host, root), counts) : null,
|
|
304
|
+
);
|
|
305
|
+
const wrapped = decision.reason
|
|
306
|
+
.split(/(?<=\.) (?=[A-Z])/)
|
|
307
|
+
.map((part) => ` # ${part}`)
|
|
308
|
+
.join('\n');
|
|
309
|
+
return decision.mode === null
|
|
310
|
+
? { flag: '', note: ` # no --projection-mode:\n${wrapped}\n` }
|
|
311
|
+
: { flag: `--projection-mode ${decision.mode} `, note: `${wrapped}\n` };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `--pointable-bare` — the D-4 join: host truth against disk truth.
|
|
316
|
+
*
|
|
317
|
+
* Intersects the bare names of every per-entry observation with the catalogue
|
|
318
|
+
* the runtime ranker reads, and publishes how many skills the ranker can name
|
|
319
|
+
* while the model never received their description.
|
|
320
|
+
*
|
|
321
|
+
* `--catalogue-root` is honoured here, through the SAME resolver the main path
|
|
322
|
+
* uses. Two resolvers over one catalogue is how the ranker and the tool that
|
|
323
|
+
* reports on it start reading different trees — `resolveSkillsRoot`'s own
|
|
324
|
+
* docstring names that hazard — and silently ignoring the flag in one mode is
|
|
325
|
+
* exactly that split inside a single invocation.
|
|
326
|
+
*/
|
|
327
|
+
function runPointableBareMode(): number {
|
|
328
|
+
const explicitRoot = argValue('--catalogue-root');
|
|
329
|
+
const rankerRoot = explicitRoot ? resolveCatalogueRoot(explicitRoot) : resolveSkillsRoot(REPO);
|
|
330
|
+
if (rankerRoot === null) {
|
|
331
|
+
process.stderr.write(
|
|
332
|
+
'❌ no catalogue root resolved for the ranker — tried ' +
|
|
333
|
+
`${DEFAULT_CATALOGUE_ROOTS.join(', ')} under ${REPO}.\n` +
|
|
334
|
+
' An empty catalogue is never a clean join: it would report 0 pointable\n' +
|
|
335
|
+
' entries because nothing was read, not because nothing diverged.\n',
|
|
336
|
+
);
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
const catalogueNames = readProjectedCatalogue(rankerRoot).map((entry) => entry.name);
|
|
340
|
+
// The guard the error text above already promised, and did not have. A
|
|
341
|
+
// present-but-empty or half-generated projection resolves fine — the
|
|
342
|
+
// resolver returns the first EXISTING directory, not the first non-empty
|
|
343
|
+
// one — and then every row scores 0 pointable off a catalogue nobody read,
|
|
344
|
+
// printing a clean D-4 verdict. That is the zero-inferred-from-silence this
|
|
345
|
+
// module's header forbids outright, and the sibling path guards it the same
|
|
346
|
+
// way.
|
|
347
|
+
if (catalogueNames.length === 0) {
|
|
348
|
+
process.stderr.write(
|
|
349
|
+
`❌ the ranker catalogue at ${rankerRoot} holds no entries.\n` +
|
|
350
|
+
' An empty catalogue is never a clean join: every observation would score 0\n' +
|
|
351
|
+
' pointable because nothing was read, not because nothing diverged.\n',
|
|
352
|
+
);
|
|
353
|
+
return 1;
|
|
354
|
+
}
|
|
355
|
+
const records = readObservationLog(path.join(REPO, OBSERVATION_LOG));
|
|
356
|
+
const join = joinPointableBare(records, catalogueNames);
|
|
357
|
+
|
|
358
|
+
if (process.argv.includes('--json')) {
|
|
359
|
+
process.stdout.write(
|
|
360
|
+
`${JSON.stringify(
|
|
361
|
+
{
|
|
362
|
+
ranker_catalogue_root: path.relative(REPO, rankerRoot) || rankerRoot,
|
|
363
|
+
ranker_catalogue_entries: catalogueNames.length,
|
|
364
|
+
// The disk side carries the same scope discipline the host
|
|
365
|
+
// side does: `pointableBare` grows with a stale-large root,
|
|
366
|
+
// so which projection the join ran against is recorded
|
|
367
|
+
// rather than left to the entry count to imply.
|
|
368
|
+
ranker_catalogue_projection: classifyHostProjection(
|
|
369
|
+
measureCatalogueVolume('ranker', rankerRoot),
|
|
370
|
+
projectionModeCounts(),
|
|
371
|
+
).matches,
|
|
372
|
+
skipped_non_per_entry: join.skippedNonPerEntry,
|
|
373
|
+
skipped_malformed: join.skippedMalformed,
|
|
374
|
+
observations: join.rows,
|
|
375
|
+
},
|
|
376
|
+
null,
|
|
377
|
+
2,
|
|
378
|
+
)}\n`,
|
|
379
|
+
);
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
process.stdout.write(
|
|
384
|
+
`${formatPointableBare(join, path.relative(REPO, rankerRoot) || rankerRoot, catalogueNames.length)}\n`,
|
|
385
|
+
);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
154
389
|
/**
|
|
155
390
|
* `--limits` — publish the per-host picture: what each host was measured to
|
|
156
391
|
* deliver, and whether the observed hosts truncate the same way.
|
|
@@ -196,14 +431,19 @@ function runLimitsMode(): number {
|
|
|
196
431
|
* same pair of numbers in one run. Each host is reported on its own row: no
|
|
197
432
|
* host's count is ever used to say anything about another's.
|
|
198
433
|
*/
|
|
199
|
-
|
|
434
|
+
/** The package's two projection-mode counts, one walk. Shared by both modes. */
|
|
435
|
+
function projectionModeCounts(): ProjectionModeCounts {
|
|
200
436
|
const stats = scoped_projection_stats(REPO, iter_skills());
|
|
201
|
-
|
|
437
|
+
return {
|
|
202
438
|
scoped: stats.projected,
|
|
203
439
|
legacyAll: stats.total,
|
|
204
440
|
prunedUnderScoped: stats.pruned,
|
|
205
441
|
activePacks: stats.active_packs,
|
|
206
442
|
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function runProjectionModesMode(): number {
|
|
446
|
+
const counts = projectionModeCounts();
|
|
207
447
|
|
|
208
448
|
const rows: HostProjectionRow[] = [];
|
|
209
449
|
for (const rootArg of argValues('--host-root')) {
|
|
@@ -350,6 +590,8 @@ function runHostEventMode(source: string): number {
|
|
|
350
590
|
}
|
|
351
591
|
|
|
352
592
|
function main(): number {
|
|
593
|
+
if (process.argv.includes('--cadence')) return runCadenceMode();
|
|
594
|
+
if (process.argv.includes('--pointable-bare')) return runPointableBareMode();
|
|
353
595
|
if (process.argv.includes('--limits')) return runLimitsMode();
|
|
354
596
|
if (process.argv.includes('--projection-modes')) return runProjectionModesMode();
|
|
355
597
|
|
|
@@ -425,9 +667,27 @@ function main(): number {
|
|
|
425
667
|
}
|
|
426
668
|
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
427
669
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
670
|
+
const projectionMode = argProjectionMode();
|
|
671
|
+
if (projectionMode === undefined) {
|
|
672
|
+
// A WARNING, not a refusal. Recording without the scope is a
|
|
673
|
+
// legitimate reading — the record type says absence is not
|
|
674
|
+
// `legacy-all` and a consumer comparing modes skips it — so
|
|
675
|
+
// rejecting it would refuse an observation to protect a
|
|
676
|
+
// comparison. But an unscoped record is silently incomparable,
|
|
677
|
+
// which is the failure this line exists to make loud.
|
|
678
|
+
process.stdout.write(
|
|
679
|
+
'\n⚠️ no --projection-mode given: this record will carry no projection scope.\n' +
|
|
680
|
+
' Absence is NOT `legacy-all` — a mode comparison skips a record that\n' +
|
|
681
|
+
' carries none, so the observation stays outside that series. Pass\n' +
|
|
682
|
+
' --projection-mode <scoped|legacy-all> to keep it comparable.\n',
|
|
683
|
+
);
|
|
684
|
+
}
|
|
428
685
|
fs.appendFileSync(
|
|
429
686
|
logPath,
|
|
430
|
-
|
|
687
|
+
// The catalogue root IS the skill tree, so every projected entry is
|
|
688
|
+
// a skill: the count is file-measured, not derived from the
|
|
689
|
+
// artefact total the way a host root's would be.
|
|
690
|
+
`${JSON.stringify(buildObservationRecord(report, host, stampedAt, projected.length, projectionMode))}\n`,
|
|
431
691
|
);
|
|
432
692
|
process.stdout.write(`\nrecorded → ${OBSERVATION_LOG}\n`);
|
|
433
693
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Gate — every registered evaluator emits output conforming to
|
|
4
|
+
* `evaluator-output.schema.json`, over recorded verifier fixtures.
|
|
5
|
+
*
|
|
6
|
+
* Ships in the SAME change as the schema on purpose: the estate's standing
|
|
7
|
+
* constraint is that a convention without a machine backstop is dead text, so a
|
|
8
|
+
* schema landing without its check would be exactly the failure the roadmap's
|
|
9
|
+
* Risk-Register rank-5 names.
|
|
10
|
+
*
|
|
11
|
+
* WHY FIXTURES RATHER THAN LIVE RUNS. The adapters are pure functions from a
|
|
12
|
+
* verifier's captured `{stdout, stderr, exitCode}` to the contract shape, so the
|
|
13
|
+
* mapping — the part that can be wrong — is fully exercised without re-running
|
|
14
|
+
* three verifiers CI already runs once. Running them here would double their CI
|
|
15
|
+
* cost to test code that never touches them. `--live` re-records nothing and is
|
|
16
|
+
* not offered: a fixture that drifts from its verifier is caught by that
|
|
17
|
+
* verifier's own gate changing shape, which is a review event, not a silent one.
|
|
18
|
+
*
|
|
19
|
+
* The corpus is (evaluator x fixture) pairs, and it includes deliberately RED
|
|
20
|
+
* and DEGRADED fixtures — a gate that only ever sees green output cannot tell a
|
|
21
|
+
* working mapping from one that hardcodes success.
|
|
22
|
+
*
|
|
23
|
+
* Exit codes:
|
|
24
|
+
* 0 — every pair conforms and every expectation holds
|
|
25
|
+
* 1 — one or more pairs violate the schema or their expected reading
|
|
26
|
+
* 2 — the gate could not run (fixture directory missing or empty)
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
import * as os from 'node:os';
|
|
33
|
+
|
|
34
|
+
import { EVALUATORS, validateEvaluatorOutput, type RawRun } from './_lib/evaluator_contract.js';
|
|
35
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
36
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
40
|
+
const FIXTURE_DIR = path.join(process.cwd(), 'tests', 'fixtures', 'evaluator-output');
|
|
41
|
+
|
|
42
|
+
/** What each fixture flavour must read as, so a green-only corpus cannot pass. */
|
|
43
|
+
const EXPECTED: Readonly<Record<string, { pass: boolean; metric_state: string }>> = {
|
|
44
|
+
green: { pass: true, metric_state: 'present' },
|
|
45
|
+
red: { pass: false, metric_state: 'present' },
|
|
46
|
+
degraded: { pass: true, metric_state: 'unreadable' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readRun(name: string, flavour: string): RawRun | null {
|
|
50
|
+
const base = path.join(FIXTURE_DIR, `${name}.${flavour}`);
|
|
51
|
+
if (!fs.existsSync(`${base}.stdout`)) return null;
|
|
52
|
+
return {
|
|
53
|
+
stdout: fs.readFileSync(`${base}.stdout`, 'utf-8'),
|
|
54
|
+
stderr: fs.existsSync(`${base}.stderr`) ? fs.readFileSync(`${base}.stderr`, 'utf-8') : '',
|
|
55
|
+
exitCode: fs.existsSync(`${base}.exit`)
|
|
56
|
+
? Number(fs.readFileSync(`${base}.exit`, 'utf-8').trim())
|
|
57
|
+
: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `--self-test` — prove, from the shipped binary, that the rejections still fire.
|
|
63
|
+
*
|
|
64
|
+
* Each case builds a throwaway tree holding only a fixture directory and runs
|
|
65
|
+
* this gate against it with `cwd` pointing there, so the cases exercise the CLI
|
|
66
|
+
* a contributor actually invokes rather than an imported function.
|
|
67
|
+
*/
|
|
68
|
+
function selfTest(): number {
|
|
69
|
+
const mk = (files: Record<string, string>): string => {
|
|
70
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ces-selftest-'));
|
|
71
|
+
const dir = path.join(root, 'tests', 'fixtures', 'evaluator-output');
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
74
|
+
fs.writeFileSync(path.join(dir, rel), body, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
return root;
|
|
77
|
+
};
|
|
78
|
+
const run = (root: string): number =>
|
|
79
|
+
runGateCli(REPO_ROOT, 'src/scripts/check_evaluator_schema.ts', ['--quiet'], root);
|
|
80
|
+
|
|
81
|
+
const GREEN_FM = '\n== Frontmatter schema: 437 artefacts, 0 failing, 0 with warnings ==\n';
|
|
82
|
+
|
|
83
|
+
return runSelfTest({
|
|
84
|
+
gate: 'check_evaluator_schema',
|
|
85
|
+
minCases: 4,
|
|
86
|
+
minRejectCases: 3,
|
|
87
|
+
cases: [
|
|
88
|
+
{
|
|
89
|
+
name: 'a conformant green fixture passes',
|
|
90
|
+
expect: 'accept',
|
|
91
|
+
run: () =>
|
|
92
|
+
run(
|
|
93
|
+
mk({
|
|
94
|
+
'validate_frontmatter.green.stdout': GREEN_FM,
|
|
95
|
+
'validate_frontmatter.green.exit': '0\n',
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'a green fixture with no parseable count is rejected — unreadable is not present',
|
|
101
|
+
expect: 'reject',
|
|
102
|
+
run: () =>
|
|
103
|
+
run(
|
|
104
|
+
mk({
|
|
105
|
+
'validate_frontmatter.green.stdout': 'done, nothing to report\n',
|
|
106
|
+
'validate_frontmatter.green.exit': '0\n',
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'a red fixture that exits 0 is rejected — pass must follow the exit code',
|
|
112
|
+
expect: 'reject',
|
|
113
|
+
run: () =>
|
|
114
|
+
run(
|
|
115
|
+
mk({
|
|
116
|
+
'validate_frontmatter.red.stdout':
|
|
117
|
+
'\n== Frontmatter schema: 437 artefacts, 3 failing, 0 with warnings ==\n',
|
|
118
|
+
'validate_frontmatter.red.exit': '0\n',
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'an empty fixture directory is rejected — a gate that scanned nothing has verified nothing',
|
|
124
|
+
expect: 'reject',
|
|
125
|
+
run: () => run(mk({})),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function main(argv?: readonly string[]): number {
|
|
132
|
+
const args = argv ?? process.argv.slice(2);
|
|
133
|
+
if (args.includes('--self-test')) return selfTest();
|
|
134
|
+
const quiet = args.includes('--quiet');
|
|
135
|
+
const problems: string[] = [];
|
|
136
|
+
let scanned = 0;
|
|
137
|
+
|
|
138
|
+
if (!fs.existsSync(FIXTURE_DIR)) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`❌ check_evaluator_schema: fixture directory absent (${FIXTURE_DIR}) — the gate cannot run\n`,
|
|
141
|
+
);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Every (evaluator x flavour) pair is PLANNED before the loop, so a pair
|
|
146
|
+
// that falls through without a terminal outcome is a finalize() throw rather
|
|
147
|
+
// than a silently smaller denominator. A missing fixture is a real skip with
|
|
148
|
+
// a named reason, never an invisible `continue`.
|
|
149
|
+
const ledger = new GateLedger('check_evaluator_schema');
|
|
150
|
+
const flavours = Object.keys(EXPECTED);
|
|
151
|
+
ledger.plan(EVALUATORS.flatMap((a) => flavours.map((f) => `${a.name}.${f}`)));
|
|
152
|
+
|
|
153
|
+
for (const adapter of EVALUATORS) {
|
|
154
|
+
for (const flavour of flavours) {
|
|
155
|
+
const pair = `${adapter.name}.${flavour}`;
|
|
156
|
+
const raw = readRun(adapter.name, flavour);
|
|
157
|
+
if (raw === null) {
|
|
158
|
+
ledger.skip(pair, 'no_applicable_files');
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
scanned += 1;
|
|
162
|
+
|
|
163
|
+
let out;
|
|
164
|
+
try {
|
|
165
|
+
out = adapter.parse(raw);
|
|
166
|
+
} catch (exc) {
|
|
167
|
+
problems.push(`${pair}: adapter threw — ${String(exc)}`);
|
|
168
|
+
ledger.fail(pair, 'adapter threw');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const before = problems.length;
|
|
172
|
+
|
|
173
|
+
for (const v of validateEvaluatorOutput(out)) {
|
|
174
|
+
problems.push(`${pair}: schema — ${v}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const want = EXPECTED[flavour]!;
|
|
178
|
+
if (out.pass !== want.pass) {
|
|
179
|
+
problems.push(`${pair}: pass is ${out.pass}, fixture asserts ${want.pass}`);
|
|
180
|
+
}
|
|
181
|
+
if (out.metric_state !== want.metric_state) {
|
|
182
|
+
problems.push(
|
|
183
|
+
`${pair}: metric_state is ${String(out.metric_state)}, fixture asserts ${want.metric_state}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
// The higher-is-better invariant is the one a consumer relies on
|
|
187
|
+
// without checking, so it is asserted rather than documented.
|
|
188
|
+
if (out.metric_state === 'present' && out.direction === 'minimize') {
|
|
189
|
+
if (out.score !== -(out.metric ?? NaN)) {
|
|
190
|
+
problems.push(
|
|
191
|
+
`${pair}: score ${out.score} is not the negation of metric ${String(out.metric)}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (problems.length > before) {
|
|
197
|
+
ledger.fail(pair, `${String(problems.length - before)} violation(s)`);
|
|
198
|
+
} else {
|
|
199
|
+
ledger.complete(pair);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ledger.finalize();
|
|
205
|
+
|
|
206
|
+
if (scanned === 0) {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
`❌ check_evaluator_schema: 0 evaluator/fixture pairs found — a gate that scanned nothing has not verified anything\n`,
|
|
209
|
+
);
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
process.stderr.write(`scanned: ${scanned}\n`);
|
|
214
|
+
|
|
215
|
+
if (problems.length > 0) {
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
`❌ check_evaluator_schema: ${problems.length} violation(s) across ${scanned} pair(s)\n\n`,
|
|
218
|
+
);
|
|
219
|
+
for (const p of problems) process.stdout.write(` ${p}\n`);
|
|
220
|
+
process.stdout.write(`\nContract: docs/contracts/evaluator-output.md\n`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!quiet) {
|
|
225
|
+
process.stdout.write(
|
|
226
|
+
`✅ check_evaluator_schema: ${scanned} evaluator/fixture pair(s) conform, ${EVALUATORS.length} evaluator(s) registered.\n`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _isCliEntry(): boolean {
|
|
233
|
+
if (process.argv[1] === undefined) return false;
|
|
234
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
235
|
+
try {
|
|
236
|
+
return (
|
|
237
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
238
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
239
|
+
);
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
246
|
+
process.exit(main());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { main };
|
|
@@ -50,6 +50,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
50
50
|
|
|
51
51
|
import * as yaml from 'js-yaml';
|
|
52
52
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
53
|
+
import { GateLedger, type GateSkipReason } from './_lib/gate_ledger.js';
|
|
53
54
|
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
54
55
|
import { listGateScripts } from './_lib/gate_population.js';
|
|
55
56
|
import { describeOutcome, namesEstateInvalidatingError } from './_lib/gate_result.js';
|
|
@@ -187,6 +188,59 @@ export function parse_scanned(output: string): number | null {
|
|
|
187
188
|
return m === null ? null : Number(m[1]);
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Map a gate verdict onto its ledger outcome.
|
|
193
|
+
*
|
|
194
|
+
* Extracted and exported so the mapping can be PINNED — it was inline, and the
|
|
195
|
+
* completion review's three sharpest findings were all mis-mappings that a
|
|
196
|
+
* discrimination test would have caught in one line each.
|
|
197
|
+
*
|
|
198
|
+
* The split is INSPECTED vs NOT INSPECTED, never pass vs fail. `fail` counts
|
|
199
|
+
* into the ledger's `scanned=` (`report()` sets `checked = completed + failed`),
|
|
200
|
+
* so a target that could not be READ must never land there — that inflates the
|
|
201
|
+
* exact number the library exists to make trustworthy. This file's own comment
|
|
202
|
+
* already says a gate that could not MEASURE "is not a gate that found
|
|
203
|
+
* violations, and it is not a gate that passed either"; this agrees with it.
|
|
204
|
+
*/
|
|
205
|
+
export function ledgerOutcomeFor(
|
|
206
|
+
verdict: GateResult['verdict'],
|
|
207
|
+
): 'complete' | 'fail' | GateSkipReason {
|
|
208
|
+
switch (verdict) {
|
|
209
|
+
case 'pending':
|
|
210
|
+
return 'disabled_by_configuration';
|
|
211
|
+
case 'unavailable':
|
|
212
|
+
// NOT `missing_credentials`. The only manifest row carrying
|
|
213
|
+
// `unavailable_exit` is unavailable for an unbuilt `site/dist`, and the
|
|
214
|
+
// skip sentence IS the audit surface — naming a credential would send the
|
|
215
|
+
// reader hunting an unset token instead of running the build.
|
|
216
|
+
return 'no_applicable_files';
|
|
217
|
+
case 'crashed':
|
|
218
|
+
// NOT `dead_scan_root` — a gate that threw has no dead root, and the skip
|
|
219
|
+
// sentence would send the reader to check a scan path that is fine. The
|
|
220
|
+
// vocabulary was extended rather than approximated, same reasoning as the
|
|
221
|
+
// `unavailable` case above.
|
|
222
|
+
return 'check_did_not_run';
|
|
223
|
+
case 'estate_invalid':
|
|
224
|
+
// Nothing was read here either. `crashed` still reds the build through
|
|
225
|
+
// this gate's own filter and `estate_invalid` deliberately does not, but
|
|
226
|
+
// neither belongs in the inspected count.
|
|
227
|
+
return 'dead_scan_root';
|
|
228
|
+
case 'silent':
|
|
229
|
+
case 'below_floor':
|
|
230
|
+
return 'fail';
|
|
231
|
+
case 'ok':
|
|
232
|
+
return 'complete';
|
|
233
|
+
default: {
|
|
234
|
+
// Exhaustiveness guard. A `Verdict` member added later must be classified
|
|
235
|
+
// deliberately: without this, a new member would silently count as
|
|
236
|
+
// INSPECTED and both the switch and its test would stay green —
|
|
237
|
+
// re-creating the over-report this function was extracted to pin.
|
|
238
|
+
const unhandled: never = verdict;
|
|
239
|
+
throw new Error(`ledgerOutcomeFor: unhandled verdict ${String(unhandled)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
190
244
|
/** Classify one gate's run against its declared floor. Pure — testable without
|
|
191
245
|
* spawning anything. */
|
|
192
246
|
export function classify(
|
|
@@ -323,6 +377,31 @@ export function main(argv: readonly string[]): number {
|
|
|
323
377
|
return 0;
|
|
324
378
|
}
|
|
325
379
|
|
|
380
|
+
// Per-target accounting over the manifest rows. The gate that audits gate
|
|
381
|
+
// coverage was itself unaudited.
|
|
382
|
+
//
|
|
383
|
+
// Stated precisely, because the obvious justification overstates it: this gate
|
|
384
|
+
// ALREADY prints counted `pending N · unavailable N` warnings, so those two
|
|
385
|
+
// classes were not invisible. What the ledger adds is that they are expressed
|
|
386
|
+
// in the estate-wide vocabulary every other adopter uses, so a reader
|
|
387
|
+
// comparing gates does not have to learn this one's private status names — and
|
|
388
|
+
// that `crashed` and `estate_invalid`, which had no counted line at all, stop
|
|
389
|
+
// being counted as inspected.
|
|
390
|
+
//
|
|
391
|
+
// Note the two numbers this produces in one run: the ledger's `scanned=` (what
|
|
392
|
+
// was inspected) and the contract line `scanned:` (manifest rows read). They
|
|
393
|
+
// differ by the skips and that is correct, but only the latter is what
|
|
394
|
+
// `SCANNED_RE` parses.
|
|
395
|
+
const ledger = new GateLedger('check_gate_coverage');
|
|
396
|
+
// Deduplicated on purpose. `load_manifest` does not check id uniqueness, and a
|
|
397
|
+
// copy-pasted row with a forgotten rename would otherwise make `plan()` throw
|
|
398
|
+
// an uncaught LedgerUsageError — surfacing a manifest mistake as a stack trace
|
|
399
|
+
// and Node's exit 1, which this file's own contract reads as "a gate is blind,
|
|
400
|
+
// collapsed, or silent" rather than as the exit 2 a malformed manifest earns.
|
|
401
|
+
// The duplicate still runs twice and still prints twice; only the accounting
|
|
402
|
+
// refuses to double-count it.
|
|
403
|
+
ledger.plan([...new Set(specs.map((s) => s.id))]);
|
|
404
|
+
|
|
326
405
|
const results = specs.map((s) => {
|
|
327
406
|
if (spec_is_pending(s)) {
|
|
328
407
|
const { scanned, crashed } = probe_pending(s);
|
|
@@ -332,6 +411,20 @@ export function main(argv: readonly string[]): number {
|
|
|
332
411
|
return classify(s, scanned, crashed, exit_code, output);
|
|
333
412
|
});
|
|
334
413
|
|
|
414
|
+
// `recorded` pairs with the deduplicated plan above: a duplicated manifest id
|
|
415
|
+
// must be accounted once, and recording it twice would raise the same
|
|
416
|
+
// LedgerUsageError the dedupe exists to avoid.
|
|
417
|
+
const recorded = new Set<string>();
|
|
418
|
+
for (const r of results) {
|
|
419
|
+
if (recorded.has(r.id)) continue;
|
|
420
|
+
recorded.add(r.id);
|
|
421
|
+
const outcome = ledgerOutcomeFor(r.verdict);
|
|
422
|
+
if (outcome === 'complete') ledger.complete(r.id);
|
|
423
|
+
else if (outcome === 'fail') ledger.fail(r.id, r.message);
|
|
424
|
+
else ledger.skip(r.id, outcome);
|
|
425
|
+
}
|
|
426
|
+
ledger.report();
|
|
427
|
+
|
|
335
428
|
if (wantJson) {
|
|
336
429
|
process.stdout.write(`${JSON.stringify({ generated_by: 'check_gate_coverage', results }, null, 2)}\n`);
|
|
337
430
|
} else {
|