@nimiplatform/nimi-coding 0.5.0 → 0.6.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/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +50 -25
- package/README.zh-CN.md +50 -25
- package/cli/commands/clear.mjs +3 -2
- package/cli/commands/code.mjs +225 -0
- package/cli/commands/start.mjs +3 -2
- package/cli/constants.mjs +2 -1
- package/cli/dev/run-cli-smoke.mjs +6 -0
- package/cli/help.mjs +8 -3
- package/cli/index.mjs +3 -2
- package/cli/lib/authority/diagnostics.mjs +3 -1
- package/cli/lib/code/authority-annotation-response.mjs +43 -0
- package/cli/lib/code/authority-annotation-scan.mjs +162 -0
- package/cli/lib/code/authority-annotations.mjs +293 -0
- package/cli/lib/code/context.mjs +710 -0
- package/cli/lib/code/response.mjs +165 -0
- package/cli/lib/doctor.mjs +3 -2
- package/cli/lib/entrypoints.mjs +12 -5
- package/cli/seeds/seed-policy.yaml +2 -2
- package/methodology/authority-authoring.yaml +67 -52
- package/methodology/core.yaml +6 -4
- package/package.json +5 -3
- package/spec/code-authority-annotations.yaml +42 -0
- package/spec/code-context.yaml +38 -0
- package/spec/product-scope.yaml +8 -3
- package/cli/commands/validate-ai-governance.mjs +0 -335
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
- package/cli/lib/internal/governance/config.mjs +0 -114
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { localize } from "../lib/ui.mjs";
|
|
2
|
-
import { loadGovernanceConfig, requireProfile } from "../lib/internal/governance/config.mjs";
|
|
3
|
-
import { evaluateAiContextBudget, formatBytes } from "../lib/internal/governance/ai/ai-context-budget-core.mjs";
|
|
4
|
-
import { evaluateAiStructureBudget } from "../lib/internal/governance/ai/ai-structure-budget-core.mjs";
|
|
5
|
-
import { evaluateHighRiskDocMetadata } from "../lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs";
|
|
6
|
-
import {
|
|
7
|
-
evaluateAgentsFreshnessCheck,
|
|
8
|
-
runAgentsFreshnessCheck,
|
|
9
|
-
} from "../lib/internal/governance/ai/check-agents-freshness.mjs";
|
|
10
|
-
|
|
11
|
-
const SCOPES = new Set([
|
|
12
|
-
"agents-freshness",
|
|
13
|
-
"context-budget",
|
|
14
|
-
"structure-budget",
|
|
15
|
-
"high-risk-doc-metadata",
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
function parseOptions(args) {
|
|
19
|
-
const options = {
|
|
20
|
-
profile: null,
|
|
21
|
-
scope: "all",
|
|
22
|
-
json: false,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
26
|
-
const arg = args[index];
|
|
27
|
-
|
|
28
|
-
if (arg === "--profile") {
|
|
29
|
-
const value = args[index + 1];
|
|
30
|
-
if (!value || value.startsWith("--")) {
|
|
31
|
-
return { ok: false, error: "nimicoding validate-ai-governance refused: --profile requires a value.\n" };
|
|
32
|
-
}
|
|
33
|
-
options.profile = value;
|
|
34
|
-
index += 1;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (arg === "--scope") {
|
|
39
|
-
const value = args[index + 1];
|
|
40
|
-
if (!value || value.startsWith("--")) {
|
|
41
|
-
return { ok: false, error: "nimicoding validate-ai-governance refused: --scope requires a value.\n" };
|
|
42
|
-
}
|
|
43
|
-
options.scope = value;
|
|
44
|
-
index += 1;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (arg === "--json") {
|
|
49
|
-
options.json = true;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
error: `nimicoding validate-ai-governance refused: unknown option ${arg}.\n`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (options.scope !== "all" && !SCOPES.has(options.scope)) {
|
|
60
|
-
return {
|
|
61
|
-
ok: false,
|
|
62
|
-
error: `nimicoding validate-ai-governance refused: unsupported --scope value ${options.scope}.\n`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return { ok: true, options };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function writeJson(payload) {
|
|
70
|
-
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function buildContextBudgetReport(governanceConfig) {
|
|
74
|
-
const report = evaluateAiContextBudget({
|
|
75
|
-
cwd: process.cwd(),
|
|
76
|
-
config: governanceConfig.aiGovernance.contextBudget,
|
|
77
|
-
configPathLabel: ".nimi/config/governance.yaml#ai_governance.context_budget",
|
|
78
|
-
});
|
|
79
|
-
const exitCode = report.invalidWaivers.length > 0 || report.expiredWaivers.length > 0 || report.errors.length > 0
|
|
80
|
-
? 1
|
|
81
|
-
: 0;
|
|
82
|
-
return { exitCode, report };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function buildStructureBudgetReport(governanceConfig) {
|
|
86
|
-
const report = evaluateAiStructureBudget({
|
|
87
|
-
cwd: process.cwd(),
|
|
88
|
-
config: governanceConfig.aiGovernance.structureBudget,
|
|
89
|
-
configPathLabel: ".nimi/config/governance.yaml#ai_governance.structure_budget",
|
|
90
|
-
});
|
|
91
|
-
const exitCode = report.errors.length > 0 || report.expiredWaivers.length > 0 ? 1 : 0;
|
|
92
|
-
return { exitCode, report };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildHighRiskDocMetadataReport(governanceConfig) {
|
|
96
|
-
const config = governanceConfig.aiGovernance.highRiskDocMetadata;
|
|
97
|
-
const report = evaluateHighRiskDocMetadata({
|
|
98
|
-
repoRoot: process.cwd(),
|
|
99
|
-
docRoots: Array.isArray(config.doc_roots) ? config.doc_roots : [".local"],
|
|
100
|
-
exemptPaths: Array.isArray(config.exempt_paths) ? config.exempt_paths : [],
|
|
101
|
-
namePatterns: Array.isArray(config.name_patterns) ? config.name_patterns : [],
|
|
102
|
-
requiredMetadataKeys: Array.isArray(config.required_metadata_keys)
|
|
103
|
-
? config.required_metadata_keys
|
|
104
|
-
: [],
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
exitCode: report.failures.length > 0 ? 1 : 0,
|
|
108
|
-
report: {
|
|
109
|
-
...report,
|
|
110
|
-
exemptPaths: [...report.exemptPaths],
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function buildAgentsFreshnessReport(governanceConfig) {
|
|
116
|
-
const report = evaluateAgentsFreshnessCheck({
|
|
117
|
-
projectRoot: process.cwd(),
|
|
118
|
-
config: governanceConfig.aiGovernance.agentsFreshness,
|
|
119
|
-
});
|
|
120
|
-
return {
|
|
121
|
-
exitCode: report.failures.length > 0 ? 1 : 0,
|
|
122
|
-
report,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatStructureRow(row) {
|
|
127
|
-
if (row.check === "depth") {
|
|
128
|
-
return `${row.file} [rule=${row.ruleId}] depth=${row.depth} base=${row.depthBase} subject=${row.depthSubject} (threshold warn>=${row.warningDepth} error>=${row.errorDepth})`;
|
|
129
|
-
}
|
|
130
|
-
return `${row.file} [rule=${row.ruleId}] basename=${row.basename} (forwarding shell outside allowed basename set)`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function formatContextBudgetRow(row, thresholdPrefix) {
|
|
134
|
-
return `${row.file} [${row.profile}] lines=${row.lines} bytes=${formatBytes(row.bytes)} max-line=${formatBytes(row.maxLineBytes)} avg-line=${formatBytes(Math.round(row.averageLineBytes))} `
|
|
135
|
-
+ `(${thresholdPrefix} lines>=${row[`${thresholdPrefix}Lines`] ?? "-"} bytes>=${row[`${thresholdPrefix}Bytes`] ?? "-"} max-line>=${row[`${thresholdPrefix}MaxLineBytes`] ?? "-"} avg-line>=${row[`${thresholdPrefix}AverageLineBytes`] ?? "-"})`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function runContextBudget(governanceConfig) {
|
|
139
|
-
const { exitCode, report } = buildContextBudgetReport(governanceConfig);
|
|
140
|
-
|
|
141
|
-
process.stdout.write(`ai-context-budget: config=${report.configPath}\n`);
|
|
142
|
-
process.stdout.write(`ai-context-budget: tracked=${report.totalTrackedFiles}, analyzed=${report.analyzedFiles}\n`);
|
|
143
|
-
|
|
144
|
-
for (const row of report.warnings) {
|
|
145
|
-
process.stderr.write(`WARN: ${formatContextBudgetRow(row, "warning")}\n`);
|
|
146
|
-
}
|
|
147
|
-
for (const row of report.waivedErrors) {
|
|
148
|
-
const until = row.waiver?.until ? row.waiver.until.toISOString().slice(0, 10) : "n/a";
|
|
149
|
-
const reason = row.waiver?.reason || "no reason";
|
|
150
|
-
process.stderr.write(`WARN: WAIVED error for ${formatContextBudgetRow(row, "error")} until=${until} reason=${reason}\n`);
|
|
151
|
-
}
|
|
152
|
-
for (const row of report.expiredWaivers) {
|
|
153
|
-
process.stderr.write(`ERROR: waiver expired for ${formatContextBudgetRow(row, "error")}\n`);
|
|
154
|
-
}
|
|
155
|
-
for (const row of report.invalidWaivers) {
|
|
156
|
-
process.stderr.write(`ERROR: invalid waiver for ${row.file}: ${row.detail}\n`);
|
|
157
|
-
}
|
|
158
|
-
for (const row of report.errors) {
|
|
159
|
-
process.stderr.write(`ERROR: ${formatContextBudgetRow(row, "error")}\n`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (exitCode !== 0) {
|
|
163
|
-
return exitCode;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
process.stdout.write("ai-context-budget: OK\n");
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function runStructureBudget(governanceConfig) {
|
|
171
|
-
const { exitCode, report } = buildStructureBudgetReport(governanceConfig);
|
|
172
|
-
|
|
173
|
-
process.stdout.write(`ai-structure-budget: config=${report.configPath}\n`);
|
|
174
|
-
process.stdout.write(`ai-structure-budget: tracked=${report.totalTrackedFiles}, analyzed=${report.analyzedFiles}\n`);
|
|
175
|
-
for (const row of report.warnings) {
|
|
176
|
-
process.stderr.write(`WARN: ${formatStructureRow(row)}\n`);
|
|
177
|
-
}
|
|
178
|
-
for (const row of report.waivedErrors) {
|
|
179
|
-
const until = row.waiver?.untilDate ? row.waiver.untilDate.toISOString().slice(0, 10) : "n/a";
|
|
180
|
-
const reason = row.waiver?.reason || "no reason";
|
|
181
|
-
process.stderr.write(`WARN: WAIVED error for ${formatStructureRow(row)} until=${until} reason=${reason}\n`);
|
|
182
|
-
}
|
|
183
|
-
for (const row of report.expiredWaivers) {
|
|
184
|
-
process.stderr.write(`ERROR: expired waiver for ${formatStructureRow(row)}\n`);
|
|
185
|
-
}
|
|
186
|
-
for (const row of report.errors) {
|
|
187
|
-
process.stderr.write(`ERROR: ${formatStructureRow(row)}\n`);
|
|
188
|
-
}
|
|
189
|
-
if (exitCode !== 0) {
|
|
190
|
-
return exitCode;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
process.stdout.write("ai-structure-budget: OK\n");
|
|
194
|
-
return 0;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function runHighRiskDocMetadata(governanceConfig) {
|
|
198
|
-
const { exitCode, report } = buildHighRiskDocMetadataReport(governanceConfig);
|
|
199
|
-
|
|
200
|
-
if (exitCode !== 0) {
|
|
201
|
-
process.stderr.write("high-risk doc metadata check failed:\n");
|
|
202
|
-
for (const failure of report.failures) {
|
|
203
|
-
process.stderr.write(`- ${failure}\n`);
|
|
204
|
-
}
|
|
205
|
-
return exitCode;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
process.stdout.write(`high-risk doc metadata check passed (${report.scanned.length} file(s) scanned)\n`);
|
|
209
|
-
return 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function runJsonScope(governanceConfig, scope) {
|
|
213
|
-
if (scope === "agents-freshness") {
|
|
214
|
-
return buildAgentsFreshnessReport(governanceConfig);
|
|
215
|
-
}
|
|
216
|
-
if (scope === "context-budget") {
|
|
217
|
-
return buildContextBudgetReport(governanceConfig);
|
|
218
|
-
}
|
|
219
|
-
if (scope === "structure-budget") {
|
|
220
|
-
return buildStructureBudgetReport(governanceConfig);
|
|
221
|
-
}
|
|
222
|
-
if (scope === "high-risk-doc-metadata") {
|
|
223
|
-
return buildHighRiskDocMetadataReport(governanceConfig);
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
exitCode: 2,
|
|
227
|
-
report: { error: `unsupported scope: ${scope}` },
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function runValidateAiGovernance(args) {
|
|
232
|
-
const wantsJson = args.includes("--json");
|
|
233
|
-
const parsed = parseOptions(args);
|
|
234
|
-
if (!parsed.ok) {
|
|
235
|
-
if (wantsJson) {
|
|
236
|
-
writeJson({
|
|
237
|
-
ok: false,
|
|
238
|
-
command: "validate-ai-governance",
|
|
239
|
-
error: parsed.error.trim(),
|
|
240
|
-
});
|
|
241
|
-
} else {
|
|
242
|
-
process.stderr.write(localize(parsed.error, parsed.error));
|
|
243
|
-
}
|
|
244
|
-
return 2;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const governance = await loadGovernanceConfig(process.cwd());
|
|
248
|
-
if (!governance.ok) {
|
|
249
|
-
const error = `nimicoding validate-ai-governance refused: ${governance.reason} at ${governance.path}.`;
|
|
250
|
-
if (parsed.options.json) {
|
|
251
|
-
writeJson({
|
|
252
|
-
ok: false,
|
|
253
|
-
command: "validate-ai-governance",
|
|
254
|
-
error,
|
|
255
|
-
governancePath: governance.path,
|
|
256
|
-
});
|
|
257
|
-
} else {
|
|
258
|
-
process.stderr.write(localize(
|
|
259
|
-
`${error}\n`,
|
|
260
|
-
`nimicoding validate-ai-governance 已拒绝:${governance.path} 的治理配置不可用。\n`,
|
|
261
|
-
));
|
|
262
|
-
}
|
|
263
|
-
return 2;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const profileCheck = requireProfile(governance.config, parsed.options.profile);
|
|
267
|
-
if (!profileCheck.ok) {
|
|
268
|
-
const error = `nimicoding validate-ai-governance refused: ${profileCheck.error}.`;
|
|
269
|
-
if (parsed.options.json) {
|
|
270
|
-
writeJson({
|
|
271
|
-
ok: false,
|
|
272
|
-
command: "validate-ai-governance",
|
|
273
|
-
error,
|
|
274
|
-
profile: profileCheck.profile,
|
|
275
|
-
});
|
|
276
|
-
} else {
|
|
277
|
-
process.stderr.write(localize(
|
|
278
|
-
`${error}\n`,
|
|
279
|
-
`nimicoding validate-ai-governance 已拒绝:${profileCheck.error}。\n`,
|
|
280
|
-
));
|
|
281
|
-
}
|
|
282
|
-
return 2;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const scopes = parsed.options.scope === "all"
|
|
286
|
-
? ["agents-freshness", "context-budget", "structure-budget", "high-risk-doc-metadata"]
|
|
287
|
-
: [parsed.options.scope];
|
|
288
|
-
|
|
289
|
-
if (parsed.options.json) {
|
|
290
|
-
const results = [];
|
|
291
|
-
let exitCode = 0;
|
|
292
|
-
for (const scope of scopes) {
|
|
293
|
-
const result = runJsonScope(governance.config, scope);
|
|
294
|
-
if (exitCode === 0 && result.exitCode !== 0) {
|
|
295
|
-
exitCode = result.exitCode;
|
|
296
|
-
}
|
|
297
|
-
results.push({
|
|
298
|
-
scope,
|
|
299
|
-
ok: result.exitCode === 0,
|
|
300
|
-
exitCode: result.exitCode,
|
|
301
|
-
report: result.report,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
writeJson({
|
|
305
|
-
ok: exitCode === 0,
|
|
306
|
-
command: "validate-ai-governance",
|
|
307
|
-
profile: profileCheck.profile,
|
|
308
|
-
scope: parsed.options.scope,
|
|
309
|
-
scopes: results,
|
|
310
|
-
});
|
|
311
|
-
return exitCode;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
for (const scope of scopes) {
|
|
315
|
-
let exitCode = 0;
|
|
316
|
-
if (scope === "agents-freshness") {
|
|
317
|
-
exitCode = runAgentsFreshnessCheck({
|
|
318
|
-
projectRoot: process.cwd(),
|
|
319
|
-
config: governance.config.aiGovernance.agentsFreshness,
|
|
320
|
-
});
|
|
321
|
-
} else if (scope === "context-budget") {
|
|
322
|
-
exitCode = await runContextBudget(governance.config);
|
|
323
|
-
} else if (scope === "structure-budget") {
|
|
324
|
-
exitCode = await runStructureBudget(governance.config);
|
|
325
|
-
} else if (scope === "high-risk-doc-metadata") {
|
|
326
|
-
exitCode = await runHighRiskDocMetadata(governance.config);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (exitCode !== 0) {
|
|
330
|
-
return exitCode;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return 0;
|
|
335
|
-
}
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { execSync } from 'node:child_process';
|
|
5
|
-
import YAML from 'yaml';
|
|
6
|
-
|
|
7
|
-
const GOVERNANCE_CONFIG_RELATIVE_PATH = '.nimi/config/governance.yaml';
|
|
8
|
-
const GOVERNANCE_CONTEXT_BUDGET_SECTION = 'ai_governance.context_budget';
|
|
9
|
-
|
|
10
|
-
function escapeRegex(input) {
|
|
11
|
-
return input.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function globToRegExp(glob) {
|
|
15
|
-
const normalized = glob.replace(/\\/g, '/').trim();
|
|
16
|
-
let pattern = '^';
|
|
17
|
-
for (let i = 0; i < normalized.length; i += 1) {
|
|
18
|
-
const current = normalized[i];
|
|
19
|
-
const next = normalized[i + 1];
|
|
20
|
-
if (current === '*') {
|
|
21
|
-
if (next === '*') {
|
|
22
|
-
const afterNext = normalized[i + 2];
|
|
23
|
-
if (afterNext === '/') {
|
|
24
|
-
pattern += '(?:.*/)?';
|
|
25
|
-
i += 2;
|
|
26
|
-
} else {
|
|
27
|
-
pattern += '.*';
|
|
28
|
-
i += 1;
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
pattern += '[^/]*';
|
|
32
|
-
}
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (current === '?') {
|
|
36
|
-
pattern += '[^/]';
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
pattern += escapeRegex(current);
|
|
40
|
-
}
|
|
41
|
-
pattern += '$';
|
|
42
|
-
return new RegExp(pattern);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function compileMatchers(patterns) {
|
|
46
|
-
return (patterns || []).map((pattern) => ({
|
|
47
|
-
pattern,
|
|
48
|
-
regex: globToRegExp(pattern),
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function matchesAny(filePath, matchers) {
|
|
53
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
54
|
-
for (const matcher of matchers) {
|
|
55
|
-
if (matcher.regex.test(normalized)) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function toSeverity(value, warningThreshold, errorThreshold) {
|
|
63
|
-
if (typeof errorThreshold === 'number' && value >= errorThreshold) {
|
|
64
|
-
return 'error';
|
|
65
|
-
}
|
|
66
|
-
if (typeof warningThreshold === 'number' && value >= warningThreshold) {
|
|
67
|
-
return 'warning';
|
|
68
|
-
}
|
|
69
|
-
return 'none';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function maxSeverity(left, right) {
|
|
73
|
-
const rank = {
|
|
74
|
-
none: 0,
|
|
75
|
-
warning: 1,
|
|
76
|
-
error: 2,
|
|
77
|
-
};
|
|
78
|
-
return rank[left] >= rank[right] ? left : right;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseDateMaybe(input) {
|
|
82
|
-
if (!input) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
if (input instanceof Date) {
|
|
86
|
-
return Number.isNaN(input.getTime()) ? null : input;
|
|
87
|
-
}
|
|
88
|
-
const parsed = new Date(String(input));
|
|
89
|
-
if (Number.isNaN(parsed.getTime())) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return parsed;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function measureLineShape(buffer) {
|
|
96
|
-
if (!buffer || buffer.length === 0) {
|
|
97
|
-
return {
|
|
98
|
-
lines: 0,
|
|
99
|
-
maxLineBytes: 0,
|
|
100
|
-
averageLineBytes: 0,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let lines = 1;
|
|
105
|
-
let currentLineBytes = 0;
|
|
106
|
-
let maxLineBytes = 0;
|
|
107
|
-
|
|
108
|
-
for (const byte of buffer) {
|
|
109
|
-
if (byte === 10) {
|
|
110
|
-
maxLineBytes = Math.max(maxLineBytes, currentLineBytes);
|
|
111
|
-
currentLineBytes = 0;
|
|
112
|
-
lines += 1;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
currentLineBytes += 1;
|
|
116
|
-
}
|
|
117
|
-
maxLineBytes = Math.max(maxLineBytes, currentLineBytes);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
lines,
|
|
121
|
-
maxLineBytes,
|
|
122
|
-
averageLineBytes: buffer.byteLength / lines,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getPathSection(source, sectionPath) {
|
|
127
|
-
let current = source;
|
|
128
|
-
for (const segment of sectionPath.split('.')) {
|
|
129
|
-
if (!current || typeof current !== 'object') {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
current = current[segment];
|
|
133
|
-
}
|
|
134
|
-
return current && typeof current === 'object' ? current : null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel, configSection) {
|
|
138
|
-
const rawParsed = inlineConfig ?? (() => {
|
|
139
|
-
const configPath = path.join(cwd, relativePath);
|
|
140
|
-
if (!fs.existsSync(configPath)) {
|
|
141
|
-
throw new Error(`budget config not found: ${relativePath}`);
|
|
142
|
-
}
|
|
143
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
144
|
-
return YAML.parse(raw);
|
|
145
|
-
})();
|
|
146
|
-
const parsed = configSection ? getPathSection(rawParsed, configSection) : rawParsed;
|
|
147
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
148
|
-
throw new Error(`invalid budget config format: ${relativePath}${configSection ? `#${configSection}` : ''}`);
|
|
149
|
-
}
|
|
150
|
-
if (!parsed.profiles || typeof parsed.profiles !== 'object') {
|
|
151
|
-
throw new Error(`budget config missing profiles: ${relativePath}${configSection ? `#${configSection}` : ''}`);
|
|
152
|
-
}
|
|
153
|
-
const defaultProfile = String(parsed.default_profile || 'production');
|
|
154
|
-
if (!parsed.profiles[defaultProfile]) {
|
|
155
|
-
throw new Error(`default profile not found in profiles: ${defaultProfile}`);
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
configPath: configPathLabel || (configSection ? `${relativePath}#${configSection}` : relativePath),
|
|
159
|
-
parsed,
|
|
160
|
-
defaultProfile,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function listTrackedFiles(cwd) {
|
|
165
|
-
const output = execSync('git ls-files -z', {
|
|
166
|
-
cwd,
|
|
167
|
-
encoding: 'utf8',
|
|
168
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
|
-
});
|
|
170
|
-
return output
|
|
171
|
-
.split('\u0000')
|
|
172
|
-
.map((value) => value.trim())
|
|
173
|
-
.filter((value) => value.length > 0);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function resolveProfile(filePath, classifierMatchers, defaultProfile) {
|
|
177
|
-
for (const classifier of classifierMatchers) {
|
|
178
|
-
if (matchesAny(filePath, classifier.matchers)) {
|
|
179
|
-
return classifier.profile;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return defaultProfile;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function buildWaiverMap(waivers) {
|
|
186
|
-
const map = new Map();
|
|
187
|
-
for (const waiver of waivers || []) {
|
|
188
|
-
if (!waiver || typeof waiver !== 'object') {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
const filePath = String(waiver.path || '').trim();
|
|
192
|
-
if (!filePath) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
map.set(filePath, {
|
|
196
|
-
path: filePath,
|
|
197
|
-
reason: String(waiver.reason || '').trim(),
|
|
198
|
-
until: parseDateMaybe(waiver.until),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return map;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function waiverAllowedForProfile(profileId) {
|
|
205
|
-
return profileId === 'tests_and_scripts' || profileId === 'tests' || profileId === 'scripts' || profileId === 'generated';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function evaluateAiContextBudget(options = {}) {
|
|
209
|
-
const cwd = options.cwd || process.cwd();
|
|
210
|
-
const configRelativePath = options.configRelativePath || GOVERNANCE_CONFIG_RELATIVE_PATH;
|
|
211
|
-
const configSection = options.configSection
|
|
212
|
-
?? (options.config || options.configRelativePath ? null : GOVERNANCE_CONTEXT_BUDGET_SECTION);
|
|
213
|
-
const { parsed, defaultProfile, configPath } = loadBudgetConfig(
|
|
214
|
-
cwd,
|
|
215
|
-
configRelativePath,
|
|
216
|
-
options.config || null,
|
|
217
|
-
options.configPathLabel || null,
|
|
218
|
-
configSection,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const excludeMatchers = compileMatchers(parsed.exclude || []);
|
|
222
|
-
const classifierMatchers = Object.entries(parsed.classifiers || {}).map(([profile, patterns]) => ({
|
|
223
|
-
profile,
|
|
224
|
-
matchers: compileMatchers(patterns || []),
|
|
225
|
-
}));
|
|
226
|
-
const waiverMap = buildWaiverMap(parsed.waivers || []);
|
|
227
|
-
|
|
228
|
-
const files = listTrackedFiles(cwd);
|
|
229
|
-
const rows = [];
|
|
230
|
-
const skippedMissing = [];
|
|
231
|
-
const invalidWaivers = [];
|
|
232
|
-
|
|
233
|
-
for (const [filePath, waiver] of waiverMap.entries()) {
|
|
234
|
-
const absolutePath = path.join(cwd, filePath);
|
|
235
|
-
if (!fs.existsSync(absolutePath)) {
|
|
236
|
-
invalidWaivers.push({
|
|
237
|
-
file: filePath,
|
|
238
|
-
reason: waiver.reason,
|
|
239
|
-
kind: 'missing',
|
|
240
|
-
detail: 'waiver points to a missing path',
|
|
241
|
-
});
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
if (matchesAny(filePath, excludeMatchers)) {
|
|
245
|
-
invalidWaivers.push({
|
|
246
|
-
file: filePath,
|
|
247
|
-
reason: waiver.reason,
|
|
248
|
-
kind: 'excluded',
|
|
249
|
-
detail: 'waiver targets an excluded path',
|
|
250
|
-
});
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const profileId = resolveProfile(filePath, classifierMatchers, defaultProfile);
|
|
254
|
-
if (!waiverAllowedForProfile(profileId)) {
|
|
255
|
-
invalidWaivers.push({
|
|
256
|
-
file: filePath,
|
|
257
|
-
reason: waiver.reason,
|
|
258
|
-
kind: 'forbidden_profile',
|
|
259
|
-
detail: `waivers are forbidden for profile ${profileId}`,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (const relativePath of files) {
|
|
265
|
-
if (matchesAny(relativePath, excludeMatchers)) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const absolutePath = path.join(cwd, relativePath);
|
|
270
|
-
if (!fs.existsSync(absolutePath)) {
|
|
271
|
-
skippedMissing.push(relativePath);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const stat = fs.statSync(absolutePath);
|
|
275
|
-
if (!stat.isFile()) {
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const profileId = resolveProfile(relativePath, classifierMatchers, defaultProfile);
|
|
280
|
-
const profile = parsed.profiles[profileId] || parsed.profiles[defaultProfile];
|
|
281
|
-
if (!profile) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const buffer = fs.readFileSync(absolutePath);
|
|
286
|
-
const { lines, maxLineBytes, averageLineBytes } = measureLineShape(buffer);
|
|
287
|
-
const bytes = buffer.byteLength;
|
|
288
|
-
|
|
289
|
-
const linesSeverity = toSeverity(lines, profile.warning_lines, profile.error_lines);
|
|
290
|
-
const bytesSeverity = toSeverity(bytes, profile.warning_bytes, profile.error_bytes);
|
|
291
|
-
const maxLineBytesSeverity = toSeverity(
|
|
292
|
-
maxLineBytes,
|
|
293
|
-
profile.warning_max_line_bytes,
|
|
294
|
-
profile.error_max_line_bytes,
|
|
295
|
-
);
|
|
296
|
-
const averageLineBytesSeverity = toSeverity(
|
|
297
|
-
averageLineBytes,
|
|
298
|
-
profile.warning_average_line_bytes,
|
|
299
|
-
profile.error_average_line_bytes,
|
|
300
|
-
);
|
|
301
|
-
const severity = [
|
|
302
|
-
linesSeverity,
|
|
303
|
-
bytesSeverity,
|
|
304
|
-
maxLineBytesSeverity,
|
|
305
|
-
averageLineBytesSeverity,
|
|
306
|
-
].reduce((current, next) => maxSeverity(current, next), "none");
|
|
307
|
-
|
|
308
|
-
const waiver = waiverMap.get(relativePath) || null;
|
|
309
|
-
const waiverExpired = waiver?.until ? waiver.until.getTime() < Date.now() : false;
|
|
310
|
-
const waived = Boolean(waiver) && !waiverExpired;
|
|
311
|
-
|
|
312
|
-
rows.push({
|
|
313
|
-
file: relativePath,
|
|
314
|
-
profile: profileId,
|
|
315
|
-
lines,
|
|
316
|
-
bytes,
|
|
317
|
-
maxLineBytes,
|
|
318
|
-
averageLineBytes,
|
|
319
|
-
severity,
|
|
320
|
-
linesSeverity,
|
|
321
|
-
bytesSeverity,
|
|
322
|
-
maxLineBytesSeverity,
|
|
323
|
-
averageLineBytesSeverity,
|
|
324
|
-
warningLines: profile.warning_lines,
|
|
325
|
-
errorLines: profile.error_lines,
|
|
326
|
-
warningBytes: profile.warning_bytes,
|
|
327
|
-
errorBytes: profile.error_bytes,
|
|
328
|
-
warningMaxLineBytes: profile.warning_max_line_bytes,
|
|
329
|
-
errorMaxLineBytes: profile.error_max_line_bytes,
|
|
330
|
-
warningAverageLineBytes: profile.warning_average_line_bytes,
|
|
331
|
-
errorAverageLineBytes: profile.error_average_line_bytes,
|
|
332
|
-
waiver,
|
|
333
|
-
waiverExpired,
|
|
334
|
-
waived,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const warnings = rows.filter((row) => row.severity === 'warning');
|
|
339
|
-
const errors = rows.filter((row) => row.severity === 'error' && !row.waived);
|
|
340
|
-
const waivedErrors = rows.filter((row) => row.severity === 'error' && row.waived);
|
|
341
|
-
const expiredWaivers = rows.filter((row) => row.severity === 'error' && row.waiver && row.waiverExpired);
|
|
342
|
-
|
|
343
|
-
rows.sort((left, right) => {
|
|
344
|
-
if (right.lines !== left.lines) {
|
|
345
|
-
return right.lines - left.lines;
|
|
346
|
-
}
|
|
347
|
-
if (right.bytes !== left.bytes) {
|
|
348
|
-
return right.bytes - left.bytes;
|
|
349
|
-
}
|
|
350
|
-
return right.maxLineBytes - left.maxLineBytes;
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
configPath,
|
|
355
|
-
totalTrackedFiles: files.length,
|
|
356
|
-
analyzedFiles: rows.length,
|
|
357
|
-
skippedMissing,
|
|
358
|
-
rows,
|
|
359
|
-
warnings,
|
|
360
|
-
errors,
|
|
361
|
-
waivedErrors,
|
|
362
|
-
expiredWaivers,
|
|
363
|
-
invalidWaivers,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export function formatBytes(bytes) {
|
|
368
|
-
const kb = 1024;
|
|
369
|
-
const mb = kb * 1024;
|
|
370
|
-
if (bytes >= mb) {
|
|
371
|
-
return `${(bytes / mb).toFixed(1)}MB`;
|
|
372
|
-
}
|
|
373
|
-
if (bytes >= kb) {
|
|
374
|
-
return `${(bytes / kb).toFixed(1)}KB`;
|
|
375
|
-
}
|
|
376
|
-
return `${bytes}B`;
|
|
377
|
-
}
|