@nimiplatform/nimi-coding 0.1.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/LICENSE +21 -0
- package/README.md +348 -0
- package/adapters/README.md +25 -0
- package/adapters/claude/README.md +89 -0
- package/adapters/claude/profile.yaml +70 -0
- package/adapters/codex/README.md +53 -0
- package/adapters/codex/profile.yaml +78 -0
- package/adapters/oh-my-codex/README.md +185 -0
- package/adapters/oh-my-codex/profile.yaml +46 -0
- package/bin/nimicoding.mjs +6 -0
- package/cli/commands/admit-high-risk-decision.mjs +108 -0
- package/cli/commands/audit-sweep.mjs +341 -0
- package/cli/commands/blueprint-audit.mjs +91 -0
- package/cli/commands/clear.mjs +168 -0
- package/cli/commands/closeout.mjs +183 -0
- package/cli/commands/decide-high-risk-execution.mjs +124 -0
- package/cli/commands/doctor.mjs +53 -0
- package/cli/commands/generate-spec-derived-docs.mjs +131 -0
- package/cli/commands/handoff.mjs +123 -0
- package/cli/commands/ingest-high-risk-execution.mjs +95 -0
- package/cli/commands/review-high-risk-execution.mjs +95 -0
- package/cli/commands/start.mjs +717 -0
- package/cli/commands/topic-formatters.mjs +382 -0
- package/cli/commands/topic-goal.mjs +33 -0
- package/cli/commands/topic-options-shared.mjs +27 -0
- package/cli/commands/topic-options-workflow.mjs +767 -0
- package/cli/commands/topic-options.mjs +626 -0
- package/cli/commands/topic-runner.mjs +169 -0
- package/cli/commands/topic.mjs +795 -0
- package/cli/commands/validate-acceptance.mjs +5 -0
- package/cli/commands/validate-ai-governance.mjs +214 -0
- package/cli/commands/validate-execution-packet.mjs +5 -0
- package/cli/commands/validate-orchestration-state.mjs +5 -0
- package/cli/commands/validate-prompt.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +27 -0
- package/cli/commands/validate-spec-governance.mjs +124 -0
- package/cli/commands/validate-spec-tree.mjs +27 -0
- package/cli/commands/validate-worker-output.mjs +5 -0
- package/cli/constants.mjs +489 -0
- package/cli/help.mjs +134 -0
- package/cli/index.mjs +103 -0
- package/cli/lib/adapter-profiles.mjs +403 -0
- package/cli/lib/audit-execution.mjs +52 -0
- package/cli/lib/audit-sweep-runtime/admissions.mjs +381 -0
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +333 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +697 -0
- package/cli/lib/audit-sweep-runtime/closeout.mjs +144 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +639 -0
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +515 -0
- package/cli/lib/audit-sweep-runtime/common.mjs +329 -0
- package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +172 -0
- package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +152 -0
- package/cli/lib/audit-sweep-runtime/format.mjs +57 -0
- package/cli/lib/audit-sweep-runtime/ingest.mjs +486 -0
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +198 -0
- package/cli/lib/audit-sweep-runtime/inventory.mjs +728 -0
- package/cli/lib/audit-sweep-runtime/ledger.mjs +315 -0
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +101 -0
- package/cli/lib/audit-sweep-runtime/remediation.mjs +349 -0
- package/cli/lib/audit-sweep-runtime/rerun.mjs +129 -0
- package/cli/lib/audit-sweep-runtime/risk-budget.mjs +300 -0
- package/cli/lib/audit-sweep-runtime/status.mjs +62 -0
- package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +215 -0
- package/cli/lib/audit-sweep-runtime/validators.mjs +758 -0
- package/cli/lib/audit-sweep.mjs +18 -0
- package/cli/lib/authority-convergence.mjs +309 -0
- package/cli/lib/blueprint-audit.mjs +370 -0
- package/cli/lib/bootstrap.mjs +228 -0
- package/cli/lib/closeout.mjs +623 -0
- package/cli/lib/codex-sdk-runner.mjs +76 -0
- package/cli/lib/contracts.mjs +180 -0
- package/cli/lib/doctor.mjs +18 -0
- package/cli/lib/entrypoints.mjs +274 -0
- package/cli/lib/external-execution.mjs +101 -0
- package/cli/lib/fs-helpers.mjs +33 -0
- package/cli/lib/handoff.mjs +785 -0
- package/cli/lib/high-risk-admission.mjs +442 -0
- package/cli/lib/high-risk-decision.mjs +324 -0
- package/cli/lib/high-risk-ingest.mjs +317 -0
- package/cli/lib/high-risk-review.mjs +263 -0
- package/cli/lib/internal/contracts-loaders.mjs +132 -0
- package/cli/lib/internal/contracts-parse-high-risk.mjs +131 -0
- package/cli/lib/internal/contracts-parse.mjs +457 -0
- package/cli/lib/internal/contracts-validators.mjs +398 -0
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +359 -0
- package/cli/lib/internal/doctor-delegated-surface.mjs +256 -0
- package/cli/lib/internal/doctor-finalize.mjs +385 -0
- package/cli/lib/internal/doctor-format.mjs +286 -0
- package/cli/lib/internal/doctor-inspectors.mjs +294 -0
- package/cli/lib/internal/doctor-state.mjs +205 -0
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +315 -0
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +358 -0
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +155 -0
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +173 -0
- package/cli/lib/internal/governance/config.mjs +150 -0
- package/cli/lib/internal/governance/runner.mjs +35 -0
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +49 -0
- package/cli/lib/internal/validators-artifacts.mjs +515 -0
- package/cli/lib/internal/validators-shared.mjs +28 -0
- package/cli/lib/internal/validators-spec-helpers.mjs +186 -0
- package/cli/lib/internal/validators-spec.mjs +410 -0
- package/cli/lib/shared.mjs +83 -0
- package/cli/lib/topic-draft-packets.mjs +48 -0
- package/cli/lib/topic-goal.mjs +361 -0
- package/cli/lib/topic-runner.mjs +772 -0
- package/cli/lib/topic.mjs +93 -0
- package/cli/lib/ui.mjs +178 -0
- package/cli/lib/validators.mjs +78 -0
- package/cli/lib/value-helpers.mjs +24 -0
- package/cli/lib/yaml-helpers.mjs +133 -0
- package/cli/nimicoding.mjs +1 -0
- package/cli/seeds/bootstrap.mjs +47 -0
- package/config/audit-execution-artifacts.yaml +20 -0
- package/config/bootstrap.yaml +6 -0
- package/config/external-execution-artifacts.yaml +16 -0
- package/config/host-adapter.yaml +30 -0
- package/config/host-profile.yaml +29 -0
- package/config/installer-evidence.yaml +31 -0
- package/config/skill-installer.yaml +23 -0
- package/config/skill-manifest.yaml +46 -0
- package/config/skills.yaml +30 -0
- package/config/spec-generation-inputs.yaml +25 -0
- package/contracts/acceptance.schema.yaml +16 -0
- package/contracts/admission-checklist.schema.yaml +15 -0
- package/contracts/audit-chunk.schema.yaml +110 -0
- package/contracts/audit-closeout.schema.yaml +51 -0
- package/contracts/audit-finding.schema.yaml +61 -0
- package/contracts/audit-ledger.schema.yaml +138 -0
- package/contracts/audit-plan.schema.yaml +123 -0
- package/contracts/audit-remediation-map.schema.yaml +51 -0
- package/contracts/audit-rerun.schema.yaml +31 -0
- package/contracts/audit-sweep-result.yaml +49 -0
- package/contracts/authority-convergence-audit.schema.yaml +19 -0
- package/contracts/closeout.schema.yaml +25 -0
- package/contracts/decision-review.schema.yaml +16 -0
- package/contracts/doc-spec-audit-result.yaml +19 -0
- package/contracts/execution-packet.schema.yaml +49 -0
- package/contracts/external-host-compatibility.yaml +22 -0
- package/contracts/forbidden-shortcuts.catalog.yaml +23 -0
- package/contracts/high-risk-admission.schema.yaml +23 -0
- package/contracts/high-risk-execution-result.yaml +20 -0
- package/contracts/orchestration-state.schema.yaml +41 -0
- package/contracts/overflow-continuation.schema.yaml +12 -0
- package/contracts/packet.schema.yaml +30 -0
- package/contracts/pending-note.schema.yaml +17 -0
- package/contracts/prompt.schema.yaml +12 -0
- package/contracts/remediation.schema.yaml +16 -0
- package/contracts/result.schema.yaml +24 -0
- package/contracts/spec-generation-audit.schema.yaml +31 -0
- package/contracts/spec-generation-inputs.schema.yaml +39 -0
- package/contracts/spec-reconstruction-result.yaml +37 -0
- package/contracts/topic-goal.schema.yaml +78 -0
- package/contracts/topic-run-ledger.schema.yaml +72 -0
- package/contracts/topic-step-decision.schema.yaml +45 -0
- package/contracts/topic.schema.yaml +65 -0
- package/contracts/true-close.schema.yaml +15 -0
- package/contracts/wave.schema.yaml +29 -0
- package/contracts/worker-output.schema.yaml +15 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +45 -0
- package/methodology/authority-convergence-policy.yaml +42 -0
- package/methodology/core.yaml +25 -0
- package/methodology/four-closure-policy.yaml +28 -0
- package/methodology/overflow-continuation-policy.yaml +14 -0
- package/methodology/role-separation-policy.yaml +28 -0
- package/methodology/skill-exchange-projection.yaml +114 -0
- package/methodology/skill-handoff.yaml +34 -0
- package/methodology/skill-installer-result.yaml +27 -0
- package/methodology/skill-installer-summary-projection.yaml +181 -0
- package/methodology/skill-runtime.yaml +23 -0
- package/methodology/spec-reconstruction.yaml +63 -0
- package/methodology/spec-target-truth-profile.yaml +53 -0
- package/methodology/topic-lifecycle-report.yaml +144 -0
- package/methodology/topic-lifecycle.yaml +37 -0
- package/methodology/topic-naming-ontology.yaml +21 -0
- package/methodology/topic-ontology.yaml +38 -0
- package/methodology/topic-validation-policy.yaml +9 -0
- package/methodology/wave-dag-policy.yaml +14 -0
- package/package.json +50 -0
- package/spec/_meta/command-gating-matrix.yaml +110 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +155 -0
- package/spec/_meta/governance-routing-cutover-checklist.yaml +35 -0
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +44 -0
- package/spec/_meta/spec-authority-cutover-readiness.yaml +104 -0
- package/spec/_meta/spec-tree-model.yaml +72 -0
- package/spec/bootstrap-state.yaml +99 -0
- package/spec/product-scope.yaml +56 -0
|
@@ -0,0 +1,358 @@
|
|
|
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
|
+
function escapeRegex(input) {
|
|
8
|
+
return input.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function globToRegExp(glob) {
|
|
12
|
+
const normalized = glob.replace(/\\/g, '/').trim();
|
|
13
|
+
let pattern = '^';
|
|
14
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
15
|
+
const current = normalized[index];
|
|
16
|
+
const next = normalized[index + 1];
|
|
17
|
+
if (current === '*') {
|
|
18
|
+
if (next === '*') {
|
|
19
|
+
const afterNext = normalized[index + 2];
|
|
20
|
+
if (afterNext === '/') {
|
|
21
|
+
pattern += '(?:.*/)?';
|
|
22
|
+
index += 2;
|
|
23
|
+
} else {
|
|
24
|
+
pattern += '.*';
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
pattern += '[^/]*';
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (current === '?') {
|
|
33
|
+
pattern += '[^/]';
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
pattern += escapeRegex(current);
|
|
37
|
+
}
|
|
38
|
+
pattern += '$';
|
|
39
|
+
return new RegExp(pattern);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compileMatchers(patterns) {
|
|
43
|
+
return (patterns || []).map((pattern) => ({
|
|
44
|
+
pattern,
|
|
45
|
+
regex: globToRegExp(pattern),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePathPrefix(input) {
|
|
50
|
+
return String(input || '')
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/\\/g, '/')
|
|
53
|
+
.replace(/^\/+|\/+$/g, '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesAny(filePath, matchers) {
|
|
57
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
58
|
+
for (const matcher of matchers) {
|
|
59
|
+
if (matcher.regex.test(normalized)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseDateMaybe(input) {
|
|
67
|
+
if (!input) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (input instanceof Date) {
|
|
71
|
+
return Number.isNaN(input.getTime()) ? null : input;
|
|
72
|
+
}
|
|
73
|
+
const parsed = new Date(String(input));
|
|
74
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel) {
|
|
81
|
+
const parsed = inlineConfig ?? (() => {
|
|
82
|
+
const configPath = path.join(cwd, relativePath);
|
|
83
|
+
if (!fs.existsSync(configPath)) {
|
|
84
|
+
throw new Error(`budget config not found: ${relativePath}`);
|
|
85
|
+
}
|
|
86
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
87
|
+
return YAML.parse(raw);
|
|
88
|
+
})();
|
|
89
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
90
|
+
throw new Error(`invalid budget config format: ${relativePath}`);
|
|
91
|
+
}
|
|
92
|
+
if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
|
|
93
|
+
throw new Error(`budget config missing rules: ${relativePath}`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
configPath: configPathLabel || relativePath,
|
|
97
|
+
parsed,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function listTrackedFiles(cwd) {
|
|
102
|
+
const output = execSync('git ls-files -z', {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
});
|
|
107
|
+
return output
|
|
108
|
+
.split('\u0000')
|
|
109
|
+
.map((value) => value.trim())
|
|
110
|
+
.filter((value) => value.length > 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toSeverity(value, warningThreshold, errorThreshold) {
|
|
114
|
+
if (typeof errorThreshold === 'number' && value >= errorThreshold) {
|
|
115
|
+
return 'error';
|
|
116
|
+
}
|
|
117
|
+
if (typeof warningThreshold === 'number' && value >= warningThreshold) {
|
|
118
|
+
return 'warning';
|
|
119
|
+
}
|
|
120
|
+
return 'none';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeLines(source) {
|
|
124
|
+
return source
|
|
125
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
126
|
+
.replace(/^\s*\/\/.*$/gmu, '')
|
|
127
|
+
.split(/\r?\n/u)
|
|
128
|
+
.map((line) => line.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isDisallowedForwardingShell(filePath, source, allowedBasenames) {
|
|
133
|
+
const basename = path.basename(filePath);
|
|
134
|
+
if (allowedBasenames.has(basename)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const lines = normalizeLines(source);
|
|
139
|
+
if (lines.length === 0) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tsPatterns = [
|
|
144
|
+
/^export\s+\*\s+from\s+['"].+['"];?$/u,
|
|
145
|
+
/^export\s+\*\s+as\s+\w+\s+from\s+['"].+['"];?$/u,
|
|
146
|
+
/^export\s+(type\s+)?\{[^}]+\}\s+from\s+['"].+['"];?$/u,
|
|
147
|
+
];
|
|
148
|
+
const rustPatterns = [
|
|
149
|
+
/^mod\s+\w+;$/u,
|
|
150
|
+
/^pub\s+mod\s+\w+;$/u,
|
|
151
|
+
/^use\s+.+;$/u,
|
|
152
|
+
/^pub(\([^)]*\))?\s+use\s+.+;$/u,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
return lines.every((line) => tsPatterns.some((pattern) => pattern.test(line)) || rustPatterns.some((pattern) => pattern.test(line)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRule(filePath, compiledRules) {
|
|
159
|
+
for (const rule of compiledRules) {
|
|
160
|
+
if (matchesAny(filePath, rule.matchers)) {
|
|
161
|
+
return rule;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function describeDepthSubject(filePath, depthBase) {
|
|
168
|
+
const normalizedFilePath = filePath.replace(/\\/g, '/');
|
|
169
|
+
if (!depthBase) {
|
|
170
|
+
return {
|
|
171
|
+
depthBase: '.',
|
|
172
|
+
depthSubject: normalizedFilePath,
|
|
173
|
+
depth: normalizedFilePath.split('/').length,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const normalizedBase = normalizePathPrefix(depthBase);
|
|
178
|
+
const prefix = `${normalizedBase}/`;
|
|
179
|
+
if (!normalizedFilePath.startsWith(prefix)) {
|
|
180
|
+
throw new Error(`depth_base "${normalizedBase}" does not match file "${normalizedFilePath}"`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const depthSubject = normalizedFilePath.slice(prefix.length);
|
|
184
|
+
return {
|
|
185
|
+
depthBase: normalizedBase,
|
|
186
|
+
depthSubject,
|
|
187
|
+
depth: depthSubject ? depthSubject.split('/').length : 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildWaivers(waivers) {
|
|
192
|
+
return (waivers || []).map((waiver) => ({
|
|
193
|
+
...waiver,
|
|
194
|
+
matcher: globToRegExp(String(waiver.pattern || '').trim()),
|
|
195
|
+
checks: new Set((waiver.checks || []).map((value) => String(value).trim()).filter(Boolean)),
|
|
196
|
+
hasUntil: typeof waiver.until !== 'undefined' && String(waiver.until).trim().length > 0,
|
|
197
|
+
untilDate: parseDateMaybe(waiver.until),
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findWaiver(filePath, check, waivers) {
|
|
202
|
+
for (const waiver of waivers) {
|
|
203
|
+
if (!waiver.matcher.test(filePath)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (waiver.checks.size > 0 && !waiver.checks.has(check)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
return waiver;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getWaiverDisposition(waiver) {
|
|
215
|
+
if (!waiver) {
|
|
216
|
+
return 'none';
|
|
217
|
+
}
|
|
218
|
+
if (!waiver.hasUntil) {
|
|
219
|
+
return 'active';
|
|
220
|
+
}
|
|
221
|
+
if (waiver.untilDate && waiver.untilDate.getTime() >= Date.now()) {
|
|
222
|
+
return 'active';
|
|
223
|
+
}
|
|
224
|
+
if (waiver.untilDate && waiver.untilDate.getTime() < Date.now()) {
|
|
225
|
+
return 'expired';
|
|
226
|
+
}
|
|
227
|
+
return 'none';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function compareRows(left, right) {
|
|
231
|
+
const severityRank = {
|
|
232
|
+
error: 2,
|
|
233
|
+
warning: 1,
|
|
234
|
+
none: 0,
|
|
235
|
+
};
|
|
236
|
+
if (severityRank[left.severity] !== severityRank[right.severity]) {
|
|
237
|
+
return severityRank[right.severity] - severityRank[left.severity];
|
|
238
|
+
}
|
|
239
|
+
if (left.check === 'depth' && right.check === 'depth' && left.depth !== right.depth) {
|
|
240
|
+
return right.depth - left.depth;
|
|
241
|
+
}
|
|
242
|
+
return left.file.localeCompare(right.file);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function evaluateAiStructureBudget(options = {}) {
|
|
246
|
+
const cwd = options.cwd || process.cwd();
|
|
247
|
+
const configRelativePath = options.configRelativePath || 'config/ai/ai-structure-budget.yaml';
|
|
248
|
+
const { parsed, configPath } = loadBudgetConfig(
|
|
249
|
+
cwd,
|
|
250
|
+
configRelativePath,
|
|
251
|
+
options.config || null,
|
|
252
|
+
options.configPathLabel || null,
|
|
253
|
+
);
|
|
254
|
+
const excludeMatchers = compileMatchers(parsed.exclude || []);
|
|
255
|
+
const compiledRules = (parsed.rules || []).map((rule) => ({
|
|
256
|
+
id: String(rule.id || '').trim(),
|
|
257
|
+
warningDepth: Number(rule.warning_depth),
|
|
258
|
+
errorDepth: Number(rule.error_depth),
|
|
259
|
+
depthBase: normalizePathPrefix(rule.depth_base),
|
|
260
|
+
matchers: compileMatchers(rule.include || []),
|
|
261
|
+
}));
|
|
262
|
+
const waivers = buildWaivers(parsed.waivers || []);
|
|
263
|
+
const allowedForwardingShells = new Set((parsed.allowed_forwarding_shells || []).map((value) => String(value).trim()).filter(Boolean));
|
|
264
|
+
|
|
265
|
+
const files = listTrackedFiles(cwd);
|
|
266
|
+
const rows = [];
|
|
267
|
+
const warnings = [];
|
|
268
|
+
const errors = [];
|
|
269
|
+
const waivedErrors = [];
|
|
270
|
+
const expiredWaivers = [];
|
|
271
|
+
let analyzedFiles = 0;
|
|
272
|
+
|
|
273
|
+
for (const relativePath of files) {
|
|
274
|
+
if (matchesAny(relativePath, excludeMatchers)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const rule = resolveRule(relativePath, compiledRules);
|
|
279
|
+
if (!rule) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const absolutePath = path.join(cwd, relativePath);
|
|
284
|
+
if (!fs.existsSync(absolutePath)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
analyzedFiles += 1;
|
|
288
|
+
|
|
289
|
+
const depthInfo = describeDepthSubject(relativePath, rule.depthBase);
|
|
290
|
+
const depth = depthInfo.depth;
|
|
291
|
+
const depthSeverity = toSeverity(depth, rule.warningDepth, rule.errorDepth);
|
|
292
|
+
if (depthSeverity !== 'none') {
|
|
293
|
+
const waiver = findWaiver(relativePath, 'depth', waivers);
|
|
294
|
+
const row = {
|
|
295
|
+
file: relativePath,
|
|
296
|
+
ruleId: rule.id,
|
|
297
|
+
check: 'depth',
|
|
298
|
+
severity: depthSeverity,
|
|
299
|
+
depth,
|
|
300
|
+
depthBase: depthInfo.depthBase,
|
|
301
|
+
depthSubject: depthInfo.depthSubject,
|
|
302
|
+
warningDepth: rule.warningDepth,
|
|
303
|
+
errorDepth: rule.errorDepth,
|
|
304
|
+
waiver,
|
|
305
|
+
};
|
|
306
|
+
rows.push(row);
|
|
307
|
+
if (depthSeverity === 'warning') {
|
|
308
|
+
warnings.push(row);
|
|
309
|
+
} else if (getWaiverDisposition(waiver) === 'active') {
|
|
310
|
+
waivedErrors.push(row);
|
|
311
|
+
} else if (getWaiverDisposition(waiver) === 'expired') {
|
|
312
|
+
expiredWaivers.push(row);
|
|
313
|
+
} else {
|
|
314
|
+
errors.push(row);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const source = fs.readFileSync(absolutePath, 'utf8');
|
|
319
|
+
if (!isDisallowedForwardingShell(relativePath, source, allowedForwardingShells)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const waiver = findWaiver(relativePath, 'forwarding_shell', waivers);
|
|
324
|
+
const row = {
|
|
325
|
+
file: relativePath,
|
|
326
|
+
ruleId: rule.id,
|
|
327
|
+
check: 'forwarding_shell',
|
|
328
|
+
severity: 'error',
|
|
329
|
+
basename: path.basename(relativePath),
|
|
330
|
+
waiver,
|
|
331
|
+
};
|
|
332
|
+
rows.push(row);
|
|
333
|
+
if (getWaiverDisposition(waiver) === 'active') {
|
|
334
|
+
waivedErrors.push(row);
|
|
335
|
+
} else if (getWaiverDisposition(waiver) === 'expired') {
|
|
336
|
+
expiredWaivers.push(row);
|
|
337
|
+
} else {
|
|
338
|
+
errors.push(row);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
rows.sort(compareRows);
|
|
343
|
+
warnings.sort(compareRows);
|
|
344
|
+
errors.sort(compareRows);
|
|
345
|
+
waivedErrors.sort(compareRows);
|
|
346
|
+
expiredWaivers.sort(compareRows);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
configPath,
|
|
350
|
+
totalTrackedFiles: files.length,
|
|
351
|
+
analyzedFiles,
|
|
352
|
+
rows,
|
|
353
|
+
warnings,
|
|
354
|
+
errors,
|
|
355
|
+
waivedErrors,
|
|
356
|
+
expiredWaivers,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TARGETS = [
|
|
5
|
+
{ rel: "AGENTS.md", maxLines: 100 },
|
|
6
|
+
{ rel: "nimi-backend/AGENTS.md", maxLines: 80 },
|
|
7
|
+
{ rel: "nimi-dashboard/AGENTS.md", maxLines: 65 },
|
|
8
|
+
{ rel: ".nimi/spec/AGENTS.md", maxLines: 70 },
|
|
9
|
+
{ rel: "scripts/AGENTS.md", maxLines: 45 },
|
|
10
|
+
{ rel: "tests/AGENTS.md", maxLines: 35 },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const DEFAULT_REQUIRED_SECTIONS = [
|
|
14
|
+
"## Scope",
|
|
15
|
+
"## Hard Boundaries",
|
|
16
|
+
"## Retrieval Defaults",
|
|
17
|
+
"## Verification Commands",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const DEFAULT_STALE_TOKENS = ["AISC-", "R-WORLD-", "R-AGENT-", "SOCIAL-013", "ECON-014"];
|
|
21
|
+
|
|
22
|
+
const GENERIC_PNPM_COMMANDS = new Set([
|
|
23
|
+
"install",
|
|
24
|
+
"test",
|
|
25
|
+
"build",
|
|
26
|
+
"typecheck",
|
|
27
|
+
"lint",
|
|
28
|
+
"dev",
|
|
29
|
+
"preview",
|
|
30
|
+
"check",
|
|
31
|
+
"verify",
|
|
32
|
+
"exec",
|
|
33
|
+
"run",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function readJson(filePath) {
|
|
37
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectKnownPnpmScripts(projectRoot) {
|
|
41
|
+
const rootPkg = readJson(path.join(projectRoot, "package.json"));
|
|
42
|
+
const scripts = new Set(Object.keys(rootPkg.scripts || {}));
|
|
43
|
+
|
|
44
|
+
for (const rel of ["nimi-backend/package.json", "nimi-dashboard/package.json"]) {
|
|
45
|
+
const abs = path.join(projectRoot, rel);
|
|
46
|
+
if (!fs.existsSync(abs)) continue;
|
|
47
|
+
const pkg = readJson(abs);
|
|
48
|
+
for (const name of Object.keys(pkg.scripts || {})) {
|
|
49
|
+
scripts.add(name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return scripts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validatePnpmCommand(command, knownScripts, failures, relPath) {
|
|
57
|
+
const tokens = command.trim().split(/\s+/u);
|
|
58
|
+
if (tokens[0] !== "pnpm") return;
|
|
59
|
+
|
|
60
|
+
let index = 1;
|
|
61
|
+
while (tokens[index]?.startsWith("--")) {
|
|
62
|
+
index += tokens[index]?.includes("=") ? 1 : 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const subcommand = String(tokens[index] || "").trim();
|
|
66
|
+
if (!subcommand || GENERIC_PNPM_COMMANDS.has(subcommand) || knownScripts.has(subcommand)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
failures.push(`${relPath}: unknown pnpm command: ${command}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validatePathToken(token, failures, relPath, agentsDir, projectRoot) {
|
|
74
|
+
if (token.includes(" ")) return;
|
|
75
|
+
if (!token.includes("/")) return;
|
|
76
|
+
if (
|
|
77
|
+
token.startsWith("http") ||
|
|
78
|
+
token.startsWith("@") ||
|
|
79
|
+
token.includes("*") ||
|
|
80
|
+
token.includes("{")
|
|
81
|
+
) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cleaned = token.replace(/[`,.;:()]+$/gu, "").replace(/^[(]+/u, "");
|
|
86
|
+
if (!cleaned || cleaned.startsWith("$") || cleaned.endsWith("/")) return;
|
|
87
|
+
if (cleaned.startsWith("nimi/")) return;
|
|
88
|
+
|
|
89
|
+
const absFromAgents = path.join(agentsDir, cleaned);
|
|
90
|
+
const absFromRoot = path.join(projectRoot, cleaned);
|
|
91
|
+
if (!fs.existsSync(absFromAgents) && !fs.existsSync(absFromRoot)) {
|
|
92
|
+
failures.push(`${relPath}: stale path reference: ${cleaned}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function runAgentsFreshnessCheck(options = {}) {
|
|
97
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
98
|
+
const config = options.config || {};
|
|
99
|
+
const targets = Array.isArray(config.targets) && config.targets.length > 0
|
|
100
|
+
? config.targets
|
|
101
|
+
: DEFAULT_TARGETS;
|
|
102
|
+
const requiredSections = Array.isArray(config.requiredSections) && config.requiredSections.length > 0
|
|
103
|
+
? config.requiredSections
|
|
104
|
+
: DEFAULT_REQUIRED_SECTIONS;
|
|
105
|
+
const staleTokens = Array.isArray(config.staleTokens) && config.staleTokens.length > 0
|
|
106
|
+
? config.staleTokens
|
|
107
|
+
: DEFAULT_STALE_TOKENS;
|
|
108
|
+
|
|
109
|
+
const failures = [];
|
|
110
|
+
const knownScripts = collectKnownPnpmScripts(projectRoot);
|
|
111
|
+
|
|
112
|
+
for (const target of targets) {
|
|
113
|
+
const abs = path.join(projectRoot, target.rel);
|
|
114
|
+
if (!fs.existsSync(abs)) {
|
|
115
|
+
failures.push(`missing AGENTS file: ${target.rel}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
120
|
+
const lines = content.split(/\r?\n/u);
|
|
121
|
+
if (Number(target.maxLines) > 0 && lines.length > Number(target.maxLines)) {
|
|
122
|
+
failures.push(`${target.rel}: exceeds line budget (${lines.length} > ${target.maxLines})`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const section of requiredSections) {
|
|
126
|
+
if (!content.includes(section)) {
|
|
127
|
+
failures.push(`${target.rel}: missing required section ${section}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const token of staleTokens) {
|
|
132
|
+
if (content.includes(token)) {
|
|
133
|
+
failures.push(`${target.rel}: contains stale token ${token}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const agentsDir = path.dirname(abs);
|
|
138
|
+
const backtickTokens = content.match(/`[^`\n]+`/gu) || [];
|
|
139
|
+
for (const raw of backtickTokens) {
|
|
140
|
+
const inner = raw.slice(1, -1);
|
|
141
|
+
validatePathToken(inner, failures, target.rel, agentsDir, projectRoot);
|
|
142
|
+
validatePnpmCommand(inner, knownScripts, failures, target.rel);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (failures.length > 0) {
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
`agents freshness check failed:\n${failures.map((entry) => `- ${entry}`).join("\n")}\n`,
|
|
149
|
+
);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.stdout.write(`agents freshness check passed (${targets.length} files)\n`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { readYamlWithFragments } from '../shared/read-yaml-with-fragments.mjs';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DOC_ROOTS = ['.local'];
|
|
7
|
+
const HIGH_RISK_NAME_PATTERNS = [
|
|
8
|
+
/design/iu,
|
|
9
|
+
/audit/iu,
|
|
10
|
+
/implementation-plan/iu,
|
|
11
|
+
/architecture/iu,
|
|
12
|
+
/refactor/iu,
|
|
13
|
+
/refactory/iu,
|
|
14
|
+
/remediation-plan/iu,
|
|
15
|
+
/unification/iu,
|
|
16
|
+
/migration/iu,
|
|
17
|
+
];
|
|
18
|
+
const REQUIRED_METADATA_KEYS = [
|
|
19
|
+
'Spec Status',
|
|
20
|
+
'Authority Owner',
|
|
21
|
+
'Work Type',
|
|
22
|
+
'Parallel Truth',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function normalizeRel(filePath, repoRoot) {
|
|
26
|
+
return path.relative(repoRoot, filePath).replace(/\\/g, '/');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isMarkdownFile(filePath) {
|
|
30
|
+
return filePath.endsWith('.md');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldSkip(relPath) {
|
|
34
|
+
return relPath.includes('/archive/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isHighRiskByName(relPath, patterns = HIGH_RISK_NAME_PATTERNS) {
|
|
38
|
+
const base = path.basename(relPath);
|
|
39
|
+
return patterns.some((pattern) => pattern.test(base));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readFileSafe(filePath) {
|
|
43
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectMetadata(content, requiredMetadataKeys = REQUIRED_METADATA_KEYS) {
|
|
47
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u);
|
|
48
|
+
if (frontmatterMatch) {
|
|
49
|
+
const doc = YAML.parse(frontmatterMatch[1]) || {};
|
|
50
|
+
const values = new Map([
|
|
51
|
+
['Spec Status', String(doc?.spec_status || '').trim()],
|
|
52
|
+
['Authority Owner', String(doc?.authority_owner || '').trim()],
|
|
53
|
+
['Work Type', String(doc?.work_type || '').trim()],
|
|
54
|
+
['Parallel Truth', String(doc?.parallel_truth || '').trim()],
|
|
55
|
+
]);
|
|
56
|
+
for (const key of requiredMetadataKeys) {
|
|
57
|
+
if (!values.has(key)) {
|
|
58
|
+
values.set(key, '');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return values;
|
|
62
|
+
}
|
|
63
|
+
const lines = content.split(/\r?\n/u).slice(0, 20);
|
|
64
|
+
const found = new Map();
|
|
65
|
+
for (const key of requiredMetadataKeys) {
|
|
66
|
+
const prefix = `> **${key}**:`;
|
|
67
|
+
const line = lines.find((item) => item.startsWith(prefix)) || null;
|
|
68
|
+
found.set(key, line ? line.slice(prefix.length).trim() : '');
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateMetadata(meta, requiredMetadataKeys = REQUIRED_METADATA_KEYS) {
|
|
74
|
+
const failures = [];
|
|
75
|
+
for (const key of requiredMetadataKeys) {
|
|
76
|
+
const value = meta.get(key) || '';
|
|
77
|
+
if (!value) {
|
|
78
|
+
failures.push(`missing metadata field "${key}"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const workType = meta.get('Work Type') || '';
|
|
82
|
+
if (workType && workType !== 'alignment' && workType !== 'redesign') {
|
|
83
|
+
failures.push('Work Type must be "alignment" or "redesign"');
|
|
84
|
+
}
|
|
85
|
+
const parallelTruth = meta.get('Parallel Truth') || '';
|
|
86
|
+
if (parallelTruth && parallelTruth !== 'yes' && parallelTruth !== 'no') {
|
|
87
|
+
failures.push('Parallel Truth must be "yes" or "no"');
|
|
88
|
+
}
|
|
89
|
+
const specStatus = meta.get('Spec Status') || '';
|
|
90
|
+
const validSpecStatuses = new Set([
|
|
91
|
+
'aligned',
|
|
92
|
+
'requires spec change',
|
|
93
|
+
'requires_change',
|
|
94
|
+
'preflight-required',
|
|
95
|
+
]);
|
|
96
|
+
if (specStatus && !validSpecStatuses.has(specStatus)) {
|
|
97
|
+
failures.push('Spec Status must be one of: aligned, requires spec change, requires_change, preflight-required');
|
|
98
|
+
}
|
|
99
|
+
return failures;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function walkMarkdownFiles(dirPath) {
|
|
103
|
+
const results = [];
|
|
104
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const nextPath = path.join(dirPath, entry.name);
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
results.push(...walkMarkdownFiles(nextPath));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (entry.isFile() && isMarkdownFile(nextPath)) {
|
|
112
|
+
results.push(nextPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function evaluateHighRiskDocMetadata(options = {}) {
|
|
119
|
+
const repoRoot = options.repoRoot || process.cwd();
|
|
120
|
+
const docRoots = Array.isArray(options.docRoots) && options.docRoots.length > 0
|
|
121
|
+
? options.docRoots
|
|
122
|
+
: DEFAULT_DOC_ROOTS;
|
|
123
|
+
const requiredMetadataKeys = Array.isArray(options.requiredMetadataKeys) && options.requiredMetadataKeys.length > 0
|
|
124
|
+
? options.requiredMetadataKeys.map((value) => String(value || '').trim()).filter(Boolean)
|
|
125
|
+
: REQUIRED_METADATA_KEYS;
|
|
126
|
+
const namePatterns = Array.isArray(options.namePatterns) && options.namePatterns.length > 0
|
|
127
|
+
? options.namePatterns.map((value) => new RegExp(String(value), 'iu'))
|
|
128
|
+
: HIGH_RISK_NAME_PATTERNS;
|
|
129
|
+
const exemptionsPath = options.exemptionsPath
|
|
130
|
+
|| path.join(repoRoot, 'scripts/config/high-risk-doc-metadata-exemptions.yaml');
|
|
131
|
+
const exemptionsDoc = options.exemptPaths
|
|
132
|
+
? { exempt_paths: options.exemptPaths }
|
|
133
|
+
: fs.existsSync(exemptionsPath)
|
|
134
|
+
? (readYamlWithFragments(exemptionsPath) || {})
|
|
135
|
+
: {};
|
|
136
|
+
const exemptPaths = new Set(
|
|
137
|
+
(Array.isArray(exemptionsDoc?.exempt_paths) ? exemptionsDoc.exempt_paths : [])
|
|
138
|
+
.map((value) => String(value || '').trim())
|
|
139
|
+
.filter(Boolean),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const scanned = [];
|
|
143
|
+
const failures = [];
|
|
144
|
+
|
|
145
|
+
for (const rootRel of docRoots) {
|
|
146
|
+
const absRoot = path.join(repoRoot, rootRel);
|
|
147
|
+
if (!fs.existsSync(absRoot)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const filePath of walkMarkdownFiles(absRoot)) {
|
|
151
|
+
const relPath = normalizeRel(filePath, repoRoot);
|
|
152
|
+
if (shouldSkip(relPath) || !isHighRiskByName(relPath, namePatterns)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
scanned.push(relPath);
|
|
156
|
+
if (exemptPaths.has(relPath)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const content = readFileSafe(filePath);
|
|
160
|
+
const meta = detectMetadata(content, requiredMetadataKeys);
|
|
161
|
+
const fileFailures = validateMetadata(meta, requiredMetadataKeys);
|
|
162
|
+
for (const failure of fileFailures) {
|
|
163
|
+
failures.push(`${relPath}: ${failure}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
scanned,
|
|
170
|
+
failures,
|
|
171
|
+
exemptPaths,
|
|
172
|
+
};
|
|
173
|
+
}
|