@event4u/agent-config 14.8.0 → 14.10.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 +214 -237
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/pr/merge.md +9 -5
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +36 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/skills/async-python-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/code-refactoring/SKILL.md +1 -1
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/discovery-interview/SKILL.md +1 -1
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/editorial-calendar/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -1
- 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-artifact-completeness/SKILL.md +1 -1
- package/dist/agent-src/skills/judge-injection-defense/SKILL.md +1 -1
- 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/laravel-validation/SKILL.md +1 -1
- package/dist/agent-src/skills/legal-intake-triage/SKILL.md +1 -1
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +1 -1
- package/dist/agent-src/skills/messaging-architecture/SKILL.md +1 -1
- package/dist/agent-src/skills/migration-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +1 -1
- package/dist/agent-src/skills/module-management/SKILL.md +1 -1
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +1 -1
- package/dist/agent-src/skills/onboarding-program/SKILL.md +1 -1
- package/dist/agent-src/skills/perf-feedback-craft/SKILL.md +1 -1
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +1 -1
- package/dist/agent-src/skills/receiving-code-review/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/scene-expander/SKILL.md +1 -1
- package/dist/agent-src/skills/secrets-management/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +8 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-MMSGVZKV.js → chunk-77GR3VD4.js} +65 -40
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-EEJUMCDZ.js → chunk-PLSG6UL5.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +89 -13
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +64 -42
- 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 +562 -204
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +808 -331
- 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/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/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/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/config/estate-count-budget.json +13 -744
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +267 -9
- package/src/config/gate-violation-baselines.json +72 -72
- package/src/config/preamble-payload-budget.json +71 -67
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +8 -1
- package/src/scripts/_dispatch.bash +7 -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/host_capability.ts +59 -5
- 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/regenerate_index.ts +14 -7
- 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/seating.ts +211 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +33 -6
- 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_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_passport_reconciliation.ts +274 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- 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/generate_capabilities_index.ts +41 -0
- package/src/scripts/generate_pack_manifests.ts +78 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +19 -1
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +120 -10
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- 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/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/schemas/pack.schema.json +62 -4
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/sync_pr_branch.ts +14 -7
- package/src/scripts/tighten_skill_descriptions.ts +264 -0
- 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-N7CDDGI3.js → chunk-CYH5SP6M.js} +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CI guard: every candidate row in an ADR evidence sweep carries a route AND a
|
|
4
|
+
* dated follow-up.
|
|
5
|
+
*
|
|
6
|
+
* `road-to-evidence-based-adr-governance` 3.4 asks for exactly this, and the
|
|
7
|
+
* step was once marked `[x]` on the adjudication pass it DID run rather than on
|
|
8
|
+
* the half its own `verify:` named — "zero candidate rows without both a route
|
|
9
|
+
* and a dated follow-up; a lint over sweep artifacts red on a dateless
|
|
10
|
+
* candidate row". Neither the columns nor the lint existed. That is the
|
|
11
|
+
* "prose lifecycle enforcement is satisfied by its weakest honest reading"
|
|
12
|
+
* defect the roadmap cites as its third motivating measurement, so the fix is a
|
|
13
|
+
* gate rather than another sentence.
|
|
14
|
+
*
|
|
15
|
+
* WHAT A CANDIDATE IS. A table row whose disposition cell carries `REVIEW-NOW`
|
|
16
|
+
* or `SUPERSEDE (candidate)` — the two dispositions that assert the record needs
|
|
17
|
+
* further action. Every other disposition in the vocabulary is terminal for the
|
|
18
|
+
* sweep (`KEEP`, `HISTORICAL-ONLY`, …) or names its own mechanism, and a
|
|
19
|
+
* follow-up requirement on those would be noise.
|
|
20
|
+
*
|
|
21
|
+
* WHY A DATE AND NOT A PROMISE. The three accepted follow-up forms are a linked
|
|
22
|
+
* roadmap step, an ADR draft, or an explicit dated defer on the ADR-134 pattern.
|
|
23
|
+
* All three are checkable only through the date: without one, a lapse is a
|
|
24
|
+
* silent extension instead of a compliance finding, which is the property the
|
|
25
|
+
* step was specified to buy.
|
|
26
|
+
*
|
|
27
|
+
* HONEST SCOPE. This gate checks that a route and a date are PRESENT and that
|
|
28
|
+
* the routing section covers every candidate. It cannot check that the route is
|
|
29
|
+
* the right venue or that the date is met — the first is a judgement and the
|
|
30
|
+
* second is a calendar. Read a green run as "every candidate is routed and
|
|
31
|
+
* dated", never as "every follow-up happened".
|
|
32
|
+
*
|
|
33
|
+
* Exit codes: 0 = clean, 1 = violations found, 2 = internal error.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* ./scripts-run src/scripts/lint_adr_sweep_routing
|
|
37
|
+
* ./scripts-run src/scripts/lint_adr_sweep_routing --json
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as os from 'node:os';
|
|
42
|
+
import * as path from 'node:path';
|
|
43
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
44
|
+
|
|
45
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
46
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
50
|
+
|
|
51
|
+
let SWEEP_DIR = path.join(REPO_ROOT, 'docs', 'decisions');
|
|
52
|
+
|
|
53
|
+
/** Test seam, mirroring the sibling roadmap lints. */
|
|
54
|
+
export function _setSweepDirForTest(p: string): void {
|
|
55
|
+
SWEEP_DIR = p;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Sweep artifacts: `adr-evidence-sweep-*.md` under docs/decisions/. */
|
|
59
|
+
const SWEEP_NAME = /^adr-evidence-sweep-.*\.md$/;
|
|
60
|
+
|
|
61
|
+
const CANDIDATE = /REVIEW-NOW|SUPERSEDE \(candidate\)/;
|
|
62
|
+
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/;
|
|
63
|
+
const ROUTING_HEADING = /^##+\s+Candidate routing\b/;
|
|
64
|
+
const NEXT_HEADING = /^##\s+/;
|
|
65
|
+
|
|
66
|
+
export interface Violation {
|
|
67
|
+
file: string;
|
|
68
|
+
line: number;
|
|
69
|
+
key: string;
|
|
70
|
+
msg: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The record identity a candidate row and its routing row must agree on.
|
|
75
|
+
*
|
|
76
|
+
* Sweep tables label the same record four ways — `208`, `ADR-239`,
|
|
77
|
+
* `ADR-016 installer-architecture`, `docs/adrs/router/0001 three-tier-routing`
|
|
78
|
+
* — so a literal string match would report every row as unrouted. Normalising
|
|
79
|
+
* to the numeric identity is what makes the two sections comparable at all.
|
|
80
|
+
*/
|
|
81
|
+
export function recordKey(cell: string): string | null {
|
|
82
|
+
const text = cell.replace(/[`*[\]]/g, '').trim();
|
|
83
|
+
const area = /^docs\/adrs\/([a-z-]+)\/0*(\d+)/.exec(text);
|
|
84
|
+
if (area) return `${area[1]}/${String(Number(area[2]))}`;
|
|
85
|
+
const numbered = /^(?:ADR-)?0*(\d{1,4})\b/.exec(text);
|
|
86
|
+
if (numbered) return `adr/${String(Number(numbered[1]))}`;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cells(line: string): string[] {
|
|
91
|
+
return line
|
|
92
|
+
.replace(/^\||\|$/g, '')
|
|
93
|
+
.split('|')
|
|
94
|
+
.map((c) => c.trim());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Is this a table body row (not a header or a `|---|` separator)? */
|
|
98
|
+
function isBodyRow(line: string): boolean {
|
|
99
|
+
return line.startsWith('|') && !/^\|[\s:-]+\|/.test(line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SweepScan {
|
|
103
|
+
/** key -> first line number the candidate was seen on */
|
|
104
|
+
candidates: Map<string, number>;
|
|
105
|
+
/** key -> { route, followUp, line } */
|
|
106
|
+
routed: Map<string, { route: string; followUp: string; line: number }>;
|
|
107
|
+
/** true when the artifact has a `## Candidate routing` section at all */
|
|
108
|
+
hasRoutingSection: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function scanSweep(text: string): SweepScan {
|
|
112
|
+
const lines = text.split('\n');
|
|
113
|
+
const candidates = new Map<string, number>();
|
|
114
|
+
const routed = new Map<string, { route: string; followUp: string; line: number }>();
|
|
115
|
+
let inRouting = false;
|
|
116
|
+
let hasRoutingSection = false;
|
|
117
|
+
let inFence = false;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
120
|
+
const line = lines[i] as string;
|
|
121
|
+
if (/^\s*```/.test(line)) {
|
|
122
|
+
inFence = !inFence;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (inFence) continue;
|
|
126
|
+
|
|
127
|
+
if (ROUTING_HEADING.test(line)) {
|
|
128
|
+
inRouting = true;
|
|
129
|
+
hasRoutingSection = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (inRouting && NEXT_HEADING.test(line) && !ROUTING_HEADING.test(line)) {
|
|
133
|
+
inRouting = false;
|
|
134
|
+
}
|
|
135
|
+
if (!isBodyRow(line)) continue;
|
|
136
|
+
|
|
137
|
+
const c = cells(line);
|
|
138
|
+
const key = recordKey(c[0] ?? '');
|
|
139
|
+
if (key === null) continue;
|
|
140
|
+
|
|
141
|
+
if (inRouting) {
|
|
142
|
+
// | Record | Disposition | Route | Dated follow-up |
|
|
143
|
+
routed.set(key, {
|
|
144
|
+
route: (c[2] ?? '').trim(),
|
|
145
|
+
followUp: (c[3] ?? '').trim(),
|
|
146
|
+
line: i + 1,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (CANDIDATE.test(line) && !candidates.has(key)) {
|
|
151
|
+
candidates.set(key, i + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { candidates, routed, hasRoutingSection };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function lintSweep(rel: string, text: string): [Violation[], number] {
|
|
158
|
+
const { candidates, routed, hasRoutingSection } = scanSweep(text);
|
|
159
|
+
const out: Violation[] = [];
|
|
160
|
+
if (candidates.size > 0 && !hasRoutingSection) {
|
|
161
|
+
out.push({
|
|
162
|
+
file: rel,
|
|
163
|
+
line: 1,
|
|
164
|
+
key: '—',
|
|
165
|
+
msg:
|
|
166
|
+
`${candidates.size} candidate row(s) and no \`## Candidate routing\` section. ` +
|
|
167
|
+
'Every REVIEW-NOW / SUPERSEDE (candidate) row needs a route and a dated follow-up ' +
|
|
168
|
+
'(road-to-evidence-based-adr-governance 3.4).',
|
|
169
|
+
});
|
|
170
|
+
return [out, candidates.size];
|
|
171
|
+
}
|
|
172
|
+
for (const [key, line] of [...candidates].sort((a, b) => a[1] - b[1])) {
|
|
173
|
+
const row = routed.get(key);
|
|
174
|
+
if (row === undefined) {
|
|
175
|
+
out.push({
|
|
176
|
+
file: rel,
|
|
177
|
+
line,
|
|
178
|
+
key,
|
|
179
|
+
msg: `candidate ${key} has no row in \`## Candidate routing\` — unrouted.`,
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (row.route === '' || row.route === '—') {
|
|
184
|
+
out.push({ file: rel, line: row.line, key, msg: `candidate ${key} has an empty route cell.` });
|
|
185
|
+
}
|
|
186
|
+
if (!ISO_DATE.test(row.followUp)) {
|
|
187
|
+
out.push({
|
|
188
|
+
file: rel,
|
|
189
|
+
line: row.line,
|
|
190
|
+
key,
|
|
191
|
+
msg:
|
|
192
|
+
`candidate ${key} carries no YYYY-MM-DD in its follow-up cell — a dateless ` +
|
|
193
|
+
'follow-up makes a lapse a silent extension rather than a compliance finding.',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [out, candidates.size];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Floors for `--self-test`, declared here so a truncation is a visible diff. */
|
|
201
|
+
const SELF_TEST_MIN_CASES = 6;
|
|
202
|
+
const SELF_TEST_MIN_REJECT = 4;
|
|
203
|
+
|
|
204
|
+
const SCRIPT_REL = 'src/scripts/lint_adr_sweep_routing.ts';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build a one-artifact sweep in a temp dir and run the REAL CLI against it.
|
|
208
|
+
*
|
|
209
|
+
* The candidate table and the routing table are supplied separately because
|
|
210
|
+
* every rejection this gate can make is a disagreement between the two.
|
|
211
|
+
*/
|
|
212
|
+
function _selfTestFixture(name: string, candidateRows: string, routingSection: string): number {
|
|
213
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `adr-sweep-${name}-`));
|
|
214
|
+
const docs = path.join(dir, 'docs', 'decisions');
|
|
215
|
+
fs.mkdirSync(docs, { recursive: true });
|
|
216
|
+
fs.writeFileSync(
|
|
217
|
+
path.join(docs, 'adr-evidence-sweep-fixture.md'),
|
|
218
|
+
`# fixture\n\n## Tranche dispositions\n\n| ADR | Disposition |\n|---|---|\n${candidateRows}\n${routingSection}\n`,
|
|
219
|
+
'utf8',
|
|
220
|
+
);
|
|
221
|
+
return runGateCli(REPO_ROOT, SCRIPT_REL, ['--dir', docs], dir);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const _GOOD_ROUTING =
|
|
225
|
+
'## Candidate routing\n\n| Record | Disposition | Route | Dated follow-up |\n|---|---|---|---|\n' +
|
|
226
|
+
'| ADR-016 | REVIEW-NOW | council | disposition by **2026-10-03** |\n';
|
|
227
|
+
|
|
228
|
+
function selfTest(): number {
|
|
229
|
+
const cases: SelfTestCase[] = [
|
|
230
|
+
{
|
|
231
|
+
name: 'a candidate with a route and a date is accepted',
|
|
232
|
+
expect: 'accept',
|
|
233
|
+
run: () => _selfTestFixture('ok', '| ADR-016 | REVIEW-NOW |', _GOOD_ROUTING),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'a candidate with no routing section at all is refused',
|
|
237
|
+
expect: 'reject',
|
|
238
|
+
run: () => _selfTestFixture('nosection', '| ADR-016 | REVIEW-NOW |', ''),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'a candidate absent from the routing table is refused',
|
|
242
|
+
expect: 'reject',
|
|
243
|
+
run: () =>
|
|
244
|
+
_selfTestFixture(
|
|
245
|
+
'unrouted',
|
|
246
|
+
'| ADR-016 | REVIEW-NOW |\n| ADR-044 | REVIEW-NOW |',
|
|
247
|
+
_GOOD_ROUTING,
|
|
248
|
+
),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'a dateless follow-up is refused',
|
|
252
|
+
expect: 'reject',
|
|
253
|
+
run: () =>
|
|
254
|
+
_selfTestFixture(
|
|
255
|
+
'dateless',
|
|
256
|
+
'| ADR-016 | REVIEW-NOW |',
|
|
257
|
+
'## Candidate routing\n\n| Record | Disposition | Route | Dated follow-up |\n|---|---|---|---|\n' +
|
|
258
|
+
'| ADR-016 | REVIEW-NOW | council | disposition soon |\n',
|
|
259
|
+
),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'an empty route cell is refused',
|
|
263
|
+
expect: 'reject',
|
|
264
|
+
run: () =>
|
|
265
|
+
_selfTestFixture(
|
|
266
|
+
'noroute',
|
|
267
|
+
'| ADR-016 | REVIEW-NOW |',
|
|
268
|
+
'## Candidate routing\n\n| Record | Disposition | Route | Dated follow-up |\n|---|---|---|---|\n' +
|
|
269
|
+
'| ADR-016 | REVIEW-NOW | — | disposition by **2026-10-03** |\n',
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
// The label-shape case. Sweep tables spell the same record four
|
|
274
|
+
// ways, so a literal match would report every row unrouted; this
|
|
275
|
+
// pins the normalisation rather than trusting it.
|
|
276
|
+
name: 'a bare number in the sweep matches an ADR-NNN routing row',
|
|
277
|
+
expect: 'accept',
|
|
278
|
+
run: () => _selfTestFixture('bare', '| 016 | SUPERSEDE (candidate) |', _GOOD_ROUTING),
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
return runSelfTest({
|
|
282
|
+
gate: 'lint_adr_sweep_routing',
|
|
283
|
+
cases,
|
|
284
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
285
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
290
|
+
const json = argv.includes('--json');
|
|
291
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
292
|
+
process.stdout.write('usage: lint_adr_sweep_routing [-h] [--json] [--dir DIR] [--self-test]\n');
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
296
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
297
|
+
const a = argv[i] as string;
|
|
298
|
+
if (a === '--json') continue;
|
|
299
|
+
if (a === '--dir') {
|
|
300
|
+
const next = argv[i + 1];
|
|
301
|
+
if (next === undefined) {
|
|
302
|
+
process.stderr.write('lint_adr_sweep_routing: --dir requires a path\n');
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
SWEEP_DIR = path.resolve(next);
|
|
306
|
+
i += 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
process.stderr.write(`lint_adr_sweep_routing: unrecognized argument: ${a}\n`);
|
|
310
|
+
return 2;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let names: string[] = [];
|
|
314
|
+
try {
|
|
315
|
+
names = fs.readdirSync(SWEEP_DIR).filter((n) => SWEEP_NAME.test(n)).sort();
|
|
316
|
+
} catch {
|
|
317
|
+
names = [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const violations: Violation[] = [];
|
|
321
|
+
let scanned = 0;
|
|
322
|
+
for (const name of names) {
|
|
323
|
+
const abs = path.join(SWEEP_DIR, name);
|
|
324
|
+
const rel = path.relative(REPO_ROOT, abs).split(path.sep).join('/');
|
|
325
|
+
const [v, n] = lintSweep(rel, fs.readFileSync(abs, 'utf-8'));
|
|
326
|
+
violations.push(...v);
|
|
327
|
+
scanned += n;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
process.stdout.write(`scanned: ${scanned}\n`);
|
|
331
|
+
try {
|
|
332
|
+
assertScanned({
|
|
333
|
+
gate: 'lint_adr_sweep_routing',
|
|
334
|
+
scanned,
|
|
335
|
+
units: 'candidate row(s)',
|
|
336
|
+
roots: ['docs/decisions'],
|
|
337
|
+
});
|
|
338
|
+
} catch (exc) {
|
|
339
|
+
if (exc instanceof DeadScopeError) {
|
|
340
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
throw exc;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (json) {
|
|
347
|
+
process.stdout.write(`${JSON.stringify({ violations }, null, 2)}\n`);
|
|
348
|
+
} else {
|
|
349
|
+
for (const v of violations) {
|
|
350
|
+
process.stderr.write(`❌ ${v.file}:${v.line}: ${v.msg}\n`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (violations.length > 0) {
|
|
354
|
+
process.stderr.write(`lint_adr_sweep_routing: ${violations.length} violation(s)\n`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
process.stdout.write(
|
|
358
|
+
`✅ lint_adr_sweep_routing: every candidate row across ${names.length} sweep artifact(s) carries a route and a dated follow-up\n`,
|
|
359
|
+
);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
364
|
+
process.exit(main());
|
|
365
|
+
}
|
|
@@ -17,8 +17,17 @@
|
|
|
17
17
|
* permissions: write-all;
|
|
18
18
|
* npm install / npm ci without --ignore-scripts in a
|
|
19
19
|
* pull_request_target workflow.
|
|
20
|
-
* MEDIUM —
|
|
21
|
-
*
|
|
20
|
+
* MEDIUM — actions pinned by mutable tag instead of full SHA;
|
|
21
|
+
* an actions/checkout step that keeps its credential.
|
|
22
|
+
*
|
|
23
|
+
* The MEDIUM tier's original wording read "third-party actions … (first-party
|
|
24
|
+
* actions/* are skipped)". Corrected 2026-08-22: the first-party exemption is
|
|
25
|
+
* GONE (see the removal note below), so the parenthetical documented an
|
|
26
|
+
* exemption that no longer exists — a false claim in a tracked artefact, which
|
|
27
|
+
* is the defect class the change that removed it was written to repair. The
|
|
28
|
+
* TIERS are untouched; only the description of what MEDIUM covers now matches
|
|
29
|
+
* the code. Re-tiering the locked model is a separate decision, recorded as
|
|
30
|
+
* unresolved in agents/evidence/analysis/workflow-security-net-degraded-decision.md.
|
|
22
31
|
*
|
|
23
32
|
* Script-injection detection (regex-based) is intentionally deferred — it
|
|
24
33
|
* requires an AST-aware pass to avoid false positives on quoted / escaped
|
|
@@ -53,8 +62,23 @@ function _setAllowlistPathForTest(p: string): void {
|
|
|
53
62
|
// Triggers that expose the repository token to untrusted pull-request context
|
|
54
63
|
const DANGEROUS_TRIGGERS: ReadonlySet<string> = new Set(['pull_request_target', 'workflow_run']);
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
|
|
65
|
+
// REMOVED 2026-08-22 (road-to-ci-supply-chain-integrity 1.4). This was
|
|
66
|
+
// `FIRST_PARTY_OWNERS = {'actions', 'github'}` and it exempted first-party
|
|
67
|
+
// owners from the mutable-tag rule. The premise — "mutable tags on these are
|
|
68
|
+
// acceptable" — is the one a supply-chain rule cannot hold: a first-party tag
|
|
69
|
+
// is still a mutable pointer, and `actions/checkout@v7` moving under the repo
|
|
70
|
+
// is the same class of event as any other owner's tag moving.
|
|
71
|
+
//
|
|
72
|
+
// What it cost, measured before removal: 100 of the 112 unpinned references in
|
|
73
|
+
// this tree were first-party, so the gate saw 12 of 112 defects and reported
|
|
74
|
+
// green on the rest. An exemption sized like that is not an exemption, it is
|
|
75
|
+
// the rule being off.
|
|
76
|
+
//
|
|
77
|
+
// The removal ships in the SAME change as the 112 pins, and in that order:
|
|
78
|
+
// pin first, then lift, so the transition is red-then-green inside one
|
|
79
|
+
// reviewable diff. Reverting the pins locally must make this gate exit
|
|
80
|
+
// non-zero — that is the assertion that the gate can still see the defect,
|
|
81
|
+
// which a green-only check cannot make.
|
|
58
82
|
|
|
59
83
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
60
84
|
type JsonObject = { [k: string]: JsonValue };
|
|
@@ -262,13 +286,33 @@ function _mutable_third_party_actions(doc: JsonObject, _workflow_name: string):
|
|
|
262
286
|
if (!uses || !uses.includes('@')) {
|
|
263
287
|
continue;
|
|
264
288
|
}
|
|
289
|
+
// `actions/checkout` leaves the repository token in the runner's git
|
|
290
|
+
// config for the rest of the job unless told not to. Measured before
|
|
291
|
+
// this rule existed: 0 of 50 checkouts in this tree set the flag, so
|
|
292
|
+
// every job carried a usable credential it did not need.
|
|
293
|
+
//
|
|
294
|
+
// `true` is a legitimate answer — two jobs here push — so the rule
|
|
295
|
+
// asks for the key to be PRESENT, not for a particular value. An
|
|
296
|
+
// explicit `true` is a reviewable decision; a missing key is an
|
|
297
|
+
// ambient one, and only the second is a finding.
|
|
298
|
+
if (uses.slice(0, uses.indexOf('@')).toLowerCase() === 'actions/checkout') {
|
|
299
|
+
const withObj = _asObject(stepObj['with'] as JsonValue);
|
|
300
|
+
const hasKey =
|
|
301
|
+
withObj !== null &&
|
|
302
|
+
Object.prototype.hasOwnProperty.call(withObj, 'persist-credentials');
|
|
303
|
+
if (!hasKey) {
|
|
304
|
+
findings.push({
|
|
305
|
+
severity: 'MEDIUM',
|
|
306
|
+
rule: 'persist-credentials',
|
|
307
|
+
detail:
|
|
308
|
+
'actions/checkout without an explicit persist-credentials — ' +
|
|
309
|
+
'the repository token stays in the runner git config for the rest of the job',
|
|
310
|
+
location: `job:${job_name}/step:${i + 1}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
265
314
|
const at = uses.indexOf('@');
|
|
266
|
-
const action_ref = uses.slice(0, at);
|
|
267
315
|
const pin = uses.slice(at + 1);
|
|
268
|
-
const owner = action_ref.includes('/') ? (action_ref.split('/')[0] as string) : action_ref;
|
|
269
|
-
if (FIRST_PARTY_OWNERS.has(owner.toLowerCase())) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
316
|
// A full SHA pin is 40 hex chars; anything else is mutable
|
|
273
317
|
if (pin.length === 40 && [...pin.toLowerCase()].every((c) => '0123456789abcdef'.includes(c))) {
|
|
274
318
|
continue;
|
|
@@ -650,7 +694,6 @@ export {
|
|
|
650
694
|
ALLOWLIST_PATH,
|
|
651
695
|
ALLOWLIST_CAP,
|
|
652
696
|
DANGEROUS_TRIGGERS,
|
|
653
|
-
FIRST_PARTY_OWNERS,
|
|
654
697
|
_setWorkflowsDirForTest,
|
|
655
698
|
_setAllowlistPathForTest,
|
|
656
699
|
load_allowlist,
|
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Legitimate workflow security findings. Hard-capped at 20 entries (lint_workflow_security.py ALLOWLIST_CAP). Exceeding the cap signals the linter is wrong, not the content
|
|
3
|
-
"findings": [
|
|
4
|
-
{
|
|
5
|
-
"workflow": "bench-drift.yml",
|
|
6
|
-
"rule": "mutable-action-tag",
|
|
7
|
-
"reason": "marocchino/sticky-pull-request-comment@v2 — sticky PR comment for bench drift reporting; no code execution, output-only action. SHA-pinning tracked in road-to-security-hardening P3."
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
"workflow": "skill-lint.yml",
|
|
11
|
-
"rule": "mutable-action-tag",
|
|
12
|
-
"reason": "marocchino/sticky-pull-request-comment@v2 — sticky PR comment for skill-lint reporting; same rationale as bench-drift.yml. SHA-pinning tracked in road-to-security-hardening P3."
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"workflow": "cloud-release.yml",
|
|
16
|
-
"rule": "mutable-action-tag",
|
|
17
|
-
"reason": "softprops/action-gh-release@v2 — creates GitHub releases; runs only on push to main (not on pull_request_target), no untrusted input. SHA-pinning tracked in road-to-security-hardening P3."
|
|
18
|
-
}
|
|
19
|
-
]
|
|
2
|
+
"_comment": "Legitimate workflow security findings. Hard-capped at 20 entries (lint_workflow_security.py ALLOWLIST_CAP). Exceeding the cap signals the linter is wrong, not the content \u2014 tighten the heuristic, do not grow this list (autonomous-execution allowlist-growth antipattern). Each entry MUST cite why the finding is acceptable in `reason`. EMPTIED 2026-08-22 (road-to-ci-supply-chain-integrity 0.2). It held three mutable-action-tag exemptions for marocchino/sticky-pull-request-comment and softprops/action-gh-release. All three are now pinned to full commit SHAs, so the findings they excused no longer exist and the entries matched nothing \u2014 lint_workflow_security reports \"0 allowlisted\". The roadmap step asked to REPAIR their `reason` text, each of which closed by naming an archived roadmap phase as the tracker for SHA-pinning; that phase is a git-discipline hook and its file mentions pinning nowhere, so the pointer dangled. Deleting a dead exemption is the honest repair \u2014 rewriting the reason on an entry that excuses nothing would have kept the allowlist looking load-bearing. The dangling pointer is also WHY the three survived: each one read as deferred rather than open. The full trace lives in the archived roadmap's completion note.",
|
|
3
|
+
"findings": []
|
|
20
4
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Publish `valid_envelope_rate` — three numbers, never one.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-subagent-envelope-adoption` step 1.2: a rate with no denominator is
|
|
6
|
+
* a claim, not a measurement. So every line carries the rate, the window
|
|
7
|
+
* bounds, the stop count, AND the path it read — because the ledger is
|
|
8
|
+
* gitignored and machine-local, so a figure with no path is not reproducible
|
|
9
|
+
* even in principle.
|
|
10
|
+
*
|
|
11
|
+
* Named `report_` and not `check_` deliberately: this publishes a number and
|
|
12
|
+
* gates nothing. `gate_population.matchesGatePattern` classifies on a
|
|
13
|
+
* `lint|check|audit|skill|verify_` prefix, and a reporting script wearing a gate
|
|
14
|
+
* prefix would be required to carry a coverage floor it has no business having.
|
|
15
|
+
*
|
|
16
|
+
* ## Rows this counts, and rows it refuses
|
|
17
|
+
*
|
|
18
|
+
* Only `subagent_stop` rows carrying a POST-SPLIT `envelope_parse` value. The
|
|
19
|
+
* retired `absent` value collapsed `no_message` and `no_envelope` into one
|
|
20
|
+
* bucket, so a rate computed across the boundary would mix two vocabularies and
|
|
21
|
+
* report a number that is not about either. Rows with `absent` are excluded and
|
|
22
|
+
* counted separately, so the exclusion is visible rather than silent.
|
|
23
|
+
*
|
|
24
|
+
* Exit 0 always: a report that fails is a gate, and this is not one.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
32
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
33
|
+
const LEDGER_DIR = path.join(REPO_ROOT, 'agents', 'runtime', 'state', 'subagent-ledger');
|
|
34
|
+
|
|
35
|
+
export interface RateReport {
|
|
36
|
+
ledger_paths: string[];
|
|
37
|
+
stops: number;
|
|
38
|
+
ok: number;
|
|
39
|
+
rate: number;
|
|
40
|
+
window_start: string | null;
|
|
41
|
+
window_end: string | null;
|
|
42
|
+
by_verdict: Record<string, number>;
|
|
43
|
+
excluded_absent: number;
|
|
44
|
+
by_agent_type: Record<string, number>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function computeRate(lines: readonly string[], paths: readonly string[]): RateReport {
|
|
48
|
+
const by: Record<string, number> = {};
|
|
49
|
+
const types: Record<string, number> = {};
|
|
50
|
+
let stops = 0;
|
|
51
|
+
let absent = 0;
|
|
52
|
+
let start: string | null = null;
|
|
53
|
+
let end: string | null = null;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
let r: Record<string, unknown>;
|
|
56
|
+
try {
|
|
57
|
+
r = JSON.parse(line) as Record<string, unknown>;
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (r['event'] !== 'subagent_stop') continue;
|
|
62
|
+
const verdict = r['envelope_parse'];
|
|
63
|
+
if (typeof verdict !== 'string') continue;
|
|
64
|
+
if (verdict === 'absent') {
|
|
65
|
+
absent += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
stops += 1;
|
|
69
|
+
by[verdict] = (by[verdict] ?? 0) + 1;
|
|
70
|
+
const at = typeof r['agent_type'] === 'string' ? r['agent_type'] : '(null)';
|
|
71
|
+
types[at] = (types[at] ?? 0) + 1;
|
|
72
|
+
const ts = r['ts'];
|
|
73
|
+
if (typeof ts === 'string') {
|
|
74
|
+
if (start === null || ts < start) start = ts;
|
|
75
|
+
if (end === null || ts > end) end = ts;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const ok = by['ok'] ?? 0;
|
|
79
|
+
return {
|
|
80
|
+
ledger_paths: [...paths],
|
|
81
|
+
stops,
|
|
82
|
+
ok,
|
|
83
|
+
// 0/0 is 0, not NaN: an empty ledger has a rate of zero valid envelopes
|
|
84
|
+
// and a denominator that says so on the same line.
|
|
85
|
+
rate: stops === 0 ? 0 : ok / stops,
|
|
86
|
+
window_start: start,
|
|
87
|
+
window_end: end,
|
|
88
|
+
by_verdict: by,
|
|
89
|
+
excluded_absent: absent,
|
|
90
|
+
by_agent_type: types,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readLedger(dir: string): { lines: string[]; paths: string[] } {
|
|
95
|
+
let names: string[] = [];
|
|
96
|
+
try {
|
|
97
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.jsonl')).sort();
|
|
98
|
+
} catch {
|
|
99
|
+
return { lines: [], paths: [] };
|
|
100
|
+
}
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
const paths: string[] = [];
|
|
103
|
+
for (const n of names) {
|
|
104
|
+
const abs = path.join(dir, n);
|
|
105
|
+
paths.push(path.relative(REPO_ROOT, abs).split(path.sep).join('/'));
|
|
106
|
+
lines.push(...fs.readFileSync(abs, 'utf-8').split('\n'));
|
|
107
|
+
}
|
|
108
|
+
return { lines, paths };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
112
|
+
// `--dir` so the report is reproducible against a ledger that is not this
|
|
113
|
+
// checkout's. The ledger is gitignored and machine-local, so without it the
|
|
114
|
+
// only readable ledger is the one beside the running copy — and a worktree
|
|
115
|
+
// has none, which would make the figure unobtainable exactly where the work
|
|
116
|
+
// happens.
|
|
117
|
+
const i = argv.indexOf('--dir');
|
|
118
|
+
const dir = i >= 0 && i + 1 < argv.length ? path.resolve(argv[i + 1] as string) : LEDGER_DIR;
|
|
119
|
+
const { lines, paths } = readLedger(dir);
|
|
120
|
+
const r = computeRate(lines, paths);
|
|
121
|
+
if (argv.includes('--json')) {
|
|
122
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (r.ledger_paths.length === 0) {
|
|
126
|
+
// Not a failure, and not a rate either. The ledger is gitignored, so a
|
|
127
|
+
// clean clone has none — reporting 0 % there would be a number about
|
|
128
|
+
// nothing.
|
|
129
|
+
process.stdout.write(
|
|
130
|
+
`valid_envelope_rate: NO LEDGER at ${dir} — ` +
|
|
131
|
+
'the ledger is gitignored and machine-local, so this is expected in a fresh ' +
|
|
132
|
+
'clone and is not a measurement of zero.\n',
|
|
133
|
+
);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
const pct = (r.rate * 100).toFixed(2);
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`valid_envelope_rate: ${pct}% — ${String(r.ok)} ok of ${String(r.stops)} stops, ` +
|
|
139
|
+
`window ${r.window_start ?? '?'} → ${r.window_end ?? '?'}, ` +
|
|
140
|
+
`read from ${r.ledger_paths.join(', ')}\n`,
|
|
141
|
+
);
|
|
142
|
+
const verdicts = Object.entries(r.by_verdict)
|
|
143
|
+
.sort((a, b) => b[1] - a[1])
|
|
144
|
+
.map(([k, v]) => `${k} ${String(v)}`)
|
|
145
|
+
.join(' · ');
|
|
146
|
+
process.stdout.write(` verdicts: ${verdicts}\n`);
|
|
147
|
+
process.stdout.write(
|
|
148
|
+
` excluded (retired \`absent\` vocabulary): ${String(r.excluded_absent)}\n`,
|
|
149
|
+
);
|
|
150
|
+
const comp = Object.entries(r.by_agent_type)
|
|
151
|
+
.sort((a, b) => b[1] - a[1])
|
|
152
|
+
.map(([k, v]) => `${k} ${String(v)}`)
|
|
153
|
+
.join(' · ');
|
|
154
|
+
process.stdout.write(` agent-type composition: ${comp}\n`);
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
' CAVEAT: machine-local. This ledger is one machine\'s drain traffic, so the\n' +
|
|
157
|
+
' composition above is the shape of THIS traffic and the rate does not\n' +
|
|
158
|
+
' generalise. Published with the composition rather than alone, so the limit\n' +
|
|
159
|
+
' is readable instead of hidden.\n',
|
|
160
|
+
);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
165
|
+
process.exit(main());
|
|
166
|
+
}
|