@event4u/agent-config 12.1.0 → 14.0.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- 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-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- 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 +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -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/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- 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-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- 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 +7 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.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/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- 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 +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- 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/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- 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 +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- 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 +101 -164
- 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/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -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/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -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/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- 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/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -75,19 +75,38 @@ import {
|
|
|
75
75
|
OBSERVATION_LOG,
|
|
76
76
|
analyzeSelector,
|
|
77
77
|
buildHostEventRecord,
|
|
78
|
+
buildNoTruncationRecord,
|
|
78
79
|
buildObservationRecord,
|
|
80
|
+
classifyHostProjection,
|
|
79
81
|
formatPerHostVerdicts,
|
|
82
|
+
formatProjectionModes,
|
|
80
83
|
formatReport,
|
|
81
84
|
knownHostLimits,
|
|
82
85
|
measureCatalogueVolume,
|
|
83
86
|
parseHostBudgetEvent,
|
|
84
87
|
readObservationLog,
|
|
85
88
|
readProjectedCatalogue,
|
|
89
|
+
resolveSkillsRoot,
|
|
90
|
+
type HostProjectionRow,
|
|
91
|
+
type ProjectionMode,
|
|
92
|
+
type ProjectionModeCounts,
|
|
86
93
|
type SelectorReport,
|
|
87
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';
|
|
102
|
+
import { scoped_projection_stats } from './_lib/scoped_projection.js';
|
|
103
|
+
import { iter_skills } from './update_counts.js';
|
|
88
104
|
|
|
89
|
-
// 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.
|
|
90
108
|
export * from './_lib/skill_catalogue.js';
|
|
109
|
+
export * from './_lib/skill_catalogue_series.js';
|
|
91
110
|
|
|
92
111
|
const _HERE = fileURLToPath(import.meta.url);
|
|
93
112
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -114,11 +133,259 @@ function argValue(flag: string): string | null {
|
|
|
114
133
|
return process.argv[index + 1]!;
|
|
115
134
|
}
|
|
116
135
|
|
|
136
|
+
/** Every occurrence of a repeatable flag, in argv order. */
|
|
137
|
+
function argValues(flag: string): string[] {
|
|
138
|
+
const out: string[] = [];
|
|
139
|
+
for (let i = 0; i < process.argv.length - 1; i += 1) {
|
|
140
|
+
if (process.argv[i] === flag) out.push(process.argv[i + 1]!);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `--projection-mode <scoped|legacy-all>`, validated against the closed enum.
|
|
147
|
+
*
|
|
148
|
+
* Returns `undefined` when the flag is absent — which the record schema reads
|
|
149
|
+
* as "not captured", never as `legacy-all`. An unrecognised value throws
|
|
150
|
+
* rather than falling back: a typo silently recorded as the wrong mode is the
|
|
151
|
+
* failure this whole module refuses elsewhere.
|
|
152
|
+
*/
|
|
153
|
+
function argProjectionMode(): ProjectionMode | undefined {
|
|
154
|
+
const raw = argValue('--projection-mode');
|
|
155
|
+
if (raw === null) return undefined;
|
|
156
|
+
if (raw === 'scoped' || raw === 'legacy-all') return raw;
|
|
157
|
+
throw new Error(`--projection-mode must be scoped or legacy-all, got: ${raw}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
117
160
|
/** `~` expanded against the current user's home. */
|
|
118
161
|
function expandHome(p: string): string {
|
|
119
162
|
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
120
163
|
}
|
|
121
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
|
+
|
|
122
389
|
/**
|
|
123
390
|
* `--limits` — publish the per-host picture: what each host was measured to
|
|
124
391
|
* deliver, and whether the observed hosts truncate the same way.
|
|
@@ -151,6 +418,52 @@ function runLimitsMode(): number {
|
|
|
151
418
|
return 0;
|
|
152
419
|
}
|
|
153
420
|
|
|
421
|
+
/**
|
|
422
|
+
* `--projection-modes` — the two modes side by side, as a MEASUREMENT.
|
|
423
|
+
*
|
|
424
|
+
* It reads no setting, writes no setting, and recommends no default. The
|
|
425
|
+
* scoped-projection default and its migration notice are owned by a different
|
|
426
|
+
* roadmap and were decided on 2026-08-15; this mode exists so the size of the
|
|
427
|
+
* difference is a published number instead of an intuition, and it deliberately
|
|
428
|
+
* offers no flag that would change anything.
|
|
429
|
+
*
|
|
430
|
+
* `--host-root` is repeatable, so several installed hosts are read against the
|
|
431
|
+
* same pair of numbers in one run. Each host is reported on its own row: no
|
|
432
|
+
* host's count is ever used to say anything about another's.
|
|
433
|
+
*/
|
|
434
|
+
/** The package's two projection-mode counts, one walk. Shared by both modes. */
|
|
435
|
+
function projectionModeCounts(): ProjectionModeCounts {
|
|
436
|
+
const stats = scoped_projection_stats(REPO, iter_skills());
|
|
437
|
+
return {
|
|
438
|
+
scoped: stats.projected,
|
|
439
|
+
legacyAll: stats.total,
|
|
440
|
+
prunedUnderScoped: stats.pruned,
|
|
441
|
+
activePacks: stats.active_packs,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function runProjectionModesMode(): number {
|
|
446
|
+
const counts = projectionModeCounts();
|
|
447
|
+
|
|
448
|
+
const rows: HostProjectionRow[] = [];
|
|
449
|
+
for (const rootArg of argValues('--host-root')) {
|
|
450
|
+
const root = expandHome(rootArg);
|
|
451
|
+
if (!fs.existsSync(root)) {
|
|
452
|
+
process.stderr.write(`❌ host root does not exist: ${root}\n`);
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
const host = path.basename(root).replace(/^\./, '');
|
|
456
|
+
rows.push(classifyHostProjection(measureCatalogueVolume(host, root), counts));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (process.argv.includes('--json')) {
|
|
460
|
+
process.stdout.write(`${JSON.stringify({ counts, hosts: rows }, null, 2)}\n`);
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
process.stdout.write(`${formatProjectionModes(counts, rows)}\n`);
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
154
467
|
/** `--volume <host-root>` — the projection half for one host, byte-accurate. */
|
|
155
468
|
function runVolumeMode(rootArg: string): number {
|
|
156
469
|
const root = expandHome(rootArg);
|
|
@@ -213,8 +526,32 @@ function runHostEventMode(source: string): number {
|
|
|
213
526
|
return 1;
|
|
214
527
|
}
|
|
215
528
|
process.stdout.write(
|
|
216
|
-
`host: ${host}\noffered: ${volume.artefacts}
|
|
529
|
+
`host: ${host}\noffered: ${volume.artefacts} (${volume.skillEntries} skills + ${volume.commandEntries} commands)\n` +
|
|
530
|
+
`dropped: 0 (asserted — no budget event in the stream)\n` +
|
|
531
|
+
'descriptions stripped: no — the host published nothing to strip.\n',
|
|
217
532
|
);
|
|
533
|
+
if (process.argv.includes('--record')) {
|
|
534
|
+
const stampedAt = argValue('--observed-at');
|
|
535
|
+
if (!stampedAt) {
|
|
536
|
+
process.stderr.write('❌ --record requires --observed-at <ISO date>\n');
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
540
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
541
|
+
fs.appendFileSync(
|
|
542
|
+
logPath,
|
|
543
|
+
`${JSON.stringify(
|
|
544
|
+
buildNoTruncationRecord(
|
|
545
|
+
host,
|
|
546
|
+
stampedAt,
|
|
547
|
+
volume.artefacts,
|
|
548
|
+
volume.skillEntries,
|
|
549
|
+
argProjectionMode(),
|
|
550
|
+
),
|
|
551
|
+
)}\n`,
|
|
552
|
+
);
|
|
553
|
+
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
554
|
+
}
|
|
218
555
|
return 0;
|
|
219
556
|
}
|
|
220
557
|
|
|
@@ -238,7 +575,14 @@ function runHostEventMode(source: string): number {
|
|
|
238
575
|
}
|
|
239
576
|
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
240
577
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
241
|
-
const record = buildHostEventRecord(
|
|
578
|
+
const record = buildHostEventRecord(
|
|
579
|
+
host,
|
|
580
|
+
stampedAt,
|
|
581
|
+
volume.artefacts,
|
|
582
|
+
event,
|
|
583
|
+
volume.skillEntries,
|
|
584
|
+
argProjectionMode(),
|
|
585
|
+
);
|
|
242
586
|
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
243
587
|
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
244
588
|
}
|
|
@@ -246,7 +590,10 @@ function runHostEventMode(source: string): number {
|
|
|
246
590
|
}
|
|
247
591
|
|
|
248
592
|
function main(): number {
|
|
593
|
+
if (process.argv.includes('--cadence')) return runCadenceMode();
|
|
594
|
+
if (process.argv.includes('--pointable-bare')) return runPointableBareMode();
|
|
249
595
|
if (process.argv.includes('--limits')) return runLimitsMode();
|
|
596
|
+
if (process.argv.includes('--projection-modes')) return runProjectionModesMode();
|
|
250
597
|
|
|
251
598
|
const volumeRoot = argValue('--volume');
|
|
252
599
|
if (volumeRoot) return runVolumeMode(volumeRoot);
|
|
@@ -320,9 +667,27 @@ function main(): number {
|
|
|
320
667
|
}
|
|
321
668
|
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
322
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
|
+
}
|
|
323
685
|
fs.appendFileSync(
|
|
324
686
|
logPath,
|
|
325
|
-
|
|
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`,
|
|
326
691
|
);
|
|
327
692
|
process.stdout.write(`\nrecorded → ${OBSERVATION_LOG}\n`);
|
|
328
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 };
|