@event4u/agent-config 12.1.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 +151 -0
- package/README.md +1 -1
- 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/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/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/update_roadmap_progress.ts +17 -2
- 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 +4 -4
- 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/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- 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 +43 -22
- 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 +936 -346
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1192 -160
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/src/config/evaluator-budgets.json +5 -4
- package/src/config/gate-coverage.yml +33 -11
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- 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/skill_catalogue.ts +258 -5
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +21 -21
- 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 +107 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -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/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +28 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +5 -5
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_source_size_budget — a growth ratchet over oversized TypeScript sources.
|
|
4
|
+
*
|
|
5
|
+
* ## The gap
|
|
6
|
+
*
|
|
7
|
+
* Every other size budget in this suite guards prose. Rules carry a char cap
|
|
8
|
+
* (`lint_load_context.cap_for`). Skills carry a token budget class
|
|
9
|
+
* (`lint_token_budget_discipline`). The always-loaded set carries two budgets
|
|
10
|
+
* (`check_always_budget`). The on-demand depth layer carries a per-file char
|
|
11
|
+
* ceiling (`check_depth_budget`). The 1,063 hand-written `.ts` files this
|
|
12
|
+
* package ships carry **none** — which is where the largest artefacts sit:
|
|
13
|
+
* `install.ts` at 5,461 lines, `skill_linter.ts` at 4,742, `council_cli.ts` at
|
|
14
|
+
* 4,058.
|
|
15
|
+
*
|
|
16
|
+
* Fifteen files are over 1,500 lines. Nothing measured that, so nothing stopped
|
|
17
|
+
* it, and nothing would notice a sixteenth.
|
|
18
|
+
*
|
|
19
|
+
* ## Why a ratchet BEFORE a split — the ordering is the decision
|
|
20
|
+
*
|
|
21
|
+
* The obvious move is to split the god-files. Doing that first is how a refactor
|
|
22
|
+
* becomes unreviewable: a 5,461-line file broken into six is a diff nobody can
|
|
23
|
+
* read, with nothing asserting it improved anything. A ratchet freezes the
|
|
24
|
+
* number where it is, which makes every later split a **lowering commit** whose
|
|
25
|
+
* gain is visible in one integer. That ordering is the deliberate call carried
|
|
26
|
+
* from the roadmap this gate closes, not an implementation detail.
|
|
27
|
+
*
|
|
28
|
+
* ## What the ratchet counts, and why it is NOT a file count
|
|
29
|
+
*
|
|
30
|
+
* The metric is **total lines above the ceiling**, summed across the tree:
|
|
31
|
+
*
|
|
32
|
+
* Σ max(0, lines(f) − CEILING) for every .ts under src/
|
|
33
|
+
*
|
|
34
|
+
* A file *count* was the obvious shape and it is the weak one: `install.ts`
|
|
35
|
+
* could grow 5,461 → 9,000 and the count of over-ceiling files would not move,
|
|
36
|
+
* so the gate would stay green while the exact defect it exists for got worse.
|
|
37
|
+
* That objection was raised against the count shape and it is correct. Summing
|
|
38
|
+
* the excess costs the same one integer and closes it:
|
|
39
|
+
*
|
|
40
|
+
* - a listed file growing by one line raises the total → **regressed**, fails;
|
|
41
|
+
* - a new file over the ceiling raises the total → **regressed**, fails;
|
|
42
|
+
* - splitting any god-file lowers the total → **improved**, tighten the ratchet;
|
|
43
|
+
* - the 56-day expiry in `_lib/gate_baseline` fails a total that never drops,
|
|
44
|
+
* so "freeze it and forget it" is not a stable state.
|
|
45
|
+
*
|
|
46
|
+
* That last clause is the one that separates a control from a number that feels
|
|
47
|
+
* like progress. A ceiling with no forcing function would leave these fifteen
|
|
48
|
+
* files at these fifteen sizes indefinitely with the gate green the whole time.
|
|
49
|
+
*
|
|
50
|
+
* ## What the ceiling is — and what it is NOT
|
|
51
|
+
*
|
|
52
|
+
* 1,500 lines is a **growth ratchet, not a quality threshold**, and the same
|
|
53
|
+
* honesty its sibling `check_depth_budget` states applies here: nothing claims a
|
|
54
|
+
* 1,499-line module is comprehensible and a 1,501-line one is not. The number
|
|
55
|
+
* was chosen by measurement of this tree and of the ledger it produces — 15
|
|
56
|
+
* files over 1,500, versus 44 over 1,000 and 75 over 800. A ceiling that lists
|
|
57
|
+
* 75 files is an allowlist, and this repository's own rules call an allowlist
|
|
58
|
+
* past 20 entries evidence that the linter is wrong. 1,500 is the tightest
|
|
59
|
+
* ceiling whose ledger stays signal.
|
|
60
|
+
*
|
|
61
|
+
* Exit codes: 0 = at or under baseline, 1 = the excess grew (or the baseline
|
|
62
|
+
* went stale), 2 = usage.
|
|
63
|
+
*/
|
|
64
|
+
import * as fs from 'node:fs';
|
|
65
|
+
import * as os from 'node:os';
|
|
66
|
+
import * as path from 'node:path';
|
|
67
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
68
|
+
|
|
69
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
70
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
71
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
72
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
73
|
+
|
|
74
|
+
const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
const REAL_REPO_ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The ceiling, in lines.
|
|
79
|
+
*
|
|
80
|
+
* Lines rather than chars — unlike the prose layers, a source file's reviewable
|
|
81
|
+
* unit is the line, and a line count is what every reader, editor and diff
|
|
82
|
+
* already reports.
|
|
83
|
+
*/
|
|
84
|
+
export const SOURCE_CEILING_LINES = 1_500;
|
|
85
|
+
|
|
86
|
+
/** The root this gate walks. */
|
|
87
|
+
export const SOURCE_ROOTS = ['src'] as const;
|
|
88
|
+
|
|
89
|
+
export interface SourceFile {
|
|
90
|
+
readonly file: string;
|
|
91
|
+
readonly lines: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function* walk(dir: string): Generator<string> {
|
|
95
|
+
let entries: fs.Dirent[];
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
102
|
+
const full = path.join(dir, e.name);
|
|
103
|
+
if (e.isDirectory()) {
|
|
104
|
+
if (e.name === 'node_modules') continue;
|
|
105
|
+
yield* walk(full);
|
|
106
|
+
} else if (e.isFile() && e.name.endsWith('.ts')) {
|
|
107
|
+
// Same two exclusions this repository's own gate-population
|
|
108
|
+
// classifier makes (`_lib/gate_population.ts`), for the same stated
|
|
109
|
+
// reasons: a type declaration has no runtime behaviour, and a test
|
|
110
|
+
// file is not the reviewable code this ceiling is aimed at. Latent
|
|
111
|
+
// rather than cosmetic — a GENERATED `.d.ts` over the ceiling would
|
|
112
|
+
// otherwise red a ratchet that exists to bound hand-written code.
|
|
113
|
+
if (e.name.endsWith('.d.ts') || e.name.endsWith('.test.ts')) continue;
|
|
114
|
+
yield full;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function collectSourceFiles(repoRoot: string): string[] {
|
|
120
|
+
const out: string[] = [];
|
|
121
|
+
for (const root of SOURCE_ROOTS) {
|
|
122
|
+
for (const f of walk(path.join(repoRoot, root))) {
|
|
123
|
+
out.push(f);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...new Set(out)].sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Count lines the way `wc -l` does — newline characters.
|
|
131
|
+
*
|
|
132
|
+
* Stated rather than left implicit: a file with no trailing newline reads one
|
|
133
|
+
* lower than an editor would show. Every file in this tree ends with one, and
|
|
134
|
+
* pinning the convention to `wc -l` means a contributor can reproduce the
|
|
135
|
+
* gate's number with a command they already know.
|
|
136
|
+
*/
|
|
137
|
+
export function countLines(text: string): number {
|
|
138
|
+
let n = 0;
|
|
139
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
140
|
+
if (text[i] === '\n') n += 1;
|
|
141
|
+
}
|
|
142
|
+
return n;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function measure(repoRoot: string, files: readonly string[]): SourceFile[] {
|
|
146
|
+
const out: SourceFile[] = [];
|
|
147
|
+
for (const f of files) {
|
|
148
|
+
// Deliberately NOT wrapped in a try/catch that `continue`s. A file this
|
|
149
|
+
// walker just enumerated and cannot read is a real problem, and a bare
|
|
150
|
+
// `continue` is the silent skip `_lib/gate_ledger` exists to catch: the
|
|
151
|
+
// target is already planned, so swallowing the error only defers the
|
|
152
|
+
// failure into an `UnaccountedTargetsError` naming a file whose read
|
|
153
|
+
// nobody can connect to it. Letting the read throw names the file and
|
|
154
|
+
// the reason. The ledger's skip vocabulary carries no code for
|
|
155
|
+
// "unreadable", and widening that shared union is not this change.
|
|
156
|
+
const text = fs.readFileSync(f, 'utf-8');
|
|
157
|
+
out.push({ file: path.relative(repoRoot, f).split(path.sep).join('/'), lines: countLines(text) });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Lines a single file carries above the ceiling; 0 when it is within budget. */
|
|
163
|
+
export function excessOf(file: SourceFile, ceiling: number = SOURCE_CEILING_LINES): number {
|
|
164
|
+
return Math.max(0, file.lines - ceiling);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The ratchet value: total lines above the ceiling across the whole tree. */
|
|
168
|
+
export function totalExcess(
|
|
169
|
+
files: readonly SourceFile[],
|
|
170
|
+
ceiling: number = SOURCE_CEILING_LINES,
|
|
171
|
+
): number {
|
|
172
|
+
let sum = 0;
|
|
173
|
+
for (const f of files) sum += excessOf(f, ceiling);
|
|
174
|
+
return sum;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface Args {
|
|
178
|
+
readonly json: boolean;
|
|
179
|
+
readonly quiet: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseArgs(argv: readonly string[]): Args {
|
|
183
|
+
let json = false;
|
|
184
|
+
let quiet = false;
|
|
185
|
+
for (const arg of argv) {
|
|
186
|
+
if (arg === '--json') { json = true; }
|
|
187
|
+
else if (arg === '--quiet') { quiet = true; }
|
|
188
|
+
else if (arg === '-h' || arg === '--help') {
|
|
189
|
+
process.stdout.write('usage: check_source_size_budget [--json] [--quiet]\n');
|
|
190
|
+
process.exit(0);
|
|
191
|
+
} else {
|
|
192
|
+
process.stderr.write(`check_source_size_budget: unrecognized argument: ${arg}\n`);
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { json, quiet };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function selfTest(): number {
|
|
200
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cssb-selftest-'));
|
|
201
|
+
const mkRepo = (bodies: Record<string, number>, baseline?: number): string => {
|
|
202
|
+
const root = fs.mkdtempSync(path.join(tmp, 'repo-'));
|
|
203
|
+
for (const [rel, lines] of Object.entries(bodies)) {
|
|
204
|
+
const p = path.join(root, rel);
|
|
205
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
206
|
+
fs.writeFileSync(p, '//\n'.repeat(lines), 'utf-8');
|
|
207
|
+
}
|
|
208
|
+
const cfg = path.join(root, 'src', 'config');
|
|
209
|
+
fs.mkdirSync(cfg, { recursive: true });
|
|
210
|
+
// Absent baseline ⇒ any excess is a violation, which is the
|
|
211
|
+
// discrimination most cases need. A supplied baseline is how the
|
|
212
|
+
// per-file-growth case proves the metric is not a file count.
|
|
213
|
+
const gates =
|
|
214
|
+
baseline === undefined
|
|
215
|
+
? {}
|
|
216
|
+
: {
|
|
217
|
+
check_source_size_budget: {
|
|
218
|
+
count: baseline,
|
|
219
|
+
landed: new Date().toISOString().slice(0, 10),
|
|
220
|
+
note: 'self-test fixture',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
path.join(cfg, 'gate-violation-baselines.json'),
|
|
225
|
+
JSON.stringify({ gates }),
|
|
226
|
+
'utf-8',
|
|
227
|
+
);
|
|
228
|
+
return root;
|
|
229
|
+
};
|
|
230
|
+
const run = (root: string): number => {
|
|
231
|
+
process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'] = root;
|
|
232
|
+
try {
|
|
233
|
+
return runGateCli(
|
|
234
|
+
REAL_REPO_ROOT,
|
|
235
|
+
'src/scripts/check_source_size_budget.ts',
|
|
236
|
+
['--quiet'],
|
|
237
|
+
root,
|
|
238
|
+
);
|
|
239
|
+
} finally {
|
|
240
|
+
delete process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'];
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const over = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 1 });
|
|
246
|
+
const under = mkRepo({ 'src/ok.ts': SOURCE_CEILING_LINES - 1 });
|
|
247
|
+
const exact = mkRepo({ 'src/edge.ts': SOURCE_CEILING_LINES });
|
|
248
|
+
const empty = mkRepo({ 'README.md': 10 });
|
|
249
|
+
// The discriminator against a file-count ratchet: ONE over-ceiling file
|
|
250
|
+
// in both trees, so a count would read 1 = 1 and pass. The excess grew
|
|
251
|
+
// by 100 lines, so this metric fails.
|
|
252
|
+
const grown = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 200 }, 100);
|
|
253
|
+
const shrunk = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 50 }, 100);
|
|
254
|
+
return runSelfTest({
|
|
255
|
+
gate: 'check_source_size_budget',
|
|
256
|
+
minCases: 4,
|
|
257
|
+
minRejectCases: 3,
|
|
258
|
+
cases: [
|
|
259
|
+
{
|
|
260
|
+
name: 'a file one line over the ceiling is rejected',
|
|
261
|
+
expect: 'reject',
|
|
262
|
+
run: () => run(over),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'a file one line under the ceiling passes',
|
|
266
|
+
expect: 'accept',
|
|
267
|
+
run: () => run(under),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'the ceiling itself is inclusive — exactly at the limit passes',
|
|
271
|
+
expect: 'accept',
|
|
272
|
+
run: () => run(exact),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'a dead scan root is rejected, not reported as clean',
|
|
276
|
+
expect: 'reject',
|
|
277
|
+
run: () => run(empty),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'an existing over-ceiling file that GREW is rejected — the metric is excess lines, not a file count',
|
|
281
|
+
expect: 'reject',
|
|
282
|
+
run: () => run(grown),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'an over-ceiling file that shrank passes and asks for a tighter ratchet',
|
|
286
|
+
expect: 'accept',
|
|
287
|
+
run: () => run(shrunk),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
} finally {
|
|
292
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function main(argv?: readonly string[]): number {
|
|
297
|
+
const raw = argv ?? process.argv.slice(2);
|
|
298
|
+
if (raw.includes('--self-test')) {
|
|
299
|
+
return selfTest();
|
|
300
|
+
}
|
|
301
|
+
const args = parseArgs(raw);
|
|
302
|
+
// Root derived from this file's own location, so the gate is robust to the
|
|
303
|
+
// cwd a wrapper or hook invokes it from. The env override exists so the
|
|
304
|
+
// self-test can drive the REAL binary against a synthetic tree; deliberately
|
|
305
|
+
// NOT a cwd fallback, because a "fall back to the real repo when the given
|
|
306
|
+
// root looks empty" rule would make the dead-scan-root case silently pass.
|
|
307
|
+
const root = process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'] ?? REAL_REPO_ROOT;
|
|
308
|
+
|
|
309
|
+
const files = collectSourceFiles(root);
|
|
310
|
+
const ledger = new GateLedger('check_source_size_budget');
|
|
311
|
+
ledger.plan(files.map((f) => path.relative(root, f).split(path.sep).join('/')));
|
|
312
|
+
|
|
313
|
+
const measured = measure(root, files);
|
|
314
|
+
const over = measured
|
|
315
|
+
.filter((m) => excessOf(m) > 0)
|
|
316
|
+
.sort((a, b) => b.lines - a.lines);
|
|
317
|
+
const overSet = new Set(over.map((o) => o.file));
|
|
318
|
+
for (const m of measured) {
|
|
319
|
+
if (overSet.has(m.file)) {
|
|
320
|
+
ledger.fail(
|
|
321
|
+
m.file,
|
|
322
|
+
`${String(m.lines)} lines, ${String(excessOf(m))} over ceiling ${String(SOURCE_CEILING_LINES)}`,
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
ledger.complete(m.file);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const tally = ledger.finalize();
|
|
329
|
+
const excess = totalExcess(measured);
|
|
330
|
+
|
|
331
|
+
// `--json` must emit JSON and nothing else, so a caller can pipe it to a
|
|
332
|
+
// JSON reader. Both of the calls below normally append prose to the same
|
|
333
|
+
// stdout, which would make the document unparseable — so under `--json` the
|
|
334
|
+
// scan report is routed to a no-op writer (the `assertScanned` dead-root
|
|
335
|
+
// check inside it still runs, it just does not print) and the ratchet
|
|
336
|
+
// verdict is carried as a FIELD rather than as a trailing paragraph.
|
|
337
|
+
if (args.json) {
|
|
338
|
+
reportScanned(
|
|
339
|
+
{
|
|
340
|
+
gate: 'check_source_size_budget',
|
|
341
|
+
scanned: measured.length,
|
|
342
|
+
units: 'source file(s)',
|
|
343
|
+
roots: [...SOURCE_ROOTS],
|
|
344
|
+
},
|
|
345
|
+
() => true,
|
|
346
|
+
);
|
|
347
|
+
const verdict = checkRatchet({
|
|
348
|
+
gate: 'check_source_size_budget',
|
|
349
|
+
actual: excess,
|
|
350
|
+
repoRoot: root,
|
|
351
|
+
});
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
`${JSON.stringify(
|
|
354
|
+
{
|
|
355
|
+
version: 1,
|
|
356
|
+
ceiling: SOURCE_CEILING_LINES,
|
|
357
|
+
excess_lines: excess,
|
|
358
|
+
over: over.map((o) => ({ ...o, excess: excessOf(o) })),
|
|
359
|
+
scanned: measured.length,
|
|
360
|
+
ledger: tally,
|
|
361
|
+
verdict: {
|
|
362
|
+
status: verdict.status,
|
|
363
|
+
ok: verdict.ok,
|
|
364
|
+
baseline: verdict.baseline,
|
|
365
|
+
message: verdict.message,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
null,
|
|
369
|
+
2,
|
|
370
|
+
)}\n`,
|
|
371
|
+
);
|
|
372
|
+
return verdict.ok ? 0 : 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!args.quiet && over.length > 0) {
|
|
376
|
+
process.stdout.write(
|
|
377
|
+
`\nOver the ${String(SOURCE_CEILING_LINES)}-line source ceiling ` +
|
|
378
|
+
'(a growth ratchet, NOT a measured quality threshold):\n',
|
|
379
|
+
);
|
|
380
|
+
for (const o of over) {
|
|
381
|
+
process.stdout.write(
|
|
382
|
+
` ${String(o.lines).padStart(6, ' ')} (+${String(excessOf(o)).padStart(5, ' ')}) ${o.file}\n`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write(` ${'—'.repeat(6)}\n total excess: ${String(excess)} line(s)\n`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
reportScanned({
|
|
389
|
+
gate: 'check_source_size_budget',
|
|
390
|
+
scanned: measured.length,
|
|
391
|
+
units: 'source file(s)',
|
|
392
|
+
roots: [...SOURCE_ROOTS],
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const verdict = checkRatchet({
|
|
396
|
+
gate: 'check_source_size_budget',
|
|
397
|
+
actual: excess,
|
|
398
|
+
repoRoot: root,
|
|
399
|
+
});
|
|
400
|
+
if (verdict.ok) {
|
|
401
|
+
process.stdout.write(`\n⚠️ ${verdict.message}\n`);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
process.stderr.write(`\n❌ ${verdict.message}\n`);
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
409
|
+
function _isCliEntry(): boolean {
|
|
410
|
+
if (process.argv[1] === undefined) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
return (
|
|
418
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
419
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
420
|
+
);
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
427
|
+
process.exit(main());
|
|
428
|
+
}
|
|
@@ -238,7 +238,11 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
|
|
|
238
238
|
listKey: 'entries',
|
|
239
239
|
tier: 'object',
|
|
240
240
|
keyFields: ['slug'],
|
|
241
|
-
newInThisChange:
|
|
241
|
+
// `newInThisChange` removed: the baseline now resolves at the base ref,
|
|
242
|
+
// so the bootstrap window this flag exists for has closed. That is the
|
|
243
|
+
// self-closing contract below working as designed — the gate reported
|
|
244
|
+
// the stale flag rather than leaving it to silently accept a future
|
|
245
|
+
// mistyped path as a fresh baseline.
|
|
242
246
|
what: 'review prompts whose recorded prompt_hash does not re-derive',
|
|
243
247
|
},
|
|
244
248
|
];
|