@event4u/agent-config 14.7.0 → 14.9.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1358 -805
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +134 -21
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Stub-inventory-table guard.
|
|
4
|
+
*
|
|
5
|
+
* `agents/roadmaps/stubs/README.md` carried a hand-maintained index of the stub
|
|
6
|
+
* files sitting beside it: two tables, 6 demand-gated rows and 27 transfer rows,
|
|
7
|
+
* appended by hand by every roadmap-transfer session. It was the largest
|
|
8
|
+
* *authored* merge-conflict path in the repository — everything above it in the
|
|
9
|
+
* ranking is generated — and every row duplicated facts already in the stub it
|
|
10
|
+
* pointed at. `3793855b3` deleted it on 2026-08-21, after the AI council chose
|
|
11
|
+
* deletion over building a generator (both seats) and after all 33 rows were
|
|
12
|
+
* checked cell by cell against their stub file.
|
|
13
|
+
*
|
|
14
|
+
* It came back on 2026-08-22. A merge (`28ba2f592`, PR #1505) reintroduced a
|
|
15
|
+
* 12-row version; both parents of that merge lacked the table, so it was not a
|
|
16
|
+
* mis-resolved `modify/delete` but a deliberate restore — taken against a note
|
|
17
|
+
* in `agents/evidence/notes/drain-run-handoff.md` written in the same change as
|
|
18
|
+
* the deletion for the express purpose of preventing one. Three further rows
|
|
19
|
+
* were then appended by later sessions.
|
|
20
|
+
*
|
|
21
|
+
* That is the whole reason this gate exists rather than another paragraph: the
|
|
22
|
+
* paragraph was tried, and it lost to a merge. Prose cannot refuse.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT REJECTS — the structure, never "tables"
|
|
25
|
+
* ----------------------------------------------
|
|
26
|
+
* Two signatures, both keyed to an inventory of the guarded file's own
|
|
27
|
+
* directory:
|
|
28
|
+
*
|
|
29
|
+
* 1. a table header row whose first cell is `Stub` — the header both
|
|
30
|
+
* historical tables used (`| Stub | Transferred from | … |` and
|
|
31
|
+
* `| Stub | Triggers org-mode surface | Gates |`);
|
|
32
|
+
* 2. a table body row whose FIRST cell links to a `.md` file in the SAME
|
|
33
|
+
* directory — an inventory row by construction.
|
|
34
|
+
*
|
|
35
|
+
* A `../`-prefixed or otherwise pathed link does not match: a prose table that
|
|
36
|
+
* cites parent roadmaps is not an index of this directory. A future two-column
|
|
37
|
+
* glossary, a decision matrix, or any table that does not enumerate the
|
|
38
|
+
* neighbouring stubs is unaffected. This narrowness is deliberate and was asked
|
|
39
|
+
* for by review: a blanket ban on markdown tables in the file would block
|
|
40
|
+
* legitimate documentation to stop one specific regression.
|
|
41
|
+
*
|
|
42
|
+
* WHY A WATCH LIST AND NOT A TREE WALK
|
|
43
|
+
* ------------------------------------
|
|
44
|
+
* The guarded set is one known file, so there is no corpus to count, and a
|
|
45
|
+
* one-path guard is exactly the shape that rots into a permanent pass when the
|
|
46
|
+
* path moves — `check_safety_floor_untouched` announced "4 rules guarded" while
|
|
47
|
+
* guarding none for months. `assertWatchlistResolves` refuses to report clean when its
|
|
48
|
+
* targets do not resolve, so a rename reds this gate instead of retiring it
|
|
49
|
+
* silently.
|
|
50
|
+
*
|
|
51
|
+
* Exit codes: 0 clean · 1 findings or a dead watch list.
|
|
52
|
+
*/
|
|
53
|
+
// ledger-exempt: the scope is ONE watch-list file, not a collected population — there is
|
|
54
|
+
// no per-target accounting to publish, and the empty path is already refused by
|
|
55
|
+
// assertWatchlistResolves rather than skipped. Same shape as check_branch_freshness.
|
|
56
|
+
|
|
57
|
+
import * as fs from 'node:fs';
|
|
58
|
+
import * as os from 'node:os';
|
|
59
|
+
import * as path from 'node:path';
|
|
60
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
61
|
+
|
|
62
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
63
|
+
import { assertWatchlistResolves, DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
64
|
+
|
|
65
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
66
|
+
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Self-test floors. Below these, `--self-test` fails instead of printing success —
|
|
70
|
+
* deleting cases would otherwise be the cheapest route to a green self-test.
|
|
71
|
+
*/
|
|
72
|
+
const SELF_TEST_MIN_CASES = 5;
|
|
73
|
+
const SELF_TEST_MIN_REJECT = 3;
|
|
74
|
+
|
|
75
|
+
/** This script's repo-relative path, for the self-test's CLI invocations. */
|
|
76
|
+
const SELF = 'src/scripts/check_no_stub_inventory_table.ts';
|
|
77
|
+
|
|
78
|
+
/** Files whose inventory tables are banned. Repo-relative. */
|
|
79
|
+
export const GUARDED: readonly string[] = ['agents/roadmaps/stubs/README.md'];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A header row that opens with the literal `Stub` column.
|
|
83
|
+
*
|
|
84
|
+
* Both deleted tables used it, and it is not a phrase a prose table reaches for
|
|
85
|
+
* by accident: a first cell of exactly `Stub` announces a per-stub listing.
|
|
86
|
+
*/
|
|
87
|
+
const INVENTORY_HEADER = /^\|\s*Stub\s*\|/;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A body row whose first cell links to a `.md` file in the same directory.
|
|
91
|
+
*
|
|
92
|
+
* `[^)/]*` is the load-bearing part: it excludes `../road-to-x.md` and any other
|
|
93
|
+
* pathed target, so only a link to a sibling of the guarded file matches. That
|
|
94
|
+
* is what makes this an inventory signature rather than a link count.
|
|
95
|
+
*/
|
|
96
|
+
const INVENTORY_ROW = /^\|\s*\[`?[^\]]*`?\]\([^)/]*\.md\)/;
|
|
97
|
+
|
|
98
|
+
export interface Finding {
|
|
99
|
+
readonly file: string;
|
|
100
|
+
readonly line: number;
|
|
101
|
+
readonly kind: 'inventory-header' | 'inventory-row';
|
|
102
|
+
readonly text: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function scan_file(rel: string, text: string): Finding[] {
|
|
106
|
+
const findings: Finding[] = [];
|
|
107
|
+
const lines = text.split('\n');
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i]!;
|
|
110
|
+
if (INVENTORY_HEADER.test(line)) {
|
|
111
|
+
findings.push({ file: rel, line: i + 1, kind: 'inventory-header', text: line.trim() });
|
|
112
|
+
} else if (INVENTORY_ROW.test(line)) {
|
|
113
|
+
findings.push({ file: rel, line: i + 1, kind: 'inventory-row', text: line.trim() });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read `--root <path>` / `--root=<path>` out of argv.
|
|
121
|
+
*
|
|
122
|
+
* Parsed by NAME, never by position. `lint_handoffs` took `args[0]` as its scan
|
|
123
|
+
* root, and the Taskfile injects `--quiet` first — so the flag became the root,
|
|
124
|
+
* the scan found nothing, and the gate was red under the argv CI runs while
|
|
125
|
+
* green under a bare probe. A named flag cannot make that mistake.
|
|
126
|
+
*/
|
|
127
|
+
export function parseRoot(argv: readonly string[], fallback: string): string {
|
|
128
|
+
for (let i = 0; i < argv.length; i++) {
|
|
129
|
+
const a = argv[i]!;
|
|
130
|
+
if (a.startsWith('--root=')) return path.resolve(a.slice('--root='.length));
|
|
131
|
+
if (a === '--root' && argv[i + 1] !== undefined) return path.resolve(argv[i + 1]!);
|
|
132
|
+
}
|
|
133
|
+
return fallback;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function main(argv: readonly string[] = process.argv.slice(2), root?: string): number {
|
|
137
|
+
const QUIET = argv.includes('--quiet');
|
|
138
|
+
root = root ?? parseRoot(argv, ROOT);
|
|
139
|
+
let present: readonly string[];
|
|
140
|
+
try {
|
|
141
|
+
present = assertWatchlistResolves({
|
|
142
|
+
gate: 'check_no_stub_inventory_table',
|
|
143
|
+
candidates: GUARDED,
|
|
144
|
+
repoRoot: root,
|
|
145
|
+
});
|
|
146
|
+
} catch (exc) {
|
|
147
|
+
if (exc instanceof DeadScopeError) {
|
|
148
|
+
process.stdout.write(`❌ ${exc.message}\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
throw exc;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const findings: Finding[] = [];
|
|
155
|
+
for (const rel of present) {
|
|
156
|
+
findings.push(...scan_file(rel, fs.readFileSync(path.join(root, rel), 'utf-8')));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Outside the --quiet guard on purpose: CI passes --quiet, and a gate whose
|
|
160
|
+
// count disappears under the real argv reads as `silent` to the coverage guard.
|
|
161
|
+
reportScanned({
|
|
162
|
+
gate: 'check_no_stub_inventory_table',
|
|
163
|
+
scanned: present.length,
|
|
164
|
+
units: 'guarded file(s)',
|
|
165
|
+
roots: GUARDED,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (findings.length === 0) {
|
|
169
|
+
if (!QUIET) process.stdout.write('✅ No stub-inventory table in the guarded file(s).\n');
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
`❌ Stub-inventory table reintroduced — ${String(findings.length)} row(s):\n`,
|
|
175
|
+
);
|
|
176
|
+
for (const f of findings) {
|
|
177
|
+
process.stdout.write(` ${f.file}:${String(f.line)}: ${f.kind} — ${f.text.slice(0, 100)}\n`);
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
'\nThe stub files ARE the inventory; `ls agents/roadmaps/stubs/*.md` lists them.\n' +
|
|
181
|
+
'If you reached this while resolving a merge conflict, the resolution\n' +
|
|
182
|
+
'WITHOUT a table is the correct one — the table was deleted deliberately\n' +
|
|
183
|
+
'(3793855b3, AI council 2026-08-21) and restored once by a merge already.\n' +
|
|
184
|
+
'If you need a per-stub fact, it is in that stub; all 33 rows were verified\n' +
|
|
185
|
+
'against their stub file before deletion and all 15 again before the second.\n',
|
|
186
|
+
);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build a fixture repository holding one guarded file with `body`, or none at all
|
|
192
|
+
* when `body` is null (the dead-watch-list case).
|
|
193
|
+
*/
|
|
194
|
+
function _fixture(body: string | null): string {
|
|
195
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stub-inv-self-'));
|
|
196
|
+
if (body !== null) {
|
|
197
|
+
const target = path.join(dir, GUARDED[0]!);
|
|
198
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
199
|
+
fs.writeFileSync(target, body);
|
|
200
|
+
}
|
|
201
|
+
return dir;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Prove the gate's rejections still fire, through the CLI a contributor runs.
|
|
206
|
+
*
|
|
207
|
+
* The three rejecting cases are the sabotage probes this gate was verified with
|
|
208
|
+
* by hand before it landed; keeping them here is what stops the verification
|
|
209
|
+
* from being a one-time claim in a commit message.
|
|
210
|
+
*/
|
|
211
|
+
export function selfTest(): number {
|
|
212
|
+
const cases: SelfTestCase[] = [
|
|
213
|
+
{
|
|
214
|
+
name: 'rejects a restored transfer table (header + rows)',
|
|
215
|
+
expect: 'reject',
|
|
216
|
+
run: () =>
|
|
217
|
+
runGateCli(
|
|
218
|
+
ROOT,
|
|
219
|
+
SELF,
|
|
220
|
+
['--quiet', '--root', _fixture(
|
|
221
|
+
'| Stub | Transferred from |\n|---|---|\n' +
|
|
222
|
+
'| [`road-to-a.md`](road-to-a.md) | p |\n',
|
|
223
|
+
)],
|
|
224
|
+
ROOT,
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'rejects the inventory header on its own',
|
|
229
|
+
expect: 'reject',
|
|
230
|
+
run: () => runGateCli(ROOT, SELF, ['--quiet', '--root', _fixture('| Stub | Gates |\n')], ROOT),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'rejects a dead watch list rather than reporting clean',
|
|
234
|
+
expect: 'reject',
|
|
235
|
+
run: () => runGateCli(ROOT, SELF, ['--quiet', '--root', _fixture(null)], ROOT),
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'accepts a guarded file with no inventory table',
|
|
239
|
+
expect: 'accept',
|
|
240
|
+
run: () =>
|
|
241
|
+
runGateCli(
|
|
242
|
+
ROOT,
|
|
243
|
+
SELF,
|
|
244
|
+
['--quiet', '--root', _fixture('# Stubs\n\nThe directory is the index.\n')],
|
|
245
|
+
ROOT,
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'accepts a glossary table and a table citing parent roadmaps',
|
|
250
|
+
expect: 'accept',
|
|
251
|
+
run: () =>
|
|
252
|
+
runGateCli(
|
|
253
|
+
ROOT,
|
|
254
|
+
SELF,
|
|
255
|
+
['--quiet', '--root', _fixture(
|
|
256
|
+
'| Term | Meaning |\n|---|---|\n| capability-gated | env missing |\n\n' +
|
|
257
|
+
'| Parent | Phase |\n|---|---|\n' +
|
|
258
|
+
'| [`road-to-x.md`](../archive/road-to-x.md) | 2.1 |\n',
|
|
259
|
+
)],
|
|
260
|
+
ROOT,
|
|
261
|
+
),
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
return runSelfTest({
|
|
265
|
+
gate: 'check_no_stub_inventory_table',
|
|
266
|
+
cases,
|
|
267
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
268
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _isCliEntry(): boolean {
|
|
273
|
+
if (process.argv[1] === undefined) return false;
|
|
274
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
275
|
+
if (import.meta.url === argvUrl) return true;
|
|
276
|
+
try {
|
|
277
|
+
return (
|
|
278
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
279
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
280
|
+
);
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
287
|
+
const argv = process.argv.slice(2);
|
|
288
|
+
if (argv.includes('--self-test') && process.env['GATE_SELF_TEST_CHILD'] !== '1') {
|
|
289
|
+
process.exit(selfTest());
|
|
290
|
+
}
|
|
291
|
+
process.exit(main(argv));
|
|
292
|
+
}
|
|
@@ -28,6 +28,7 @@ import * as path from 'node:path';
|
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
|
|
30
30
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
import { runSelfTest } from './_lib/gate_self_test.js';
|
|
31
32
|
|
|
32
33
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
33
34
|
const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
|
|
@@ -98,6 +99,126 @@ export function skillBytes(files: readonly PackFile[]): { perSkill: Record<strin
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/** Every violation, as human-readable lines. Empty array means green. */
|
|
102
|
+
/**
|
|
103
|
+
* Content classes the published payload is checked for.
|
|
104
|
+
*
|
|
105
|
+
* The gap this closes: before it, the tarball was measured by SIZE and by build
|
|
106
|
+
* correctness, and by nothing that read what the files ARE. A payload can be
|
|
107
|
+
* comfortably under budget and still ship compiled tests, an IDE directory, or
|
|
108
|
+
* a credential-shaped file.
|
|
109
|
+
*
|
|
110
|
+
* `limit: 0` is a hard class — nothing of that shape may ship. A positive limit
|
|
111
|
+
* is a RATCHET: the number was measured, not chosen, and it may only walk down.
|
|
112
|
+
* The distinction is in `measured_in`, which every entry carries, because the
|
|
113
|
+
* same `npm pack` reports a fifth of the source maps in an unbuilt worktree as
|
|
114
|
+
* in a built one. A threshold with no stated tree reads as drift the first time
|
|
115
|
+
* someone runs it from a fresh checkout.
|
|
116
|
+
*/
|
|
117
|
+
interface ContentClass {
|
|
118
|
+
id: string;
|
|
119
|
+
/** Matches a payload path. */
|
|
120
|
+
match: (p: string) => boolean;
|
|
121
|
+
/** 0 = must not ship at all. > 0 = shrink-only ratchet. */
|
|
122
|
+
limit: number;
|
|
123
|
+
/** Which tree the limit was measured in, and when. */
|
|
124
|
+
measured_in: string;
|
|
125
|
+
/**
|
|
126
|
+
* True when the class only exists in a BUILT tree.
|
|
127
|
+
*
|
|
128
|
+
* This is the difference between a check and a false pass. `npm pack
|
|
129
|
+
* --dry-run --ignore-scripts` on a clean checkout — which is the condition
|
|
130
|
+
* `pack-size-budget.json` declares for its own numbers — produces no
|
|
131
|
+
* `dist/cli/**` at all, so a source-map count of 22 sails under a limit of
|
|
132
|
+
* 120 while measuring a fifth of the payload. Reported NOT MEASURABLE
|
|
133
|
+
* instead, on the same principle as the dead-scope assertion above: a check
|
|
134
|
+
* that could not run is not a clean bill.
|
|
135
|
+
*/
|
|
136
|
+
requires_build: boolean;
|
|
137
|
+
why: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Was this payload produced from a built tree?
|
|
142
|
+
*
|
|
143
|
+
* Keyed on the payload itself rather than on the filesystem, so it describes
|
|
144
|
+
* the thing being judged instead of the machine judging it.
|
|
145
|
+
*/
|
|
146
|
+
function payloadIsBuilt(files: readonly PackFile[]): boolean {
|
|
147
|
+
return files.some((f) => f.path.startsWith('dist/cli/'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const CONTENT_CLASSES: readonly ContentClass[] = [
|
|
151
|
+
{
|
|
152
|
+
id: 'compiled-test-artefact',
|
|
153
|
+
requires_build: true,
|
|
154
|
+
match: (p) => /\.(test|spec)\.(js|js\.map|d\.ts)$/.test(p),
|
|
155
|
+
limit: 0,
|
|
156
|
+
measured_in: 'built main checkout, 2026-08-22: 8 `.test.js` + 8 `.test.js.map` were shipping before the `files[]` negations landed',
|
|
157
|
+
why: "a compiled test's output has no consumer-facing purpose; council 2026-08-22 decision (b') strips BOTH the JS and its map, because stripping only the maps left the compiled tests themselves in the tarball",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'credential-shaped',
|
|
161
|
+
requires_build: false,
|
|
162
|
+
match: (p) => /(^|\/)\.env(\.|$)|\.pem$|(^|\/)id_(rsa|ed25519)$|\.p12$|\.pfx$/.test(p),
|
|
163
|
+
limit: 0,
|
|
164
|
+
measured_in: 'built main checkout, 2026-08-22: 0 present',
|
|
165
|
+
why: 'a clean class with no check is indistinguishable from a class nobody looked at — and this one is worth a canary precisely because it is empty today',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'ide-metadata',
|
|
169
|
+
requires_build: false,
|
|
170
|
+
match: (p) => /(^|\/)\.(idea|vscode)\//.test(p),
|
|
171
|
+
limit: 0,
|
|
172
|
+
measured_in: 'built main checkout, 2026-08-22: 0 present',
|
|
173
|
+
why: 'same reason as the credential class: empty and unchecked is not the same as empty and verified',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'source-map',
|
|
177
|
+
requires_build: true,
|
|
178
|
+
match: (p) => p.endsWith('.js.map'),
|
|
179
|
+
limit: 120,
|
|
180
|
+
measured_in: 'built worktree, 2026-08-22, AFTER the compiled-test negations: 120 product maps (128 total minus 8 test maps)',
|
|
181
|
+
why: "a product source map is a consumer debugging affordance — proper line mapping and stepping over shipped JS — so council 2026-08-22 kept the 119/120 product maps and refused a blanket zero. Recorded as a PROVISIONAL measured ratchet, not an architectural constant: nothing here establishes that any consumer debugs the shipped JS, and nothing establishes they do not",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Count each content class in the payload and report over-limit classes.
|
|
187
|
+
*
|
|
188
|
+
* Returns one error string per violating class, with the offending paths — the
|
|
189
|
+
* count alone tells a reader a rule broke and not which file broke it.
|
|
190
|
+
*/
|
|
191
|
+
export function classifyPayload(files: readonly PackFile[]): string[] {
|
|
192
|
+
const out: string[] = [];
|
|
193
|
+
const built = payloadIsBuilt(files);
|
|
194
|
+
for (const c of CONTENT_CLASSES) {
|
|
195
|
+
if (c.requires_build && !built) continue;
|
|
196
|
+
const hits = files.filter((f) => c.match(f.path)).map((f) => f.path);
|
|
197
|
+
if (hits.length <= c.limit) continue;
|
|
198
|
+
const shown = hits.slice(0, 8);
|
|
199
|
+
out.push(
|
|
200
|
+
`content class \`${c.id}\`: ${String(hits.length)} entr${hits.length === 1 ? 'y' : 'ies'} ` +
|
|
201
|
+
`exceeds its limit of ${String(c.limit)} (measured in: ${c.measured_in}) — ` +
|
|
202
|
+
`${shown.join(', ')}${hits.length > shown.length ? `, +${String(hits.length - shown.length)} more` : ''}. ` +
|
|
203
|
+
`Why this class is checked: ${c.why}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Per-class counts, for the green-path report. */
|
|
210
|
+
export function payloadClassCounts(
|
|
211
|
+
files: readonly PackFile[],
|
|
212
|
+
): Array<{ id: string; count: number; limit: number; measurable: boolean }> {
|
|
213
|
+
const built = payloadIsBuilt(files);
|
|
214
|
+
return CONTENT_CLASSES.map((c) => ({
|
|
215
|
+
id: c.id,
|
|
216
|
+
count: files.filter((f) => c.match(f.path)).length,
|
|
217
|
+
limit: c.limit,
|
|
218
|
+
measurable: built || !c.requires_build,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
101
222
|
export function evaluate(budget: PackSizeBudget, pack: PackResult): string[] {
|
|
102
223
|
const errors: string[] = [];
|
|
103
224
|
|
|
@@ -163,7 +284,103 @@ function readPack(argv: readonly string[]): PackResult {
|
|
|
163
284
|
return parsePackJson(stdout);
|
|
164
285
|
}
|
|
165
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Per-class discrimination, proven rather than asserted.
|
|
289
|
+
*
|
|
290
|
+
* WHY THIS EXISTS AND THE CANARY DOES NOT COVER IT. `gate-coverage.yml` carries
|
|
291
|
+
* `canary?: CanarySpec` — exactly ONE recipe per gate id, not a list (see
|
|
292
|
+
* `check_gate_coverage.ts`'s entry shape). The roadmap step behind this asked
|
|
293
|
+
* for "one canary per class", which the register cannot express. Two of the four
|
|
294
|
+
* classes cannot be planted through it at all: `compiled-test-artefact` is
|
|
295
|
+
* excluded from the payload by the `files[]` negations that fixed it, so a plant
|
|
296
|
+
* never ships and never reds, and `source-map` needs 121 files in a BUILT tree.
|
|
297
|
+
*
|
|
298
|
+
* So the register gets the one canary it can hold — the credential-shaped plant,
|
|
299
|
+
* chosen because that class is empty today and an empty unchecked class is
|
|
300
|
+
* indistinguishable from one nobody looked at — and every class is proven red
|
|
301
|
+
* here, over a synthetic payload, deterministically and with no pack run.
|
|
302
|
+
*
|
|
303
|
+
* A test file was the alternative and is weaker: the non-adopter ratchet in
|
|
304
|
+
* `check_gate_coverage` requires a NEW registered gate to carry a `--self-test`
|
|
305
|
+
* or an exemption, and a self-test drives the gate's own decision function
|
|
306
|
+
* rather than a copy of it.
|
|
307
|
+
*/
|
|
308
|
+
function packFiles(...paths: readonly string[]): PackFile[] {
|
|
309
|
+
return paths.map((path) => ({ path, size: 1 }));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A payload that looks built, so `requires_build` classes do not abstain. */
|
|
313
|
+
function builtPayload(...paths: readonly string[]): PackFile[] {
|
|
314
|
+
return packFiles('dist/cli/agent-config.js', ...paths);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function selfTest(): number {
|
|
318
|
+
// NOT under `dist/cli/` — that prefix is exactly what `payloadIsBuilt`
|
|
319
|
+
// keys on, so maps placed there would make an "unbuilt" fixture built and
|
|
320
|
+
// the abstain case would test nothing. Found by the case failing, which is
|
|
321
|
+
// the self-test doing its job on its own fixtures.
|
|
322
|
+
const maps = (n: number): string[] =>
|
|
323
|
+
Array.from({ length: n }, (_, i) => `dist/hooks/chunk-${String(i)}.js.map`);
|
|
324
|
+
return runSelfTest({
|
|
325
|
+
gate: 'check_pack_size',
|
|
326
|
+
minCases: 9,
|
|
327
|
+
minRejectCases: 5,
|
|
328
|
+
cases: [
|
|
329
|
+
{
|
|
330
|
+
name: 'credential-shaped: a .pem in the payload is refused',
|
|
331
|
+
expect: 'reject',
|
|
332
|
+
run: () => (classifyPayload(packFiles('src/scripts/x.pem')).length > 0 ? 1 : 0),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'credential-shaped: a dotted .env form is refused',
|
|
336
|
+
expect: 'reject',
|
|
337
|
+
run: () => (classifyPayload(packFiles('src/config/.env.production')).length > 0 ? 1 : 0),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'ide-metadata: a .vscode entry is refused',
|
|
341
|
+
expect: 'reject',
|
|
342
|
+
run: () => (classifyPayload(packFiles('src/scripts/.vscode/settings.json')).length > 0 ? 1 : 0),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'compiled-test-artefact: a shipped .test.js is refused',
|
|
346
|
+
expect: 'reject',
|
|
347
|
+
run: () => (classifyPayload(builtPayload('dist/cli/a.test.js')).length > 0 ? 1 : 0),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'source-map: one over the measured ratchet is refused',
|
|
351
|
+
expect: 'reject',
|
|
352
|
+
run: () => (classifyPayload(builtPayload(...maps(121))).length > 0 ? 1 : 0),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: 'source-map: the ratchet boundary itself passes',
|
|
356
|
+
expect: 'accept',
|
|
357
|
+
run: () => (classifyPayload(builtPayload(...maps(120))).length > 0 ? 1 : 0),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'a plain payload passes — the classes are not firing on everything',
|
|
361
|
+
expect: 'accept',
|
|
362
|
+
run: () => (classifyPayload(packFiles('src/scripts/x.ts', 'README.md')).length > 0 ? 1 : 0),
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
// The false pass 1.2 closed. An unbuilt payload carries a fifth
|
|
366
|
+
// of the source maps, so a build-gated class must ABSTAIN there
|
|
367
|
+
// rather than report clean — and must not report a failure
|
|
368
|
+
// either, or every clean-checkout run would red.
|
|
369
|
+
name: 'unbuilt payload: build-gated classes abstain, not pass and not fail',
|
|
370
|
+
expect: 'accept',
|
|
371
|
+
run: () => (classifyPayload(packFiles(...maps(121))).length > 0 ? 1 : 0),
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'unbuilt payload: a build-INDEPENDENT class still fires',
|
|
375
|
+
expect: 'reject',
|
|
376
|
+
run: () => (classifyPayload(packFiles('src/scripts/id_rsa')).length > 0 ? 1 : 0),
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
166
382
|
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
383
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
167
384
|
let budget: PackSizeBudget;
|
|
168
385
|
let pack: PackResult;
|
|
169
386
|
try {
|
|
@@ -199,7 +416,14 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
199
416
|
throw err;
|
|
200
417
|
}
|
|
201
418
|
|
|
202
|
-
|
|
419
|
+
// The coverage register requires an enforced gate to report what it
|
|
420
|
+
// inspected, and it is the same reason `assertScanned` runs above: a verdict
|
|
421
|
+
// with no denominator cannot be told apart from a verdict over nothing.
|
|
422
|
+
// Printed on EVERY path, including the failing one, so a reader of a red run
|
|
423
|
+
// still knows the population it was red over.
|
|
424
|
+
process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
|
|
425
|
+
|
|
426
|
+
const errors = [...evaluate(budget, pack), ...classifyPayload(pack.files)];
|
|
203
427
|
const { perSkill, total } = skillBytes(pack.files);
|
|
204
428
|
if (argv.includes('--json')) {
|
|
205
429
|
process.stdout.write(
|
|
@@ -216,6 +440,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
216
440
|
`${Object.keys(perSkill).length} skills, largest share ` +
|
|
217
441
|
`${Math.max(...Object.values(perSkill).map((b) => (b / total) * 100)).toFixed(2)}%\n`,
|
|
218
442
|
);
|
|
443
|
+
// Printed on the GREEN path on purpose. A class that is clean and silent is
|
|
444
|
+
// indistinguishable from a class nobody checks, which is the whole reason
|
|
445
|
+
// two of these four are limit-0 over an empty set today.
|
|
446
|
+
for (const c of payloadClassCounts(pack.files)) {
|
|
447
|
+
const verdict = c.measurable
|
|
448
|
+
? `${String(c.count)} / ${c.limit === 0 ? 'must be 0' : `${String(c.limit)} max`}`
|
|
449
|
+
: 'NOT MEASURABLE — this payload has no dist/cli/**, so the tree is unbuilt and the class is out of view';
|
|
450
|
+
process.stdout.write(` content class ${c.id}: ${verdict}\n`);
|
|
451
|
+
}
|
|
219
452
|
return 0;
|
|
220
453
|
}
|
|
221
454
|
|