@event4u/agent-config 12.0.0 → 13.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 +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- 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/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- 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/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- 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/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- 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 +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- 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 +44 -23
- 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 +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- 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/docs/MIGRATION.md +20 -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/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -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/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- 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 +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -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/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -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/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Phase 1 of `road-to-inbox-harvest-2026-08-c-evidence-lifecycle` — measure
|
|
4
|
+
* which of the R2 manifest's THREE hash segments actually moves, and what
|
|
5
|
+
* moved it.
|
|
6
|
+
*
|
|
7
|
+
* The premise under test: `agents/roadmaps/` sits inside the review scope
|
|
8
|
+
* (`REVIEW_SCOPE_EXCLUDES` holds two entries, neither of them the roadmap
|
|
9
|
+
* tree), so flipping one checkbox changes `scope_hash` and invalidates a
|
|
10
|
+
* binding that no code change touched. That is a mechanism read from source.
|
|
11
|
+
* Whether it is what has ACTUALLY been costing re-binds is an incidence
|
|
12
|
+
* question, and only a measurement answers it.
|
|
13
|
+
*
|
|
14
|
+
* What this probe does NOT do: it never re-derives the scope definition. The
|
|
15
|
+
* flag list, the exclude list and the hash come from `dispatch_r2_reviewer`,
|
|
16
|
+
* so a scope this probe reports is a scope the gate would produce.
|
|
17
|
+
*
|
|
18
|
+
* Reading the segments:
|
|
19
|
+
*
|
|
20
|
+
* - `scope_hash` — compared against the STORED `diff.patch` in the binding's
|
|
21
|
+
* `*.review-input/` directory. That file is the recorded revision's scope
|
|
22
|
+
* body, so its sha256 is the scope the reviewer actually read. A mismatch
|
|
23
|
+
* means the artefact was re-bound after dispatch and the stored input no
|
|
24
|
+
* longer describes what the artefact binds.
|
|
25
|
+
* - `roadmap_hash` / `ac_hash` — recomputed from the roadmap named in the
|
|
26
|
+
* manifest, at the working tree, following the file into `archive/` when
|
|
27
|
+
* the roadmap has since closed.
|
|
28
|
+
*
|
|
29
|
+
* Attribution: for every binding whose `scope_hash` moved, walk the commits
|
|
30
|
+
* that touched the findings artefact, find the commit where the recorded
|
|
31
|
+
* `scope_hash` changed value, and report the in-scope paths that changed
|
|
32
|
+
* between that commit and its predecessor. Those paths are what moved the
|
|
33
|
+
* hash. Paths are classified `roadmap` / `docs` / `agents-other` / `code`.
|
|
34
|
+
*
|
|
35
|
+
* Output: `agents/evidence/analysis/review-binding-drift.md` with `--write`,
|
|
36
|
+
* stdout otherwise. Exit 0 always — this measures, it does not gate.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { execFileSync } from 'node:child_process';
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
extractAcceptanceCriteria,
|
|
46
|
+
parseManifest,
|
|
47
|
+
reviewScopeDiffArgs,
|
|
48
|
+
reviewScopeNameOnlyArgs,
|
|
49
|
+
sha256,
|
|
50
|
+
} from './dispatch_r2_reviewer.js';
|
|
51
|
+
|
|
52
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
53
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
54
|
+
const REVIEWS_DIR = path.join(ROOT, 'agents', 'evidence', 'reviews');
|
|
55
|
+
const OUT_REL = path.join('agents', 'evidence', 'analysis', 'review-binding-drift.md');
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The release span the roadmap asks about by name ("the four 12.0.0-era
|
|
59
|
+
* re-binds"). Derived from tags rather than a hardcoded commit list so the
|
|
60
|
+
* section stays correct if the era's boundaries are re-cut, and skipped
|
|
61
|
+
* entirely where the tags do not resolve.
|
|
62
|
+
*/
|
|
63
|
+
const RELEASE_ERA = { from: '11.0.0', to: '12.0.0' };
|
|
64
|
+
|
|
65
|
+
type SegmentVerdict = 'same' | 'moved' | 'none' | 'no-input' | 'missing' | 'prose-bound';
|
|
66
|
+
type PathClass = 'roadmap' | 'docs' | 'agents-other' | 'code';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What moved a binding's scope hash.
|
|
70
|
+
*
|
|
71
|
+
* - `code` — at least one code path changed between the two binding states.
|
|
72
|
+
* The review correctly noticed it; this churn is the gate working.
|
|
73
|
+
* - `non-code` — every changed path was roadmap, dashboard, docs, or other
|
|
74
|
+
* `agents/` content. This is the class the roadmap's Phase 2 would address.
|
|
75
|
+
* - `base-moved` — NO in-scope path changed at all, yet the hash differs.
|
|
76
|
+
* The scope is `base...HEAD`, so merging the trunk into the branch moves
|
|
77
|
+
* the merge base and rewrites the diff without anyone touching a reviewed
|
|
78
|
+
* file. Counting these as `code` would overstate the gate's usefulness;
|
|
79
|
+
* counting them as `non-code` would overstate the roadmap mechanism.
|
|
80
|
+
*/
|
|
81
|
+
type RebindCause = 'code' | 'non-code' | 'base-moved' | 'unattributable';
|
|
82
|
+
|
|
83
|
+
interface Attribution {
|
|
84
|
+
/** Commit at which the artefact's `scope_hash` changed value. */
|
|
85
|
+
commit: string;
|
|
86
|
+
subject: string;
|
|
87
|
+
/** In-scope paths that changed since the previous binding state. */
|
|
88
|
+
paths: string[];
|
|
89
|
+
classes: PathClass[];
|
|
90
|
+
/** The trunk merge base differs between the two binding states. */
|
|
91
|
+
baseMoved: boolean;
|
|
92
|
+
cause: RebindCause;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Did a merge land between the two binding states?
|
|
97
|
+
*
|
|
98
|
+
* This is the observable signature of the scope being rewritten by something
|
|
99
|
+
* other than an edit: the scope is `base...head`, so merging the trunk into
|
|
100
|
+
* the branch changes the diff — and the hash — without anyone touching a
|
|
101
|
+
* reviewed file.
|
|
102
|
+
*
|
|
103
|
+
* Comparing `merge-base origin/main <rev>` at the two states does NOT work and
|
|
104
|
+
* was tried: almost every reviewed commit has since landed in the trunk, which
|
|
105
|
+
* makes its merge base itself, so the comparison degenerates into "are these
|
|
106
|
+
* two commits different" and reported 100 % of events as base-moved. The merge
|
|
107
|
+
* commit inside the span is a local fact that stays true after the branch
|
|
108
|
+
* lands.
|
|
109
|
+
*/
|
|
110
|
+
function mergeInSpan(from: string, to: string): boolean {
|
|
111
|
+
const merges = gitOrNull('rev-list', '--merges', `${from}..${to}`);
|
|
112
|
+
return merges !== null && merges.trim() !== '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface Row {
|
|
116
|
+
slug: string;
|
|
117
|
+
scope: SegmentVerdict;
|
|
118
|
+
roadmap: SegmentVerdict;
|
|
119
|
+
ac: SegmentVerdict;
|
|
120
|
+
roadmapPath: string;
|
|
121
|
+
dispatched: string;
|
|
122
|
+
/** Every recorded re-bind of this artefact, newest first. */
|
|
123
|
+
rebinds: Attribution[];
|
|
124
|
+
note: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `git`, but a failure is an answer. stderr is swallowed: asking a commit for
|
|
128
|
+
* a file it predates is a normal outcome here, not a fault to report. */
|
|
129
|
+
function gitOrNull(...args: string[]): string | null {
|
|
130
|
+
try {
|
|
131
|
+
return execFileSync('git', args, {
|
|
132
|
+
cwd: ROOT,
|
|
133
|
+
encoding: 'utf-8',
|
|
134
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
135
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `agents/roadmaps-progress.md` is the generated dashboard, so it counts as
|
|
144
|
+
* roadmap content even though it sits beside the directory rather than in it —
|
|
145
|
+
* it is regenerated on every roadmap touch, which is precisely why a single
|
|
146
|
+
* checkbox produces two in-scope file changes.
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* The scope hash an artefact binds, and where it was found.
|
|
150
|
+
*
|
|
151
|
+
* A SKIP / honest-null artefact carries no `context-manifest` block — its
|
|
152
|
+
* scope hash lives in the declaration prose (`… scope <64-hex>, declared …`).
|
|
153
|
+
* Reading only the manifest would therefore drop 11 of the 52 artefacts in
|
|
154
|
+
* this tree from the measurement, and they are not a random 11: the skip
|
|
155
|
+
* grammar asserts "no code surface for this completion", so the population
|
|
156
|
+
* excluded is exactly the one where a non-code re-bind is most likely. A
|
|
157
|
+
* ratio computed over manifests alone would have been biased in the direction
|
|
158
|
+
* of its own conclusion.
|
|
159
|
+
*/
|
|
160
|
+
function scopeHashOf(text: string): { hash: string; source: 'manifest' | 'prose' } | null {
|
|
161
|
+
const manifest = parseManifest(text);
|
|
162
|
+
if (manifest !== null) return { hash: manifest.scope_hash, source: 'manifest' };
|
|
163
|
+
const prose = /\bscope ([0-9a-f]{64})\b/.exec(text);
|
|
164
|
+
return prose === null ? null : { hash: prose[1] as string, source: 'prose' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function classifyPath(p: string): PathClass {
|
|
168
|
+
if (p.startsWith('agents/roadmaps/') || p === 'agents/roadmaps-progress.md') return 'roadmap';
|
|
169
|
+
if (p.startsWith('docs/')) return 'docs';
|
|
170
|
+
if (p.startsWith('agents/')) return 'agents-other';
|
|
171
|
+
return 'code';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the roadmap named in a manifest against the working tree, following
|
|
176
|
+
* it into `archive/` — a roadmap that closed since the review was dispatched
|
|
177
|
+
* moved, and reporting `missing` for it would count a rename as drift.
|
|
178
|
+
*/
|
|
179
|
+
function resolveRoadmap(rel: string): string | null {
|
|
180
|
+
const direct = path.join(ROOT, rel);
|
|
181
|
+
if (fs.existsSync(direct)) return direct;
|
|
182
|
+
const base = path.basename(rel);
|
|
183
|
+
for (const sub of ['archive', 'skipped', 'later']) {
|
|
184
|
+
const candidate = path.join(ROOT, 'agents', 'roadmaps', sub, base);
|
|
185
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Every commit that changed the artefact's `scope_hash`, newest first, each
|
|
192
|
+
* carrying the in-scope paths that moved it.
|
|
193
|
+
*
|
|
194
|
+
* The span for a transition ends at the PREVIOUS commit that touched the
|
|
195
|
+
* artefact, not at the transition's own parent. A re-bind is frequently its
|
|
196
|
+
* own commit carrying nothing but the artefact — and the artefact sits under
|
|
197
|
+
* `agents/evidence/reviews`, which the scope excludes, so a parent-to-child
|
|
198
|
+
* diff of that commit is EMPTY and would attribute the re-bind to nothing.
|
|
199
|
+
* What actually moved the hash is everything that landed between the two
|
|
200
|
+
* binding states.
|
|
201
|
+
*
|
|
202
|
+
* An artefact introduced in a single commit already carrying its final hash
|
|
203
|
+
* has no earlier state to measure against and yields an empty list.
|
|
204
|
+
*/
|
|
205
|
+
function transitions(findingsRel: string): Attribution[] {
|
|
206
|
+
const log = gitOrNull('log', '--format=%H', '--', findingsRel);
|
|
207
|
+
if (log === null) return [];
|
|
208
|
+
const commits = log.trim().split('\n').filter(Boolean); // newest first
|
|
209
|
+
|
|
210
|
+
const hashCache = new Map<string, string | null>();
|
|
211
|
+
const hashAt = (commit: string): string | null => {
|
|
212
|
+
// Each revision is the `now` of one iteration and the `before` of the
|
|
213
|
+
// next; without the cache every blob is fetched twice.
|
|
214
|
+
const cached = hashCache.get(commit);
|
|
215
|
+
if (cached !== undefined) return cached;
|
|
216
|
+
const blob = gitOrNull('show', `${commit}:${findingsRel}`);
|
|
217
|
+
const hash = blob === null ? null : (scopeHashOf(blob)?.hash ?? null);
|
|
218
|
+
hashCache.set(commit, hash);
|
|
219
|
+
return hash;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const found: Attribution[] = [];
|
|
223
|
+
for (let i = 0; i < commits.length - 1; i++) {
|
|
224
|
+
const commit = commits[i] as string;
|
|
225
|
+
const previousState = commits[i + 1] as string;
|
|
226
|
+
const now = hashAt(commit);
|
|
227
|
+
const before = hashAt(previousState);
|
|
228
|
+
if (now === null || before === null || now === before) continue;
|
|
229
|
+
|
|
230
|
+
const names = gitOrNull(...reviewScopeNameOnlyArgs(previousState, commit));
|
|
231
|
+
const subject = (gitOrNull('log', '--format=%s', '-1', commit) ?? '').trim();
|
|
232
|
+
if (names === null) {
|
|
233
|
+
// Do NOT drop it: a silently skipped event shrinks the denominator
|
|
234
|
+
// of the published ratio, which is the one number that must not be
|
|
235
|
+
// quietly optimistic.
|
|
236
|
+
found.push({
|
|
237
|
+
commit: commit.slice(0, 9),
|
|
238
|
+
subject,
|
|
239
|
+
paths: [],
|
|
240
|
+
classes: [],
|
|
241
|
+
baseMoved: false,
|
|
242
|
+
cause: 'unattributable',
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const paths = names.trim().split('\n').filter(Boolean);
|
|
247
|
+
const classes = [...new Set(paths.map(classifyPath))];
|
|
248
|
+
|
|
249
|
+
// Measured, never inferred: the scope is `base...head`, so if the merge
|
|
250
|
+
// base against the trunk differs between the two binding states, the
|
|
251
|
+
// diff was rewritten no matter what anyone edited. Deriving this from
|
|
252
|
+
// "no paths changed" instead would mis-file every trunk merge that also
|
|
253
|
+
// carried a real edit — and those are the majority of them.
|
|
254
|
+
const baseMoved = mergeInSpan(previousState, commit);
|
|
255
|
+
const cause: RebindCause = classes.includes('code')
|
|
256
|
+
? 'code'
|
|
257
|
+
: paths.length > 0
|
|
258
|
+
? 'non-code'
|
|
259
|
+
: baseMoved
|
|
260
|
+
? 'base-moved'
|
|
261
|
+
: 'unattributable';
|
|
262
|
+
found.push({ commit: commit.slice(0, 9), subject, paths, classes, baseMoved, cause });
|
|
263
|
+
}
|
|
264
|
+
return found;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildRows(): Row[] {
|
|
268
|
+
const files = fs
|
|
269
|
+
.readdirSync(REVIEWS_DIR)
|
|
270
|
+
.filter((f) => f.endsWith('.findings.md'))
|
|
271
|
+
.sort();
|
|
272
|
+
|
|
273
|
+
const rows: Row[] = [];
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
const slug = file.replace(/\.findings\.md$/, '');
|
|
276
|
+
const abs = path.join(REVIEWS_DIR, file);
|
|
277
|
+
const rel = path.posix.join('agents/evidence/reviews', file);
|
|
278
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
279
|
+
const manifest = parseManifest(text);
|
|
280
|
+
if (manifest === null) {
|
|
281
|
+
const prose = scopeHashOf(text);
|
|
282
|
+
rows.push({
|
|
283
|
+
slug,
|
|
284
|
+
scope: prose === null ? 'missing' : 'prose-bound',
|
|
285
|
+
roadmap: 'none',
|
|
286
|
+
ac: 'none',
|
|
287
|
+
roadmapPath: '—',
|
|
288
|
+
dispatched: '—',
|
|
289
|
+
rebinds: prose === null ? [] : transitions(rel),
|
|
290
|
+
note: prose === null ? 'no scope hash at all' : 'skip artefact — scope hash in prose',
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// scope segment — the stored review input IS the recorded revision.
|
|
296
|
+
const patch = path.join(REVIEWS_DIR, `${slug}.review-input`, 'diff.patch');
|
|
297
|
+
let scope: SegmentVerdict;
|
|
298
|
+
if (!fs.existsSync(patch)) {
|
|
299
|
+
scope = 'no-input';
|
|
300
|
+
} else {
|
|
301
|
+
scope = sha256(fs.readFileSync(patch, 'utf-8')) === manifest.scope_hash ? 'same' : 'moved';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// roadmap + ac segments — recomputed at the working tree.
|
|
305
|
+
let roadmap: SegmentVerdict = 'none';
|
|
306
|
+
let ac: SegmentVerdict = 'none';
|
|
307
|
+
let note = '';
|
|
308
|
+
if (manifest.roadmap !== 'none') {
|
|
309
|
+
const resolved = resolveRoadmap(manifest.roadmap);
|
|
310
|
+
if (resolved === null) {
|
|
311
|
+
roadmap = 'missing';
|
|
312
|
+
ac = 'missing';
|
|
313
|
+
note = 'roadmap not found in tree';
|
|
314
|
+
} else {
|
|
315
|
+
const text = fs.readFileSync(resolved, 'utf-8');
|
|
316
|
+
roadmap = sha256(text) === manifest.roadmap_hash ? 'same' : 'moved';
|
|
317
|
+
const acText = extractAcceptanceCriteria(text);
|
|
318
|
+
ac =
|
|
319
|
+
manifest.ac_hash === 'none'
|
|
320
|
+
? 'none'
|
|
321
|
+
: sha256(acText) === manifest.ac_hash
|
|
322
|
+
? 'same'
|
|
323
|
+
: 'moved';
|
|
324
|
+
if (path.dirname(resolved) !== path.dirname(path.join(ROOT, manifest.roadmap))) {
|
|
325
|
+
note = 'roadmap archived since dispatch';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
rows.push({
|
|
331
|
+
slug,
|
|
332
|
+
scope,
|
|
333
|
+
roadmap,
|
|
334
|
+
ac,
|
|
335
|
+
roadmapPath: manifest.roadmap,
|
|
336
|
+
dispatched: manifest.dispatched,
|
|
337
|
+
rebinds: transitions(rel),
|
|
338
|
+
note,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return rows;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
type Tier = 'active' | 'recent' | 'archived' | 'unknown';
|
|
345
|
+
|
|
346
|
+
interface Retention {
|
|
347
|
+
slug: string;
|
|
348
|
+
tier: Tier;
|
|
349
|
+
bytes: number;
|
|
350
|
+
/** Head revision the review was dispatched at, read from `prompt.md`. */
|
|
351
|
+
head: string | null;
|
|
352
|
+
headReachable: boolean;
|
|
353
|
+
/** How the stored patch was re-derived, or why it could not be. */
|
|
354
|
+
reproducible: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function dirBytes(dir: string): number {
|
|
358
|
+
let total = 0;
|
|
359
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
360
|
+
const p = path.join(dir, entry.name);
|
|
361
|
+
total += entry.isDirectory() ? dirBytes(p) : fs.statSync(p).size;
|
|
362
|
+
}
|
|
363
|
+
return total;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Attempt to RE-DERIVE the stored `diff.patch` and report what happened.
|
|
368
|
+
*
|
|
369
|
+
* The manifest records no base revision — only `prompt.md` names the head the
|
|
370
|
+
* review was dispatched at — so reproducibility cannot be asserted from the
|
|
371
|
+
* record alone; it has to be tried. Two base candidates are attempted, in the
|
|
372
|
+
* order that makes them meaningful:
|
|
373
|
+
*
|
|
374
|
+
* 1. The first parent of the merge commit that brought the head into the
|
|
375
|
+
* trunk. That IS the trunk as it stood at merge time, which is what
|
|
376
|
+
* `base...HEAD` resolved against for a branch that has since landed.
|
|
377
|
+
* 2. `origin/main` as it stands now — correct only for a branch not yet
|
|
378
|
+
* merged, where the merge base has not moved.
|
|
379
|
+
*
|
|
380
|
+
* A patch that matches under neither is not proven irreproducible in
|
|
381
|
+
* principle; it is unproven, which is the only honest verdict available and
|
|
382
|
+
* the one that keeps the stored copy.
|
|
383
|
+
*/
|
|
384
|
+
function tryReproduce(head: string, patchSha: string): string {
|
|
385
|
+
const candidates: Array<{ base: string; how: string }> = [];
|
|
386
|
+
|
|
387
|
+
const merge = gitOrNull('rev-list', '--ancestry-path', '--merges', `${head}..origin/main`);
|
|
388
|
+
const mergeCommits = (merge ?? '').trim().split('\n').filter(Boolean);
|
|
389
|
+
const firstMerge = mergeCommits[mergeCommits.length - 1];
|
|
390
|
+
if (firstMerge !== undefined) {
|
|
391
|
+
const parent = gitOrNull('rev-parse', `${firstMerge}^1`);
|
|
392
|
+
if (parent !== null) {
|
|
393
|
+
candidates.push({ base: parent.trim(), how: `trunk at merge \`${firstMerge.slice(0, 9)}\`` });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
candidates.push({ base: 'origin/main', how: 'current `origin/main`' });
|
|
397
|
+
|
|
398
|
+
for (const c of candidates) {
|
|
399
|
+
const diff = gitOrNull(...reviewScopeDiffArgs(c.base, head));
|
|
400
|
+
if (diff !== null && sha256(diff) === patchSha) return `yes — ${c.how}`;
|
|
401
|
+
}
|
|
402
|
+
return 'no — not re-derivable from the recorded head alone';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Tier every `*.review-input/` DIRECTORY on disk.
|
|
407
|
+
*
|
|
408
|
+
* Enumerating directories rather than findings artefacts is load-bearing: a
|
|
409
|
+
* stored input whose artefact was never committed (or was renamed) still
|
|
410
|
+
* occupies bytes and still has to carry a tier, and acceptance criterion 4
|
|
411
|
+
* says "every `review-input/` directory" — not "every one with an artefact".
|
|
412
|
+
* Driving this off the artefact list silently omitted exactly such a case.
|
|
413
|
+
*/
|
|
414
|
+
function retention(rows: Row[]): Retention[] {
|
|
415
|
+
const scopeBySlug = new Map(rows.map((r) => [r.slug, r.scope]));
|
|
416
|
+
const dirs = fs
|
|
417
|
+
.readdirSync(REVIEWS_DIR, { withFileTypes: true })
|
|
418
|
+
.filter((e) => e.isDirectory() && e.name.endsWith('.review-input'))
|
|
419
|
+
.map((e) => e.name)
|
|
420
|
+
.sort();
|
|
421
|
+
|
|
422
|
+
const out: Retention[] = [];
|
|
423
|
+
for (const dirName of dirs) {
|
|
424
|
+
const slug = dirName.replace(/\.review-input$/, '');
|
|
425
|
+
const dir = path.join(REVIEWS_DIR, dirName);
|
|
426
|
+
|
|
427
|
+
const promptPath = path.join(dir, 'prompt.md');
|
|
428
|
+
const prompt = fs.existsSync(promptPath) ? fs.readFileSync(promptPath, 'utf-8') : '';
|
|
429
|
+
const head = /branch head ([0-9a-f]{40})/.exec(prompt)?.[1] ?? null;
|
|
430
|
+
const headReachable = head !== null && gitOrNull('cat-file', '-e', head) !== null;
|
|
431
|
+
|
|
432
|
+
// `merge-base --is-ancestor` exits 1 for "not an ancestor" and also
|
|
433
|
+
// fails when `origin/main` does not resolve. Distinguish them: an
|
|
434
|
+
// unresolvable trunk must not silently tier every directory.
|
|
435
|
+
let tier: Tier = 'unknown';
|
|
436
|
+
if (head !== null && headReachable && gitOrNull('rev-parse', '--verify', 'origin/main') !== null) {
|
|
437
|
+
const merged = gitOrNull('merge-base', '--is-ancestor', head, 'origin/main') !== null;
|
|
438
|
+
const scope = scopeBySlug.get(slug);
|
|
439
|
+
tier = merged
|
|
440
|
+
? 'archived'
|
|
441
|
+
: scope === 'same'
|
|
442
|
+
? 'active'
|
|
443
|
+
: scope === 'moved'
|
|
444
|
+
? 'recent'
|
|
445
|
+
: 'unknown'; // prose-bound / no artefact: not comparable, not "re-bound"
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const patchPath = path.join(dir, 'diff.patch');
|
|
449
|
+
const patchSha = fs.existsSync(patchPath)
|
|
450
|
+
? sha256(fs.readFileSync(patchPath, 'utf-8'))
|
|
451
|
+
: null;
|
|
452
|
+
|
|
453
|
+
out.push({
|
|
454
|
+
slug,
|
|
455
|
+
tier,
|
|
456
|
+
bytes: dirBytes(dir),
|
|
457
|
+
head,
|
|
458
|
+
headReachable,
|
|
459
|
+
reproducible:
|
|
460
|
+
head === null
|
|
461
|
+
? 'no — no head revision recorded'
|
|
462
|
+
: !headReachable
|
|
463
|
+
? 'no — recorded head unreachable (history rewritten or pruned)'
|
|
464
|
+
: patchSha === null
|
|
465
|
+
? 'n/a — no stored patch'
|
|
466
|
+
: tryReproduce(head, patchSha),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Abbreviated commits in the release era, or `null` when the tags do not
|
|
474
|
+
* resolve — a consumer checkout carries neither tag and must not fail here.
|
|
475
|
+
*/
|
|
476
|
+
function eraCommits(): Set<string> | null {
|
|
477
|
+
const log = gitOrNull('log', '--format=%H', `${RELEASE_ERA.from}..${RELEASE_ERA.to}`);
|
|
478
|
+
if (log === null) return null;
|
|
479
|
+
return new Set(
|
|
480
|
+
log
|
|
481
|
+
.trim()
|
|
482
|
+
.split('\n')
|
|
483
|
+
.filter(Boolean)
|
|
484
|
+
.map((c) => c.slice(0, 9)),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function render(rows: Row[]): string {
|
|
489
|
+
const withInput = rows.filter((r) => r.scope === 'same' || r.scope === 'moved');
|
|
490
|
+
const movedScope = rows.filter((r) => r.scope === 'moved');
|
|
491
|
+
|
|
492
|
+
// The ratio is counted over re-bind EVENTS, not over bindings: a binding
|
|
493
|
+
// re-bound three times cost three re-binds, and the question Phase 2 turns
|
|
494
|
+
// on is what has been costing them.
|
|
495
|
+
const events = rows.flatMap((r) => r.rebinds.map((a) => ({ slug: r.slug, a })));
|
|
496
|
+
const nonCode = events.filter((e) => e.a.cause === 'non-code');
|
|
497
|
+
const code = events.filter((e) => e.a.cause === 'code');
|
|
498
|
+
const baseMoved = events.filter((e) => e.a.cause === 'base-moved');
|
|
499
|
+
const unattributable = events.filter((e) => e.a.cause === 'unattributable');
|
|
500
|
+
|
|
501
|
+
const pct = (n: number, d: number): string => (d === 0 ? 'n/a' : `${((n / d) * 100).toFixed(1)} %`);
|
|
502
|
+
|
|
503
|
+
const out: string[] = [];
|
|
504
|
+
out.push('## Measurement');
|
|
505
|
+
out.push('');
|
|
506
|
+
out.push('Generated by `./scripts-run src/scripts/probe_review_binding_drift --write`.');
|
|
507
|
+
out.push('Re-run it rather than editing the tables by hand; every number below is derived.');
|
|
508
|
+
out.push('Everything outside the two markers is preserved across runs.');
|
|
509
|
+
out.push('');
|
|
510
|
+
out.push(`- Findings artefacts scanned: **${rows.length}**`);
|
|
511
|
+
out.push(`- Carrying a stored \`review-input/diff.patch\`: **${withInput.length}**`);
|
|
512
|
+
out.push(
|
|
513
|
+
`- \`scope_hash\` still reproduces that stored input: **${withInput.length - movedScope.length}**`,
|
|
514
|
+
);
|
|
515
|
+
out.push(`- \`scope_hash\` moved after dispatch: **${movedScope.length}**`);
|
|
516
|
+
out.push(`- Recorded re-bind events across all artefacts: **${events.length}**`);
|
|
517
|
+
out.push('');
|
|
518
|
+
out.push('## The ratio Phase 2 turns on');
|
|
519
|
+
out.push('');
|
|
520
|
+
out.push('Counted per re-bind **event** — one artefact re-bound three times cost three');
|
|
521
|
+
out.push('re-binds. A re-bind is attributed to the in-scope paths that changed between');
|
|
522
|
+
out.push('the previous binding state and the commit that moved the hash.');
|
|
523
|
+
out.push('');
|
|
524
|
+
const withMerge = events.filter((e) => e.a.baseMoved);
|
|
525
|
+
const codeWithMerge = code.filter((e) => e.a.baseMoved);
|
|
526
|
+
const addressable = nonCode.filter((e) => !e.a.baseMoved);
|
|
527
|
+
|
|
528
|
+
out.push('Two independent axes are measured, and neither is inferred from the other:');
|
|
529
|
+
out.push('the path classes that changed, and whether a MERGE landed in the span. The');
|
|
530
|
+
out.push('second matters because the scope is `base...head` — merging the trunk into a');
|
|
531
|
+
out.push('branch rewrites the diff, and therefore the hash, without anyone touching a');
|
|
532
|
+
out.push('reviewed file.');
|
|
533
|
+
out.push('');
|
|
534
|
+
out.push('| Cause (by changed paths) | Events | Share | of those, span carried a merge |');
|
|
535
|
+
out.push('|---|---:|---:|---:|');
|
|
536
|
+
out.push(
|
|
537
|
+
`| \`code\` — a code path changed | ${code.length} | ${pct(code.length, events.length)} | ${codeWithMerge.length} |`,
|
|
538
|
+
);
|
|
539
|
+
out.push(
|
|
540
|
+
`| \`non-code\` — only roadmap / dashboard / docs | ${nonCode.length} | ${pct(nonCode.length, events.length)} | ${nonCode.length - addressable.length} |`,
|
|
541
|
+
);
|
|
542
|
+
out.push(
|
|
543
|
+
`| \`base-moved\` — no path changed, but a merge landed | ${baseMoved.length} | ${pct(baseMoved.length, events.length)} | ${baseMoved.length} |`,
|
|
544
|
+
);
|
|
545
|
+
out.push(
|
|
546
|
+
`| \`unattributable\` — no path changed and no merge | ${unattributable.length} | ${pct(unattributable.length, events.length)} | 0 |`,
|
|
547
|
+
);
|
|
548
|
+
out.push('');
|
|
549
|
+
out.push(
|
|
550
|
+
`A merge landed in the span of **${withMerge.length}** of ${events.length} events`,
|
|
551
|
+
`(${pct(withMerge.length, events.length)}), including ${codeWithMerge.length} filed under \`code\`. Those`,
|
|
552
|
+
'are re-binds where the diff was rewritten by the merge as well as by an edit, so',
|
|
553
|
+
'the `code` row is an upper bound on "the review correctly noticed a change",',
|
|
554
|
+
'not an exact count.',
|
|
555
|
+
);
|
|
556
|
+
out.push('');
|
|
557
|
+
out.push(
|
|
558
|
+
`**Addressable by a segment-aware verdict: ${addressable.length} of ${events.length} events`,
|
|
559
|
+
`(${pct(addressable.length, events.length)})** — non-code paths only AND no merge in the span.`,
|
|
560
|
+
'Every other class moved either code or the diff itself, and consulting the roadmap',
|
|
561
|
+
'and AC segments does not reach any of them.',
|
|
562
|
+
);
|
|
563
|
+
out.push('');
|
|
564
|
+
out.push('## Non-code-only re-binds, in full');
|
|
565
|
+
out.push('');
|
|
566
|
+
if (nonCode.length === 0) {
|
|
567
|
+
out.push('None. No binding in the tree was invalidated by a change that touched no code.');
|
|
568
|
+
} else {
|
|
569
|
+
for (const e of nonCode) {
|
|
570
|
+
out.push(`- \`${e.slug}\` @ \`${e.a.commit}\` — ${e.a.subject}`);
|
|
571
|
+
out.push(` - paths: ${e.a.paths.join(', ')}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
out.push('');
|
|
575
|
+
const era = eraCommits();
|
|
576
|
+
out.push(`## The ${RELEASE_ERA.to}-era re-binds`);
|
|
577
|
+
out.push('');
|
|
578
|
+
if (era === null) {
|
|
579
|
+
out.push(
|
|
580
|
+
`Tags \`${RELEASE_ERA.from}\` / \`${RELEASE_ERA.to}\` do not resolve here — section skipped.`,
|
|
581
|
+
);
|
|
582
|
+
} else {
|
|
583
|
+
const inEra = events.filter((e) => era.has(e.a.commit));
|
|
584
|
+
out.push(
|
|
585
|
+
`Re-bind events landing in \`${RELEASE_ERA.from}..${RELEASE_ERA.to}\`: **${inEra.length}**.`,
|
|
586
|
+
);
|
|
587
|
+
out.push('');
|
|
588
|
+
if (inEra.length === 0) {
|
|
589
|
+
out.push('None.');
|
|
590
|
+
} else {
|
|
591
|
+
out.push('| Binding | Commit | Caused by | Paths |');
|
|
592
|
+
out.push('|---|---|---|---|');
|
|
593
|
+
for (const e of inEra) {
|
|
594
|
+
out.push(
|
|
595
|
+
`| \`${e.slug}\` | \`${e.a.commit}\` | ${e.a.cause} | ${e.a.paths.join(', ') || '(none in scope)'} |`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
out.push('');
|
|
601
|
+
out.push('## Per-binding segment verdicts');
|
|
602
|
+
out.push('');
|
|
603
|
+
out.push('`same` = segment reproduces its recorded hash · `moved` = it does not ·');
|
|
604
|
+
out.push('`none` = the manifest recorded none · `no-input` = no stored `review-input/`.');
|
|
605
|
+
out.push('');
|
|
606
|
+
out.push('| Binding | scope | roadmap | ac | re-binds | causes | note |');
|
|
607
|
+
out.push('|---|---|---|---|---:|---|---|');
|
|
608
|
+
for (const r of rows) {
|
|
609
|
+
const causes =
|
|
610
|
+
r.rebinds.length === 0
|
|
611
|
+
? '—'
|
|
612
|
+
: [...new Set(r.rebinds.map((a) => a.cause))].join(', ');
|
|
613
|
+
out.push(
|
|
614
|
+
`| \`${r.slug}\` | ${r.scope} | ${r.roadmap} | ${r.ac} | ${r.rebinds.length} | ${causes} | ${r.note || '—'} |`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
out.push('');
|
|
618
|
+
const ret = retention(rows);
|
|
619
|
+
const mb = (n: number): string => `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
620
|
+
const byTier = (t: Tier): Retention[] => ret.filter((r) => r.tier === t);
|
|
621
|
+
const proven = ret.filter((r) => r.reproducible.startsWith('yes'));
|
|
622
|
+
const unproven = ret.filter((r) => r.reproducible.startsWith('no'));
|
|
623
|
+
|
|
624
|
+
out.push('## Retention tiers');
|
|
625
|
+
out.push('');
|
|
626
|
+
out.push('`active` = the artefact still binds the scope its stored input records ·');
|
|
627
|
+
out.push('`recent` = re-bound, and the reviewed head is not yet in the trunk ·');
|
|
628
|
+
out.push('`archived` = the reviewed head is an ancestor of `origin/main`.');
|
|
629
|
+
out.push('');
|
|
630
|
+
out.push('| Tier | Dirs | Bytes |');
|
|
631
|
+
out.push('|---|---:|---:|');
|
|
632
|
+
for (const t of ['active', 'recent', 'archived', 'unknown'] as Tier[]) {
|
|
633
|
+
const g = byTier(t);
|
|
634
|
+
if (g.length > 0) out.push(`| ${t} | ${g.length} | ${mb(g.reduce((s, r) => s + r.bytes, 0))} |`);
|
|
635
|
+
}
|
|
636
|
+
out.push(`| **total** | **${ret.length}** | **${mb(ret.reduce((s, r) => s + r.bytes, 0))}** |`);
|
|
637
|
+
out.push('');
|
|
638
|
+
out.push('### Regeneration guarantee');
|
|
639
|
+
out.push('');
|
|
640
|
+
const provenBytes = proven.reduce((s, r) => s + r.bytes, 0);
|
|
641
|
+
// Summed over the SAME rows the count reports — `ret` also holds `n/a`
|
|
642
|
+
// rows (no stored patch), so `total - proven` would mix two populations.
|
|
643
|
+
const unprovenBytes = unproven.reduce((s, r) => s + r.bytes, 0);
|
|
644
|
+
const totalBytes = ret.reduce((s, r) => s + r.bytes, 0);
|
|
645
|
+
out.push(`Re-derived successfully: **${proven.length}** of ${ret.length} — ${mb(provenBytes)}.`);
|
|
646
|
+
out.push(
|
|
647
|
+
`Not re-derivable from the record: **${unproven.length}** — ${mb(unprovenBytes)}, which stays regardless.`,
|
|
648
|
+
);
|
|
649
|
+
const other = ret.length - proven.length - unproven.length;
|
|
650
|
+
if (other > 0) {
|
|
651
|
+
out.push(`Neither (no stored patch): **${other}** — ${mb(totalBytes - provenBytes - unprovenBytes)}.`);
|
|
652
|
+
}
|
|
653
|
+
out.push('');
|
|
654
|
+
out.push(
|
|
655
|
+
'That bounds the `evidence-compaction-approval` blocker: the most any compaction',
|
|
656
|
+
`could reclaim is **${mb(provenBytes)}** of ${mb(totalBytes)} (${pct(provenBytes, totalBytes)}), and only`,
|
|
657
|
+
'from the directories listed as re-derivable below.',
|
|
658
|
+
);
|
|
659
|
+
out.push('');
|
|
660
|
+
out.push('A stored patch counts as reproducible only when it was ACTUALLY re-derived');
|
|
661
|
+
out.push('here, byte-for-byte. The manifest records no base revision, so reproducibility');
|
|
662
|
+
out.push('is never assertable from the record alone — it is attempted, and the attempt is');
|
|
663
|
+
out.push('what the verdict reports.');
|
|
664
|
+
out.push('');
|
|
665
|
+
out.push('**Irreproducible directories — these patches are the only copy and stay:**');
|
|
666
|
+
out.push('');
|
|
667
|
+
if (unproven.length === 0) {
|
|
668
|
+
out.push('None.');
|
|
669
|
+
} else {
|
|
670
|
+
for (const r of unproven) {
|
|
671
|
+
out.push(`- \`${r.slug}.review-input\` (${mb(r.bytes)}, ${r.tier}) — ${r.reproducible}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
out.push('');
|
|
675
|
+
out.push('| Directory | Tier | Bytes | Re-derivable |');
|
|
676
|
+
out.push('|---|---|---:|---|');
|
|
677
|
+
for (const r of ret) {
|
|
678
|
+
out.push(
|
|
679
|
+
`| \`${r.slug}.review-input\` | ${r.tier} | ${(r.bytes / 1024).toFixed(0)} kB | ${r.reproducible} |`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
out.push('');
|
|
683
|
+
out.push('## Every re-bind event');
|
|
684
|
+
out.push('');
|
|
685
|
+
out.push('| Binding | Commit | Subject | Path classes |');
|
|
686
|
+
out.push('|---|---|---|---|');
|
|
687
|
+
for (const e of events) {
|
|
688
|
+
out.push(
|
|
689
|
+
`| \`${e.slug}\` | \`${e.a.commit}\` | ${e.a.subject} | ${e.a.classes.join(', ') || '(none in scope)'} |`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
out.push('');
|
|
693
|
+
return out.join('\n');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const BEGIN = '<!-- BEGIN probe_review_binding_drift -->';
|
|
697
|
+
const END = '<!-- END probe_review_binding_drift -->';
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Splice the generated tables into the target between the two markers,
|
|
701
|
+
* leaving everything outside them untouched.
|
|
702
|
+
*
|
|
703
|
+
* The measurement and the DECISION it feeds live in the same file by the
|
|
704
|
+
* roadmap's own step 2.1 ("write the decision into the analysis file either
|
|
705
|
+
* way"), and a decision is prose a human owns. Rewriting the whole file would
|
|
706
|
+
* delete it on the next re-run, which would make the numbers re-derivable and
|
|
707
|
+
* the conclusion disposable — the wrong way round.
|
|
708
|
+
*/
|
|
709
|
+
function splice(existing: string | null, generated: string): string {
|
|
710
|
+
const block = `${BEGIN}\n\n${generated}\n${END}\n`;
|
|
711
|
+
if (existing === null) return block;
|
|
712
|
+
const start = existing.indexOf(BEGIN);
|
|
713
|
+
const end = existing.indexOf(END);
|
|
714
|
+
if (start === -1 || end === -1 || end < start) {
|
|
715
|
+
// No markers yet: keep the human prose, append the block.
|
|
716
|
+
return `${existing.trimEnd()}\n\n${block}`;
|
|
717
|
+
}
|
|
718
|
+
return existing.slice(0, start) + block + existing.slice(end + END.length).replace(/^\n/, '');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const write = process.argv.includes('--write');
|
|
722
|
+
const text = render(buildRows());
|
|
723
|
+
if (write) {
|
|
724
|
+
const target = path.join(ROOT, OUT_REL);
|
|
725
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
726
|
+
const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf-8') : null;
|
|
727
|
+
fs.writeFileSync(target, splice(existing, text), 'utf-8');
|
|
728
|
+
process.stdout.write(`wrote ${OUT_REL}\n`);
|
|
729
|
+
} else {
|
|
730
|
+
process.stdout.write(text);
|
|
731
|
+
}
|