@event4u/agent-config 14.16.0 → 14.17.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +83 -210
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +172 -6
- package/dist/agent-src/commands/fix/commit-messages.md +336 -0
- package/dist/agent-src/commands/fix.md +5 -3
- package/dist/agent-src/rules/commit-conventions.md +1 -1
- package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
- package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
- package/dist/agent-src/skills/terraform/SKILL.md +29 -0
- package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
- package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_explain.js +24 -112
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +42 -16
- 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/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/package.json +1 -1
- package/src/config/web-launch-readiness.json +1 -1
- package/src/scripts/_cli/explain_last/index.ts +1 -1
- package/src/scripts/_cli/explain_last/memory.ts +8 -104
- package/src/scripts/check_artefact_count_messaging.ts +9 -0
- package/src/scripts/check_release_highlights.ts +77 -4
- package/src/scripts/check_web_launch_readiness.ts +295 -15
- package/src/scripts/measure_release_mix.ts +384 -0
- package/src/scripts/release_mix_taxonomy.json +172 -0
- package/src/scripts/attest_artifact.ts +0 -193
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Release governance-vs-product mix — measured from changed paths, published as
|
|
4
|
+
* two views, with no threshold.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists, and what it deliberately is not.
|
|
7
|
+
*
|
|
8
|
+
* An external reviewer asked twice for a per-PR rule: "no meta/governance
|
|
9
|
+
* feature without a user-facing artifact in the same PR". The AI council
|
|
10
|
+
* DECLINED that proposal (2026-09-04, anthropic/claude-sonnet-4-5 +
|
|
11
|
+
* openai/codex-default, 2 rounds, quorum 2/2, $0.00 — both seats
|
|
12
|
+
* subscription-authed) on two grounds: a same-PR gate measures packaging rather
|
|
13
|
+
* than progress, since one token consumer-file edit legitimises an otherwise
|
|
14
|
+
* governance-only pull request; and it rejects legitimate work by construction,
|
|
15
|
+
* since a CI fix, a dependency bump and an analysis round producing only
|
|
16
|
+
* roadmaps are all real work with no consumer surface. The decline is recorded
|
|
17
|
+
* in `docs/decisions/ADR-253-per-pr-user-artifact-gate-declined.md`.
|
|
18
|
+
*
|
|
19
|
+
* This script is the replacement both seats specified: classify at RELEASE
|
|
20
|
+
* level, publish two views, attach a response obligation, and refuse to pick a
|
|
21
|
+
* threshold until at least two readings exist. It therefore **enforces
|
|
22
|
+
* nothing about the ratio**. It measures and it publishes. The one enforced
|
|
23
|
+
* consequence lives in `check_release_highlights.ts` and is about the
|
|
24
|
+
* completeness of the written response, never about the number.
|
|
25
|
+
*
|
|
26
|
+
* The two views.
|
|
27
|
+
*
|
|
28
|
+
* - **Commit view** — exclusive counts per category, plus `mixed`, plus
|
|
29
|
+
* `unclassified`. A commit is classified from the files it touches.
|
|
30
|
+
* - **Diff view** — added/deleted lines per category over the whole range.
|
|
31
|
+
*
|
|
32
|
+
* Generated projections and lockfiles (`dist/`, `.augment/`, `.claude/`,
|
|
33
|
+
* `package-lock.json`, …) are removed from BOTH views by name. In this
|
|
34
|
+
* repository `dist/` is a byte-exact projection of `src/`, so leaving it in
|
|
35
|
+
* would let a mechanical regeneration dominate every reading. A commit left
|
|
36
|
+
* with no path after that exclusion lands in the `generated_only` diagnostic
|
|
37
|
+
* rather than in maintenance — otherwise the ratio would depend on whether the
|
|
38
|
+
* regeneration was committed separately from the source edit that caused it.
|
|
39
|
+
*
|
|
40
|
+
* Classification reads paths, never subjects.
|
|
41
|
+
*
|
|
42
|
+
* Commit subjects are mutable, inconsistently formatted, and can contradict the
|
|
43
|
+
* files a commit touches. Nothing in this file reads `%s`, `%b` or any other
|
|
44
|
+
* message field; the only git output consumed is a name list and a numstat.
|
|
45
|
+
* `tests/scripts/measure_release_mix.test.ts` pins that by rewriting a
|
|
46
|
+
* subject with `git commit --amend` and asserting the reading is byte-identical.
|
|
47
|
+
*
|
|
48
|
+
* `mixed` is never collapsed into a category.
|
|
49
|
+
*
|
|
50
|
+
* Collapsing `mixed` into product would recreate the exact gaming the declined
|
|
51
|
+
* rule permits. A commit touching two classified categories is `mixed` and is
|
|
52
|
+
* reported as its own bucket in both views.
|
|
53
|
+
*
|
|
54
|
+
* `unclassified` is separate from `mixed` and does not cause it. A commit whose
|
|
55
|
+
* paths are all unmatched is `unclassified`; a commit with one classified
|
|
56
|
+
* category plus unmatched paths keeps that category and carries
|
|
57
|
+
* `has_unclassified`. Conflating taxonomy uncertainty with genuine
|
|
58
|
+
* cross-category work would hide both.
|
|
59
|
+
*
|
|
60
|
+
* Usage.
|
|
61
|
+
*
|
|
62
|
+
* measure_release_mix --from <ref> --to <ref> [--label <tag>] [--json <path>] [--quiet]
|
|
63
|
+
* measure_release_mix --audit # every top-level tracked path resolves
|
|
64
|
+
*
|
|
65
|
+
* Exit codes: 0 clean · 1 an unresolved path in `--audit` · 2 usage or git error.
|
|
66
|
+
*/
|
|
67
|
+
import { spawnSync } from 'node:child_process';
|
|
68
|
+
import * as fs from 'node:fs';
|
|
69
|
+
import * as path from 'node:path';
|
|
70
|
+
import process from 'node:process';
|
|
71
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
72
|
+
|
|
73
|
+
const _HERE = path.resolve(fileURLToPath(import.meta.url));
|
|
74
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
75
|
+
const TAXONOMY_PATH = path.join(_HERE, '..', 'release_mix_taxonomy.json');
|
|
76
|
+
|
|
77
|
+
export type Category = 'consumer' | 'governance' | 'maintenance' | 'unclassified';
|
|
78
|
+
export const CLASSIFIED: readonly Category[] = ['consumer', 'governance', 'maintenance'];
|
|
79
|
+
|
|
80
|
+
export interface Taxonomy {
|
|
81
|
+
taxonomy_version: string;
|
|
82
|
+
excluded_generated: string[];
|
|
83
|
+
rules: { prefix: string; category: Category }[];
|
|
84
|
+
governance_contracts: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function loadTaxonomy(file = TAXONOMY_PATH): Taxonomy {
|
|
88
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Taxonomy;
|
|
89
|
+
if (!raw.taxonomy_version) throw new Error('taxonomy carries no taxonomy_version');
|
|
90
|
+
return raw;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True when the path is a generated projection or a lockfile — dropped from both views. */
|
|
94
|
+
export function isExcluded(p: string, tax: Taxonomy): boolean {
|
|
95
|
+
return tax.excluded_generated.some((pre) => (pre.endsWith('/') ? p.startsWith(pre) : p === pre));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Longest-prefix-wins. A path matching no rule is `unclassified` — a reported
|
|
100
|
+
* state, never a silent default into some category.
|
|
101
|
+
*/
|
|
102
|
+
export function classifyPath(p: string, tax: Taxonomy): Category {
|
|
103
|
+
return classifyPathDetailed(p, tax).category;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The same decision, with the rule that made it. `matched: false` is the
|
|
108
|
+
* failure `--audit` looks for: a path that fell through every rule, which is
|
|
109
|
+
* not the same thing as a path the taxonomy deliberately calls unclassified.
|
|
110
|
+
*/
|
|
111
|
+
export function classifyPathDetailed(
|
|
112
|
+
p: string,
|
|
113
|
+
tax: Taxonomy,
|
|
114
|
+
): { category: Category; matched: boolean; rule: string } {
|
|
115
|
+
if (tax.governance_contracts.includes(p)) {
|
|
116
|
+
return { category: 'governance', matched: true, rule: `governance_contracts:${p}` };
|
|
117
|
+
}
|
|
118
|
+
let best: { len: number; category: Category; rule: string } | null = null;
|
|
119
|
+
for (const rule of tax.rules) {
|
|
120
|
+
if (!p.startsWith(rule.prefix)) continue;
|
|
121
|
+
if (best === null || rule.prefix.length > best.len) {
|
|
122
|
+
best = { len: rule.prefix.length, category: rule.category, rule: rule.prefix };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return best === null
|
|
126
|
+
? { category: 'unclassified', matched: false, rule: '' }
|
|
127
|
+
: { category: best.category, matched: true, rule: best.rule };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface CommitVerdict {
|
|
131
|
+
/** The bucket the commit lands in. `generated_only` is a diagnostic, not a view bucket. */
|
|
132
|
+
bucket: Category | 'mixed' | 'generated_only';
|
|
133
|
+
categories: Category[];
|
|
134
|
+
has_unclassified: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Classify ONE commit from its changed paths. Nothing here reads the subject. */
|
|
138
|
+
export function classifyCommit(paths: readonly string[], tax: Taxonomy): CommitVerdict {
|
|
139
|
+
const kept = paths.filter((p) => !isExcluded(p, tax));
|
|
140
|
+
if (kept.length === 0) {
|
|
141
|
+
return { bucket: 'generated_only', categories: [], has_unclassified: false };
|
|
142
|
+
}
|
|
143
|
+
const seen = new Set<Category>();
|
|
144
|
+
for (const p of kept) seen.add(classifyPath(p, tax));
|
|
145
|
+
const has_unclassified = seen.has('unclassified');
|
|
146
|
+
const classified = [...seen].filter((c) => c !== 'unclassified').sort();
|
|
147
|
+
if (classified.length === 0) return { bucket: 'unclassified', categories: [], has_unclassified: true };
|
|
148
|
+
if (classified.length === 1) return { bucket: classified[0]!, categories: classified, has_unclassified };
|
|
149
|
+
return { bucket: 'mixed', categories: classified, has_unclassified };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function git(args: string[], cwd = REPO_ROOT): string {
|
|
153
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
154
|
+
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${(r.stderr || '').trim()}`);
|
|
155
|
+
return r.stdout;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface Reading {
|
|
159
|
+
schema_version: 1;
|
|
160
|
+
taxonomy_version: string;
|
|
161
|
+
classifier_version: string;
|
|
162
|
+
label: string;
|
|
163
|
+
range: { from: string; to: string; from_sha: string; to_sha: string };
|
|
164
|
+
merge_handling: 'no-merges';
|
|
165
|
+
rename_handling: 'git-default';
|
|
166
|
+
commit_view: Record<string, number>;
|
|
167
|
+
diff_view: Record<string, { added: number; deleted: number }>;
|
|
168
|
+
diagnostics: {
|
|
169
|
+
generated_only_commits: number;
|
|
170
|
+
commits_with_unclassified: number;
|
|
171
|
+
unclassified_paths: string[];
|
|
172
|
+
mixed_combinations: Record<string, number>;
|
|
173
|
+
largest_contributors: Record<string, [string, number][]>;
|
|
174
|
+
};
|
|
175
|
+
response_obligation: { triggered: boolean; rule: string; governance_only: number; consumer_only: number };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The classifier's own version, bumped independently of the taxonomy's. */
|
|
179
|
+
export const CLASSIFIER_VERSION = '1.0.0';
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The response obligation's trigger, stated once so the gate and the report
|
|
183
|
+
* cannot drift: governance-only commits STRICTLY outnumber consumer-only
|
|
184
|
+
* commits over the span. Strict inequality is the literal reading of the
|
|
185
|
+
* roadmap's own wording and both council seats named it explicitly.
|
|
186
|
+
*/
|
|
187
|
+
export const OBLIGATION_RULE = 'governance-only commits > consumer-only commits';
|
|
188
|
+
|
|
189
|
+
export function measureRange(from: string, to: string, tax: Taxonomy, label = '', cwd = REPO_ROOT): Reading {
|
|
190
|
+
const shas = git(['log', '--no-merges', '--format=%H', `${from}..${to}`], cwd)
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map((s) => s.trim())
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
|
|
195
|
+
const commit_view: Record<string, number> = {
|
|
196
|
+
consumer: 0, governance: 0, maintenance: 0, unclassified: 0, mixed: 0,
|
|
197
|
+
};
|
|
198
|
+
const mixed_combinations: Record<string, number> = {};
|
|
199
|
+
const unclassified_paths = new Set<string>();
|
|
200
|
+
let generated_only_commits = 0;
|
|
201
|
+
let commits_with_unclassified = 0;
|
|
202
|
+
|
|
203
|
+
for (const sha of shas) {
|
|
204
|
+
const paths = git(['show', '--name-only', '--format=', sha], cwd)
|
|
205
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
206
|
+
const v = classifyCommit(paths, tax);
|
|
207
|
+
if (v.bucket === 'generated_only') { generated_only_commits += 1; continue; }
|
|
208
|
+
commit_view[v.bucket] = (commit_view[v.bucket] ?? 0) + 1;
|
|
209
|
+
if (v.has_unclassified) commits_with_unclassified += 1;
|
|
210
|
+
if (v.bucket === 'mixed') {
|
|
211
|
+
const key = v.categories.join('+');
|
|
212
|
+
mixed_combinations[key] = (mixed_combinations[key] ?? 0) + 1;
|
|
213
|
+
}
|
|
214
|
+
for (const p of paths) {
|
|
215
|
+
if (!isExcluded(p, tax) && classifyPath(p, tax) === 'unclassified') unclassified_paths.add(p);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const diff_view: Record<string, { added: number; deleted: number }> = {
|
|
220
|
+
consumer: { added: 0, deleted: 0 },
|
|
221
|
+
governance: { added: 0, deleted: 0 },
|
|
222
|
+
maintenance: { added: 0, deleted: 0 },
|
|
223
|
+
unclassified: { added: 0, deleted: 0 },
|
|
224
|
+
};
|
|
225
|
+
const touchCount: Record<string, Record<string, number>> = {
|
|
226
|
+
consumer: {}, governance: {}, maintenance: {}, unclassified: {},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
for (const line of git(['diff', '--numstat', `${from}..${to}`], cwd).split('\n')) {
|
|
230
|
+
const parts = line.split('\t');
|
|
231
|
+
if (parts.length < 3) continue;
|
|
232
|
+
const [addRaw, delRaw, pathRaw] = parts as [string, string, string];
|
|
233
|
+
// `git diff --numstat` renders a rename as `old => new`; take the new path.
|
|
234
|
+
const p = pathRaw.includes('=>') ? pathRaw.replace(/^.*=>\s*/, '').replace(/[{}]/g, '') : pathRaw;
|
|
235
|
+
if (isExcluded(p, tax)) continue;
|
|
236
|
+
const cat = classifyPath(p, tax);
|
|
237
|
+
// A binary file renders as `-`; it contributes a touch, not a line count.
|
|
238
|
+
diff_view[cat]!.added += addRaw === '-' ? 0 : Number(addRaw);
|
|
239
|
+
diff_view[cat]!.deleted += delRaw === '-' ? 0 : Number(delRaw);
|
|
240
|
+
const top = p.split('/').slice(0, 2).join('/');
|
|
241
|
+
touchCount[cat]![top] = (touchCount[cat]![top] ?? 0) + 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const largest_contributors: Record<string, [string, number][]> = {};
|
|
245
|
+
for (const cat of Object.keys(touchCount)) {
|
|
246
|
+
largest_contributors[cat] = Object.entries(touchCount[cat]!)
|
|
247
|
+
.sort((a, b) => b[1] - a[1])
|
|
248
|
+
.slice(0, 5) as [string, number][];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const governance_only = commit_view.governance ?? 0;
|
|
252
|
+
const consumer_only = commit_view.consumer ?? 0;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
schema_version: 1,
|
|
256
|
+
taxonomy_version: tax.taxonomy_version,
|
|
257
|
+
classifier_version: CLASSIFIER_VERSION,
|
|
258
|
+
label: label || `${from}..${to}`,
|
|
259
|
+
range: {
|
|
260
|
+
from, to,
|
|
261
|
+
from_sha: git(['rev-parse', from], cwd).trim(),
|
|
262
|
+
to_sha: git(['rev-parse', to], cwd).trim(),
|
|
263
|
+
},
|
|
264
|
+
merge_handling: 'no-merges',
|
|
265
|
+
rename_handling: 'git-default',
|
|
266
|
+
commit_view,
|
|
267
|
+
diff_view,
|
|
268
|
+
diagnostics: {
|
|
269
|
+
generated_only_commits,
|
|
270
|
+
commits_with_unclassified,
|
|
271
|
+
unclassified_paths: [...unclassified_paths].sort().slice(0, 50),
|
|
272
|
+
mixed_combinations,
|
|
273
|
+
largest_contributors,
|
|
274
|
+
},
|
|
275
|
+
response_obligation: {
|
|
276
|
+
triggered: governance_only > consumer_only,
|
|
277
|
+
rule: OBLIGATION_RULE,
|
|
278
|
+
governance_only,
|
|
279
|
+
consumer_only,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function renderReading(r: Reading): string {
|
|
285
|
+
const cv = r.commit_view;
|
|
286
|
+
const lines: string[] = [];
|
|
287
|
+
lines.push(`release-mix · ${r.label} · taxonomy ${r.taxonomy_version} · classifier ${r.classifier_version}`);
|
|
288
|
+
lines.push(` range ${r.range.from_sha.slice(0, 9)}..${r.range.to_sha.slice(0, 9)} (${r.merge_handling})`);
|
|
289
|
+
lines.push(` commit view consumer ${cv.consumer} · governance ${cv.governance} · maintenance ${cv.maintenance} · mixed ${cv.mixed} · unclassified ${cv.unclassified}`);
|
|
290
|
+
for (const cat of ['consumer', 'governance', 'maintenance', 'unclassified'] as const) {
|
|
291
|
+
const d = r.diff_view[cat]!;
|
|
292
|
+
lines.push(` diff view ${cat.padEnd(13)} +${d.added} / -${d.deleted}`);
|
|
293
|
+
}
|
|
294
|
+
lines.push(` diagnostics generated-only commits ${r.diagnostics.generated_only_commits} · commits carrying unclassified paths ${r.diagnostics.commits_with_unclassified}`);
|
|
295
|
+
lines.push(` response owed ${r.response_obligation.triggered ? 'YES' : 'no'} (${r.response_obligation.rule}: ${r.response_obligation.governance_only} vs ${r.response_obligation.consumer_only})`);
|
|
296
|
+
lines.push(' NOTE a level, not a verdict — no threshold is committed to this repository.');
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* AC-2 — every tracked path resolves to exactly one category, or to a rule that
|
|
302
|
+
* says `unclassified` on purpose. A path that matches NO rule is the failure:
|
|
303
|
+
* it would otherwise be counted as taxonomy uncertainty when it is really a
|
|
304
|
+
* taxonomy hole. Reported grouped by the level at which the taxonomy actually
|
|
305
|
+
* decides, so `src/` shows its subdirectories rather than one aggregate row.
|
|
306
|
+
*/
|
|
307
|
+
export function auditTree(tax: Taxonomy, cwd = REPO_ROOT): {
|
|
308
|
+
rows: { unit: string; category: Category; files: number }[];
|
|
309
|
+
unmatched: string[];
|
|
310
|
+
} {
|
|
311
|
+
const byUnit = new Map<string, { category: Category; files: number }>();
|
|
312
|
+
const unmatched: string[] = [];
|
|
313
|
+
for (const f of git(['ls-files'], cwd).split('\n')) {
|
|
314
|
+
const file = f.trim();
|
|
315
|
+
if (!file || isExcluded(file, tax)) continue;
|
|
316
|
+
const d = classifyPathDetailed(file, tax);
|
|
317
|
+
if (!d.matched) {
|
|
318
|
+
unmatched.push(file);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const unit = d.rule.startsWith('governance_contracts:') ? 'docs/contracts/ (exception list)' : d.rule;
|
|
322
|
+
const cur = byUnit.get(unit);
|
|
323
|
+
if (cur) cur.files += 1;
|
|
324
|
+
else byUnit.set(unit, { category: d.category, files: 1 });
|
|
325
|
+
}
|
|
326
|
+
const rows = [...byUnit.entries()]
|
|
327
|
+
.map(([unit, v]) => ({ unit, category: v.category, files: v.files }))
|
|
328
|
+
.sort((a, b) => a.unit.localeCompare(b.unit));
|
|
329
|
+
return { rows, unmatched };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function usage(msg: string): never {
|
|
333
|
+
process.stderr.write(`measure_release_mix: ${msg}\n`);
|
|
334
|
+
process.stderr.write('usage: measure_release_mix --from <ref> --to <ref> [--label <tag>] [--json <path>] [--quiet]\n');
|
|
335
|
+
process.stderr.write(' measure_release_mix --audit\n');
|
|
336
|
+
process.exit(2);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function main(argv: readonly string[]): number {
|
|
340
|
+
const args = [...argv];
|
|
341
|
+
const flag = (name: string): string | undefined => {
|
|
342
|
+
const i = args.indexOf(name);
|
|
343
|
+
return i === -1 ? undefined : args[i + 1];
|
|
344
|
+
};
|
|
345
|
+
const tax = loadTaxonomy();
|
|
346
|
+
|
|
347
|
+
if (args.includes('--audit')) {
|
|
348
|
+
const { rows, unmatched } = auditTree(tax);
|
|
349
|
+
for (const r of rows) {
|
|
350
|
+
process.stdout.write(` ${r.category.padEnd(13)} ${String(r.files).padStart(5)} ${r.unit}\n`);
|
|
351
|
+
}
|
|
352
|
+
if (unmatched.length > 0) {
|
|
353
|
+
process.stderr.write(`\nmeasure_release_mix: ${unmatched.length} tracked path(s) match no rule:\n`);
|
|
354
|
+
for (const u of unmatched.slice(0, 20)) process.stderr.write(` ${u}\n`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
const files = rows.reduce((n, r) => n + r.files, 0);
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
`\nOK — ${files} tracked paths over ${rows.length} classification units, all resolved (taxonomy ${tax.taxonomy_version}).\n`,
|
|
360
|
+
);
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const from = flag('--from');
|
|
365
|
+
const to = flag('--to');
|
|
366
|
+
if (!from || !to) usage('--from and --to are required');
|
|
367
|
+
const reading = measureRange(from, to, tax, flag('--label') ?? '');
|
|
368
|
+
const jsonPath = flag('--json');
|
|
369
|
+
if (jsonPath) {
|
|
370
|
+
fs.mkdirSync(path.dirname(path.resolve(jsonPath)), { recursive: true });
|
|
371
|
+
fs.writeFileSync(path.resolve(jsonPath), `${JSON.stringify(reading, null, 2)}\n`, 'utf8');
|
|
372
|
+
}
|
|
373
|
+
if (!args.includes('--quiet')) process.stdout.write(`${renderReading(reading)}\n`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
378
|
+
try {
|
|
379
|
+
process.exit(main(process.argv.slice(2)));
|
|
380
|
+
} catch (e) {
|
|
381
|
+
process.stderr.write(`measure_release_mix: ${(e as Error).message}\n`);
|
|
382
|
+
process.exit(2);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"taxonomy_version": "1.0.0",
|
|
3
|
+
"updated": "2026-09-04",
|
|
4
|
+
"contract": "docs/decisions/ADR-253-per-pr-user-artifact-gate-declined.md",
|
|
5
|
+
"reader": "src/scripts/measure_release_mix.ts",
|
|
6
|
+
"note": [
|
|
7
|
+
"Path -> category mapping for the release governance-vs-product measurement.",
|
|
8
|
+
"Versioned so a later reading is comparable to an earlier one: a reading records",
|
|
9
|
+
"the taxonomy_version it was computed under, and a bump means prior readings are",
|
|
10
|
+
"recomputed rather than silently compared across two different rulers.",
|
|
11
|
+
"Matching is LONGEST-PREFIX-WINS over the repository-relative path. A path that",
|
|
12
|
+
"matches no rule is reported as `unclassified` -- a first-class state, never a",
|
|
13
|
+
"silent default. `unclassified` alone does not make a commit `mixed`.",
|
|
14
|
+
"The `consumer` label names a SHIPPED SURFACE, not delivered consumer value.",
|
|
15
|
+
"openai/codex-default asked for the label `shipped-product` on exactly that",
|
|
16
|
+
"ground and anthropic/claude-sonnet-4-5 preferred keeping `consumer` for",
|
|
17
|
+
"consistency with the roadmap's own wording; the roadmap's wording is kept and",
|
|
18
|
+
"the objection is recorded here rather than dropped."
|
|
19
|
+
],
|
|
20
|
+
"categories": {
|
|
21
|
+
"consumer": "Shipped surface an installing consumer receives or invokes: skills, rules, commands, guidelines, packs, the agent-config CLI runtime, and user-facing documentation.",
|
|
22
|
+
"governance": "This repository's own steering layer: roadmaps, evidence ledgers, verdicts, decision records, archive and promotion records, and governance-only contracts.",
|
|
23
|
+
"maintenance": "Repository infrastructure: CI gates, tests, build and release automation, packaging, dependencies, tooling configuration.",
|
|
24
|
+
"unclassified": "No rule matches, or the path is genuinely shared between categories and is left visible rather than assigned."
|
|
25
|
+
},
|
|
26
|
+
"excluded_generated": [
|
|
27
|
+
"dist/",
|
|
28
|
+
".augment/",
|
|
29
|
+
".claude/",
|
|
30
|
+
".cursor/",
|
|
31
|
+
".clinerules/",
|
|
32
|
+
".windsurfrules",
|
|
33
|
+
"GEMINI.md",
|
|
34
|
+
".claude-plugin/",
|
|
35
|
+
".augment-plugin/",
|
|
36
|
+
"package-lock.json",
|
|
37
|
+
"docs/decisions/INDEX.md",
|
|
38
|
+
"agents/roadmaps/archive/INDEX.md",
|
|
39
|
+
"agents/roadmaps/archive/index.json"
|
|
40
|
+
],
|
|
41
|
+
"excluded_note": [
|
|
42
|
+
"Generated projections and lockfiles are removed BEFORE a commit is classified,",
|
|
43
|
+
"and are excluded from both the commit view and the diff view. A commit left with",
|
|
44
|
+
"no path after the exclusion is counted in the `generated_only` diagnostic rather",
|
|
45
|
+
"than as maintenance -- otherwise a release's ratio would depend on whether the",
|
|
46
|
+
"regeneration was committed separately from the source edit that caused it."
|
|
47
|
+
],
|
|
48
|
+
"rules": [
|
|
49
|
+
{ "prefix": "src/skills/", "category": "consumer" },
|
|
50
|
+
{ "prefix": "src/rules/", "category": "consumer" },
|
|
51
|
+
{ "prefix": "src/agent-src/", "category": "consumer" },
|
|
52
|
+
{ "prefix": "src/domains/", "category": "consumer" },
|
|
53
|
+
{ "prefix": "src/packs/", "category": "consumer" },
|
|
54
|
+
{ "prefix": "src/profiles/", "category": "consumer" },
|
|
55
|
+
{ "prefix": "src/templates/", "category": "consumer" },
|
|
56
|
+
{ "prefix": "src/patterns/", "category": "consumer" },
|
|
57
|
+
{ "prefix": "src/subagents/", "category": "consumer" },
|
|
58
|
+
{ "prefix": "src/missions/", "category": "consumer" },
|
|
59
|
+
{ "prefix": "src/flows/", "category": "consumer" },
|
|
60
|
+
{ "prefix": "src/config/", "category": "consumer" },
|
|
61
|
+
{ "prefix": "src/cli/", "category": "consumer" },
|
|
62
|
+
{ "prefix": "src/install/", "category": "consumer" },
|
|
63
|
+
{ "prefix": "src/server/", "category": "consumer" },
|
|
64
|
+
{ "prefix": "src/ui/", "category": "consumer" },
|
|
65
|
+
{ "prefix": "src/shared/", "category": "unclassified" },
|
|
66
|
+
|
|
67
|
+
{ "prefix": "src/scripts/", "category": "maintenance" },
|
|
68
|
+
{ "prefix": "src/scripts/ai_council/", "category": "consumer" },
|
|
69
|
+
{ "prefix": "src/scripts/ai_team/", "category": "consumer" },
|
|
70
|
+
{ "prefix": "src/scripts/ai-video/", "category": "consumer" },
|
|
71
|
+
{ "prefix": "src/scripts/ai-image/", "category": "consumer" },
|
|
72
|
+
{ "prefix": "src/scripts/media/", "category": "consumer" },
|
|
73
|
+
{ "prefix": "src/scripts/hooks/", "category": "consumer" },
|
|
74
|
+
{ "prefix": "src/scripts/mcp_server/", "category": "consumer" },
|
|
75
|
+
{ "prefix": "src/scripts/skill_tools/", "category": "consumer" },
|
|
76
|
+
{ "prefix": "src/scripts/command_suggester/", "category": "consumer" },
|
|
77
|
+
{ "prefix": "src/scripts/code_graph/", "category": "consumer" },
|
|
78
|
+
{ "prefix": "src/scripts/prediction-pool/", "category": "consumer" },
|
|
79
|
+
{ "prefix": "src/scripts/cost/", "category": "consumer" },
|
|
80
|
+
|
|
81
|
+
{ "prefix": "agents/", "category": "governance" },
|
|
82
|
+
{ "prefix": "internal/", "category": "governance" },
|
|
83
|
+
{ "prefix": "provenance/", "category": "governance" },
|
|
84
|
+
|
|
85
|
+
{ "prefix": "docs/", "category": "consumer" },
|
|
86
|
+
{ "prefix": "docs/decisions/", "category": "governance" },
|
|
87
|
+
{ "prefix": "docs/adrs/", "category": "governance" },
|
|
88
|
+
{ "prefix": "docs/archive/", "category": "governance" },
|
|
89
|
+
{ "prefix": "docs/maintainers/", "category": "governance" },
|
|
90
|
+
{ "prefix": "docs/parity/", "category": "governance" },
|
|
91
|
+
{ "prefix": "docs/wedge/", "category": "governance" },
|
|
92
|
+
{ "prefix": "docs/experiences/", "category": "governance" },
|
|
93
|
+
{ "prefix": "docs/case-studies/", "category": "governance" },
|
|
94
|
+
{ "prefix": "docs/announcements/", "category": "governance" },
|
|
95
|
+
|
|
96
|
+
{ "prefix": "tests/", "category": "maintenance" },
|
|
97
|
+
{ "prefix": ".github/", "category": "maintenance" },
|
|
98
|
+
{ "prefix": "taskfiles/", "category": "maintenance" },
|
|
99
|
+
{ "prefix": "deploy/", "category": "maintenance" },
|
|
100
|
+
{ "prefix": "site/", "category": "maintenance" },
|
|
101
|
+
{ "prefix": "hooks/", "category": "consumer" },
|
|
102
|
+
{ "prefix": "user-types/", "category": "consumer" },
|
|
103
|
+
{ "prefix": "tokens/", "category": "consumer" },
|
|
104
|
+
{ "prefix": "LICENSES/", "category": "consumer" },
|
|
105
|
+
|
|
106
|
+
{ "prefix": "README.md", "category": "consumer" },
|
|
107
|
+
{ "prefix": "ONBOARDING.md", "category": "consumer" },
|
|
108
|
+
{ "prefix": "MIGRATION.md", "category": "consumer" },
|
|
109
|
+
{ "prefix": "BREAKING_CHANGES.md", "category": "consumer" },
|
|
110
|
+
{ "prefix": "SECURITY.md", "category": "consumer" },
|
|
111
|
+
{ "prefix": "LEGAL_NOTICE.md", "category": "consumer" },
|
|
112
|
+
{ "prefix": "CHANGELOG.md", "category": "consumer" },
|
|
113
|
+
{ "prefix": "CAPABILITIES.yaml", "category": "consumer" },
|
|
114
|
+
{ "prefix": "CREDITS.md", "category": "consumer" },
|
|
115
|
+
{ "prefix": "REUSE.toml", "category": "consumer" },
|
|
116
|
+
{ "prefix": "LICENSE", "category": "consumer" },
|
|
117
|
+
{ "prefix": "NOTICE", "category": "consumer" },
|
|
118
|
+
{ "prefix": "llms.txt", "category": "consumer" },
|
|
119
|
+
|
|
120
|
+
{ "prefix": "AGENTS.md", "category": "governance" },
|
|
121
|
+
{ "prefix": "CLAUDE.md", "category": "governance" },
|
|
122
|
+
{ "prefix": "CONTRIBUTING.md", "category": "governance" },
|
|
123
|
+
|
|
124
|
+
{ "prefix": "Taskfile.yml", "category": "maintenance" },
|
|
125
|
+
{ "prefix": "agent-config", "category": "maintenance" },
|
|
126
|
+
{ "prefix": "scripts-run", "category": "maintenance" },
|
|
127
|
+
{ "prefix": "setup.sh", "category": "maintenance" },
|
|
128
|
+
{ "prefix": "package.json", "category": "maintenance" },
|
|
129
|
+
{ "prefix": "eslint.config.js", "category": "maintenance" },
|
|
130
|
+
{ "prefix": "tsconfig", "category": "maintenance" },
|
|
131
|
+
{ "prefix": "vite.config.ts", "category": "maintenance" },
|
|
132
|
+
{ "prefix": "vitest.config.ts", "category": "maintenance" },
|
|
133
|
+
{ "prefix": "playwright.config.ts", "category": "maintenance" },
|
|
134
|
+
{ "prefix": "glama.json", "category": "maintenance" },
|
|
135
|
+
{ "prefix": "mcp.json", "category": "maintenance" },
|
|
136
|
+
{ "prefix": "allowlist_backup.json", "category": "maintenance" },
|
|
137
|
+
{ "prefix": ".augmentignore", "category": "maintenance" },
|
|
138
|
+
{ "prefix": ".dockerignore", "category": "maintenance" },
|
|
139
|
+
{ "prefix": ".gitattributes", "category": "maintenance" },
|
|
140
|
+
{ "prefix": ".gitignore", "category": "maintenance" },
|
|
141
|
+
{ "prefix": ".mailmap", "category": "maintenance" },
|
|
142
|
+
{ "prefix": ".npmignore", "category": "maintenance" },
|
|
143
|
+
{ "prefix": ".path-leak-allow", "category": "maintenance" },
|
|
144
|
+
{ "prefix": ".secret-allow", "category": "maintenance" }
|
|
145
|
+
],
|
|
146
|
+
"governance_contracts": [
|
|
147
|
+
"docs/contracts/adr-layout.md",
|
|
148
|
+
"docs/contracts/adr-architectural-consensus-mechanism.md",
|
|
149
|
+
"docs/contracts/ai-council-config.md",
|
|
150
|
+
"docs/contracts/adversarial-review-protocol.md",
|
|
151
|
+
"docs/contracts/branch-protection-policy.md",
|
|
152
|
+
"docs/contracts/ci-green-floor.md",
|
|
153
|
+
"docs/contracts/ci-cost-budget.md",
|
|
154
|
+
"docs/contracts/release-pr-gating.md",
|
|
155
|
+
"docs/contracts/release-sizing.md",
|
|
156
|
+
"docs/contracts/release-trunk-sync.md",
|
|
157
|
+
"docs/contracts/CHANGELOG-conventions.md",
|
|
158
|
+
"docs/contracts/evidence-artifact-types.md",
|
|
159
|
+
"docs/contracts/kernel-membership.md",
|
|
160
|
+
"docs/contracts/package-self-orientation.md"
|
|
161
|
+
],
|
|
162
|
+
"governance_contracts_note": [
|
|
163
|
+
"`docs/contracts/` defaults to `consumer` because most contracts on that page",
|
|
164
|
+
"describe behavior the package promises an installing consumer -- classifying the",
|
|
165
|
+
"whole directory as governance would systematically undercount shipped work.",
|
|
166
|
+
"The files listed above are the exceptions: contracts that govern this",
|
|
167
|
+
"repository's own process (decision recording, council, release cutting, CI",
|
|
168
|
+
"floors, evidence discipline) rather than promised behavior. They resolve to",
|
|
169
|
+
"`governance`. A contract absent from this list and absent from the tree is a",
|
|
170
|
+
"stale entry and is reported by `--audit`."
|
|
171
|
+
]
|
|
172
|
+
}
|