@nimiplatform/nimi-coding 0.1.0 → 0.2.1
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 +19 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +371 -344
- package/README.zh-CN.md +307 -0
- package/SECURITY.md +26 -0
- package/adapters/oh-my-codex/README.md +8 -9
- package/cli/commands/audit-sweep.mjs +10 -10
- package/cli/commands/classify-spec-tree.mjs +5 -0
- package/cli/commands/closeout.mjs +3 -0
- package/cli/commands/generate-spec-derived-docs.mjs +20 -0
- package/cli/commands/generate-spec-migration-plan.mjs +30 -0
- package/cli/commands/start.mjs +5 -1
- package/cli/commands/surface-validator-command.mjs +49 -0
- package/cli/commands/sweep-design.mjs +295 -0
- package/cli/commands/sweep.mjs +22 -0
- package/cli/commands/sync.mjs +132 -0
- package/cli/commands/topic-formatters.mjs +8 -8
- package/cli/commands/validate-ai-governance.mjs +167 -46
- package/cli/commands/validate-domain-admission.mjs +5 -0
- package/cli/commands/validate-guidance-bodies.mjs +5 -0
- package/cli/commands/validate-placement.mjs +5 -0
- package/cli/commands/validate-projection-edges.mjs +5 -0
- package/cli/commands/validate-spec-audit.mjs +5 -1
- package/cli/commands/validate-table-family.mjs +5 -0
- package/cli/commands/validate-tracked-output-admission.mjs +5 -0
- package/cli/constants.mjs +5 -49
- package/cli/help.mjs +33 -11
- package/cli/index.mjs +20 -2
- package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
- package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
- package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
- package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
- package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
- package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
- package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
- package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
- package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
- package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
- package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
- package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
- package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
- package/cli/lib/authority-convergence.mjs +397 -2
- package/cli/lib/blueprint-audit.mjs +5 -5
- package/cli/lib/closeout.mjs +126 -3
- package/cli/lib/contracts.mjs +21 -17
- package/cli/lib/handoff.mjs +29 -11
- package/cli/lib/high-risk-admission.mjs +60 -11
- package/cli/lib/high-risk-decision.mjs +31 -2
- package/cli/lib/high-risk-ingest.mjs +5 -1
- package/cli/lib/high-risk-review.mjs +5 -1
- package/cli/lib/internal/contracts-parse.mjs +195 -24
- package/cli/lib/internal/contracts-validators.mjs +3 -2
- package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
- package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
- package/cli/lib/internal/doctor-finalize.mjs +12 -8
- package/cli/lib/internal/doctor-inspectors.mjs +34 -1
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
- package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
- package/cli/lib/internal/validators-spec.mjs +229 -20
- package/cli/lib/sweep-design-runtime/common.mjs +246 -0
- package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
- package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
- package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
- package/cli/lib/sweep-design-runtime/results.mjs +324 -0
- package/cli/lib/sweep-design.mjs +8 -0
- package/cli/lib/sync.mjs +143 -0
- package/cli/lib/topic-artifacts.mjs +186 -0
- package/cli/lib/topic-authority-coverage.mjs +73 -0
- package/cli/lib/topic-closeout.mjs +560 -0
- package/cli/lib/topic-common.mjs +404 -0
- package/cli/lib/topic-decisions.mjs +332 -0
- package/cli/lib/topic-draft-packets.mjs +126 -7
- package/cli/lib/topic-execution.mjs +515 -0
- package/cli/lib/topic-goal.mjs +112 -33
- package/cli/lib/topic-ledger.mjs +281 -0
- package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
- package/cli/lib/topic-root-validation.mjs +288 -0
- package/cli/lib/topic-runner-commands.mjs +174 -0
- package/cli/lib/topic-runner-deferral.mjs +532 -0
- package/cli/lib/topic-runner-stale-gates.mjs +114 -0
- package/cli/lib/topic-runner-validation.mjs +138 -0
- package/cli/lib/topic-runner.mjs +109 -154
- package/cli/lib/topic-scaffold.mjs +252 -0
- package/cli/lib/topic-waves.mjs +403 -0
- package/cli/lib/topic.mjs +81 -93
- package/cli/lib/value-helpers.mjs +6 -1
- package/cli/seeds/bootstrap.mjs +96 -20
- package/cli/seeds/seed-policy.yaml +67 -0
- package/config/bootstrap.yaml +1 -1
- package/config/skill-manifest.yaml +4 -2
- package/config/spec-generation-inputs.yaml +41 -19
- package/contracts/audit-remediation-map.schema.yaml +1 -0
- package/contracts/audit-sweep-result.yaml +4 -0
- package/contracts/domain-admission.schema.yaml +56 -0
- package/contracts/migration-inventory.schema.yaml +80 -0
- package/contracts/negative-fixtures.yaml +91 -0
- package/contracts/placement-contract.schema.yaml +163 -0
- package/contracts/projection-edge.schema.yaml +130 -0
- package/contracts/shared-enums.yaml +68 -0
- package/contracts/spec-generation-audit.schema.yaml +19 -4
- package/contracts/spec-generation-inputs.schema.yaml +130 -29
- package/contracts/spec-reconstruction-result.yaml +9 -5
- package/contracts/surface-taxonomy.schema.yaml +201 -0
- package/contracts/sweep-design-result.yaml +349 -0
- package/contracts/table-family.schema.yaml +121 -0
- package/contracts/topic-goal.schema.yaml +10 -1
- package/contracts/tracked-output-admission.schema.yaml +70 -0
- package/contracts/workflow-consumer.schema.yaml +112 -0
- package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
- package/methodology/spec-reconstruction.yaml +53 -30
- package/package.json +19 -4
- package/spec/_meta/command-gating-matrix.yaml +33 -0
- package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
- package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
- package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
- package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
- package/spec/_meta/spec-tree-model.yaml +104 -36
- package/spec/bootstrap-state.yaml +36 -36
- package/spec/product-scope.yaml +13 -10
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
makeValidatorRefusal,
|
|
20
20
|
VALIDATOR_NATIVE_REFUSAL_CODES,
|
|
21
21
|
} from "./validators-shared.mjs";
|
|
22
|
+
import {
|
|
23
|
+
validateDomainAdmission,
|
|
24
|
+
validatePlacement,
|
|
25
|
+
validateTableFamily,
|
|
26
|
+
} from "./surface-taxonomy-validators.mjs";
|
|
22
27
|
import {
|
|
23
28
|
classifyAuditCoveredFiles,
|
|
24
29
|
classifySpecTreeFiles,
|
|
@@ -27,13 +32,136 @@ import {
|
|
|
27
32
|
isSourceRefWithinDeclaredRoots,
|
|
28
33
|
} from "./validators-spec-helpers.mjs";
|
|
29
34
|
|
|
35
|
+
async function loadV2RequiredFiles(projectRoot) {
|
|
36
|
+
const text = await readTextIfFile(path.join(projectRoot, ".nimi", "methodology", "spec-reconstruction.yaml"));
|
|
37
|
+
const parsed = parseYamlText(text);
|
|
38
|
+
return Array.isArray(parsed?.reconstruction?.target_tree_shape?.minimal_required_outputs)
|
|
39
|
+
? parsed.reconstruction.target_tree_shape.minimal_required_outputs.map(String)
|
|
40
|
+
: [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isV2SpecGenerationInputs(specGenerationInputs) {
|
|
44
|
+
return specGenerationInputs.ok && specGenerationInputs.mode === "class_filtered";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function validateSpecTreeV2(rootPath, projectRoot, specGenerationInputs) {
|
|
48
|
+
const errors = [];
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const expectedRoot = path.resolve(projectRoot, specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec");
|
|
51
|
+
const targetRoot = path.resolve(rootPath);
|
|
52
|
+
|
|
53
|
+
if (targetRoot !== expectedRoot) {
|
|
54
|
+
errors.push(`spec tree root mismatch: expected ${expectedRoot} but received ${targetRoot}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = await collectTreeFiles(targetRoot);
|
|
58
|
+
if (files.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
errors: errors.length > 0 ? errors : [`missing spec tree root: ${targetRoot}`],
|
|
62
|
+
warnings,
|
|
63
|
+
refusal: makeValidatorRefusal(
|
|
64
|
+
VALIDATOR_NATIVE_REFUSAL_CODES.SPEC_TREE_MISSING,
|
|
65
|
+
"spec tree root is missing or empty",
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const requiredFiles = await loadV2RequiredFiles(projectRoot);
|
|
71
|
+
const canonicalRoot = specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec";
|
|
72
|
+
const missingRequired = requiredFiles
|
|
73
|
+
.map((entry) => path.posix.relative(canonicalRoot, entry))
|
|
74
|
+
.filter((entry) => !files.includes(entry));
|
|
75
|
+
if (missingRequired.length > 0) {
|
|
76
|
+
errors.push(`missing required canonical files: ${missingRequired.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rootRef = path.relative(projectRoot, targetRoot).split(path.sep).join(path.posix.sep) || ".";
|
|
80
|
+
const placement = await validatePlacement(projectRoot, { rootRef });
|
|
81
|
+
const domainAdmission = await validateDomainAdmission(projectRoot, { rootRef });
|
|
82
|
+
const tableFamily = await validateTableFamily(projectRoot, { rootRef });
|
|
83
|
+
errors.push(...(placement.errors ?? []), ...(domainAdmission.errors ?? []), ...(tableFamily.errors ?? []));
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: errors.length === 0,
|
|
87
|
+
errors,
|
|
88
|
+
warnings,
|
|
89
|
+
refusal: errors.length === 0
|
|
90
|
+
? null
|
|
91
|
+
: makeValidatorRefusal(
|
|
92
|
+
VALIDATOR_NATIVE_REFUSAL_CODES.SPEC_TREE_INVALID,
|
|
93
|
+
`spec tree is invalid: ${path.basename(targetRoot)}`,
|
|
94
|
+
),
|
|
95
|
+
summary: {
|
|
96
|
+
profile: "surface_taxonomy_v1",
|
|
97
|
+
canonicalRoot,
|
|
98
|
+
totalFiles: files.length,
|
|
99
|
+
requiredFiles: requiredFiles.length,
|
|
100
|
+
missingRequired,
|
|
101
|
+
classifiedFiles: placement.summary?.total ?? files.length,
|
|
102
|
+
unexpectedFiles: [],
|
|
103
|
+
conflictingFiles: [],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toPortableProjectPath(projectRoot, absolutePath) {
|
|
109
|
+
return path.relative(projectRoot, absolutePath).split(path.sep).join(path.posix.sep);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pathStartsWithRoot(candidate, root) {
|
|
113
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function generatedOutputNormativeRootOverlaps(specTreeModel) {
|
|
117
|
+
const normativeRoots = specTreeModel.domains.map((domain) => domain.normativeRoot).filter(Boolean);
|
|
118
|
+
const overlaps = [];
|
|
119
|
+
for (const pipeline of specTreeModel.generatedPipelines) {
|
|
120
|
+
for (const outputRoot of pipeline.outputRoots) {
|
|
121
|
+
for (const normativeRoot of normativeRoots) {
|
|
122
|
+
if (pathStartsWithRoot(outputRoot, normativeRoot) || pathStartsWithRoot(normativeRoot, outputRoot)) {
|
|
123
|
+
overlaps.push({
|
|
124
|
+
pipelineId: pipeline.id,
|
|
125
|
+
outputRoot,
|
|
126
|
+
normativeRoot,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return overlaps;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeAuditFileClass(entry) {
|
|
136
|
+
const explicitSurfaceClass = typeof entry.surface_class === "string" ? entry.surface_class : null;
|
|
137
|
+
if (explicitSurfaceClass) {
|
|
138
|
+
return explicitSurfaceClass;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (String(entry.file_class ?? "")) {
|
|
142
|
+
case "kernel_markdown":
|
|
143
|
+
return "product_authority";
|
|
144
|
+
case "kernel_tables":
|
|
145
|
+
return "product_authority_table";
|
|
146
|
+
case "domain_guides":
|
|
147
|
+
case "kernel_generated":
|
|
148
|
+
return "thin_guidance";
|
|
149
|
+
default:
|
|
150
|
+
return String(entry.file_class ?? "");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
30
154
|
export async function validateSpecTree(rootPath, options = {}) {
|
|
31
155
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
32
156
|
const specTreeModel = await loadSpecTreeModelContract(projectRoot);
|
|
157
|
+
const specGenerationInputs = await loadSpecGenerationInputsConfig(projectRoot);
|
|
33
158
|
const errors = [];
|
|
34
159
|
const warnings = [];
|
|
35
160
|
|
|
36
161
|
if (!specTreeModel.ok) {
|
|
162
|
+
if (isV2SpecGenerationInputs(specGenerationInputs)) {
|
|
163
|
+
return validateSpecTreeV2(rootPath, projectRoot, specGenerationInputs);
|
|
164
|
+
}
|
|
37
165
|
return {
|
|
38
166
|
ok: false,
|
|
39
167
|
errors: [`invalid spec tree model contract: ${specTreeModel.path}`],
|
|
@@ -74,6 +202,13 @@ export async function validateSpecTree(rootPath, options = {}) {
|
|
|
74
202
|
errors.push(`missing required canonical files: ${missingRequired.join(", ")}`);
|
|
75
203
|
}
|
|
76
204
|
|
|
205
|
+
const generatedOutputOverlaps = generatedOutputNormativeRootOverlaps(specTreeModel);
|
|
206
|
+
if (generatedOutputOverlaps.length > 0) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`generated output roots overlap normative roots: ${generatedOutputOverlaps.map((entry) => `${entry.pipelineId}:${entry.outputRoot}->${entry.normativeRoot}`).join(", ")}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
77
212
|
for (const domain of specTreeModel.domains) {
|
|
78
213
|
const domainRoot = path.posix.relative(specTreeModel.canonicalRoot, domain.root);
|
|
79
214
|
const normativeRoot = path.posix.relative(specTreeModel.canonicalRoot, domain.normativeRoot);
|
|
@@ -132,10 +267,11 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
132
267
|
const specGenerationInputs = await loadSpecGenerationInputsConfig(projectRoot);
|
|
133
268
|
const blueprintReference = await loadBlueprintReference(projectRoot);
|
|
134
269
|
const auditContract = await loadSpecGenerationAuditContract(projectRoot);
|
|
270
|
+
const usesV2SurfaceModel = isV2SpecGenerationInputs(specGenerationInputs);
|
|
135
271
|
const errors = [];
|
|
136
272
|
const warnings = [];
|
|
137
273
|
|
|
138
|
-
if (!specTreeModel.ok) {
|
|
274
|
+
if (!specTreeModel.ok && !usesV2SurfaceModel) {
|
|
139
275
|
return {
|
|
140
276
|
ok: false,
|
|
141
277
|
errors: [`invalid spec tree model contract: ${specTreeModel.path}`],
|
|
@@ -199,8 +335,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
199
335
|
};
|
|
200
336
|
}
|
|
201
337
|
|
|
202
|
-
if (parsed.version
|
|
203
|
-
errors.push("spec generation audit version must be 1");
|
|
338
|
+
if (![1, 2].includes(parsed.version)) {
|
|
339
|
+
errors.push("spec generation audit version must be 1 or 2");
|
|
204
340
|
}
|
|
205
341
|
|
|
206
342
|
if (String(parsed.contract_ref ?? "") !== SPEC_GENERATION_AUDIT_CONTRACT_REF) {
|
|
@@ -224,20 +360,27 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
224
360
|
: null,
|
|
225
361
|
};
|
|
226
362
|
|
|
227
|
-
if (
|
|
363
|
+
if (usesV2SurfaceModel) {
|
|
364
|
+
if (!["mixed", "class_filtered"].includes(String(audit.generation_mode ?? ""))) {
|
|
365
|
+
errors.push("spec generation audit generation_mode must be `mixed` or `class_filtered`");
|
|
366
|
+
}
|
|
367
|
+
} else if (String(audit.generation_mode ?? "") !== "mixed") {
|
|
228
368
|
errors.push("spec generation audit generation_mode must be `mixed`");
|
|
229
369
|
}
|
|
230
|
-
|
|
231
|
-
|
|
370
|
+
|
|
371
|
+
const canonicalRoot = specTreeModel.ok ? specTreeModel.canonicalRoot : specGenerationInputs.canonicalTargetRoot ?? ".nimi/spec";
|
|
372
|
+
const declaredProfile = specTreeModel.ok ? specTreeModel.profile : "surface_taxonomy_v1";
|
|
373
|
+
if (String(audit.canonical_target_root ?? "") !== canonicalRoot) {
|
|
374
|
+
errors.push(`spec generation audit canonical_target_root must be ${canonicalRoot}`);
|
|
232
375
|
}
|
|
233
|
-
if (String(audit.declared_profile ?? "") !==
|
|
234
|
-
errors.push(`spec generation audit declared_profile must be ${
|
|
376
|
+
if (String(audit.declared_profile ?? "") !== declaredProfile) {
|
|
377
|
+
errors.push(`spec generation audit declared_profile must be ${declaredProfile}`);
|
|
235
378
|
}
|
|
236
379
|
if (!isDeclaredInputsCompatibleWithConfig(declaredInputs, specGenerationInputs, blueprintReference)) {
|
|
237
380
|
errors.push("spec generation audit input_roots must stay within the declared generation inputs and optional benchmark root");
|
|
238
381
|
}
|
|
239
382
|
|
|
240
|
-
const canonicalRootPath = path.resolve(projectRoot,
|
|
383
|
+
const canonicalRootPath = path.resolve(projectRoot, canonicalRoot);
|
|
241
384
|
const treeFiles = await collectTreeFiles(canonicalRootPath);
|
|
242
385
|
if (treeFiles.length === 0) {
|
|
243
386
|
return {
|
|
@@ -251,7 +394,14 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
251
394
|
};
|
|
252
395
|
}
|
|
253
396
|
|
|
254
|
-
const { auditedFiles, classifications } =
|
|
397
|
+
const { auditedFiles, classifications } = specTreeModel.ok
|
|
398
|
+
? classifyAuditCoveredFiles(treeFiles, specTreeModel)
|
|
399
|
+
: {
|
|
400
|
+
auditedFiles: treeFiles
|
|
401
|
+
.filter((entry) => !entry.startsWith("_meta/"))
|
|
402
|
+
.map((entry) => ({ path: entry, category: "surface_taxonomy_v1" })),
|
|
403
|
+
classifications: { unexpected: [], conflicts: [] },
|
|
404
|
+
};
|
|
255
405
|
if (classifications.unexpected.length > 0) {
|
|
256
406
|
errors.push(`spec tree contains unexpected files outside declared spec classes: ${classifications.unexpected.join(", ")}`);
|
|
257
407
|
}
|
|
@@ -263,27 +413,82 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
263
413
|
if (!Array.isArray(audit.files)) {
|
|
264
414
|
errors.push("spec generation audit files must be an array");
|
|
265
415
|
}
|
|
416
|
+
const fileEntryRefs = audit.file_entry_refs === undefined
|
|
417
|
+
? []
|
|
418
|
+
: Array.isArray(audit.file_entry_refs)
|
|
419
|
+
? audit.file_entry_refs.map(String)
|
|
420
|
+
: null;
|
|
421
|
+
if (fileEntryRefs === null) {
|
|
422
|
+
errors.push("spec generation audit file_entry_refs must be an array when present");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const referencedFileEntries = [];
|
|
426
|
+
for (const entryRef of fileEntryRefs ?? []) {
|
|
427
|
+
if (entryRef.length === 0 || path.isAbsolute(entryRef)) {
|
|
428
|
+
errors.push(`spec generation audit file_entry_refs must be non-empty repository-relative paths: ${entryRef}`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const absoluteEntryRef = path.resolve(projectRoot, entryRef);
|
|
432
|
+
const relativeEntryRef = path.relative(projectRoot, absoluteEntryRef).split(path.sep).join(path.posix.sep);
|
|
433
|
+
const expectedPrefix = usesV2SurfaceModel
|
|
434
|
+
? ".nimi/local/state/spec-generation/spec-generation-audit/"
|
|
435
|
+
: `${specTreeModel.canonicalRoot}/_meta/spec-generation-audit/`;
|
|
436
|
+
if (relativeEntryRef !== entryRef || !relativeEntryRef.startsWith(expectedPrefix)) {
|
|
437
|
+
errors.push(`spec generation audit file_entry_ref must stay under ${expectedPrefix}: ${entryRef}`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const entryText = await readTextIfFile(absoluteEntryRef);
|
|
441
|
+
if (entryText === null) {
|
|
442
|
+
errors.push(`spec generation audit file_entry_ref is missing: ${entryRef}`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const entryParsed = parseYamlText(entryText);
|
|
446
|
+
const entryPayload = entryParsed?.spec_generation_audit_file_entries;
|
|
447
|
+
if (!isPlainObject(entryParsed) || !isPlainObject(entryPayload)) {
|
|
448
|
+
errors.push(`spec generation audit file_entry_ref is not a valid entry shard: ${entryRef}`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const expectedShardVersion = usesV2SurfaceModel ? 2 : 1;
|
|
452
|
+
if (entryParsed.version !== expectedShardVersion) {
|
|
453
|
+
errors.push(`spec generation audit file_entry_ref version must be ${expectedShardVersion}: ${entryRef}`);
|
|
454
|
+
}
|
|
455
|
+
if (String(entryParsed.contract_ref ?? "") !== SPEC_GENERATION_AUDIT_CONTRACT_REF) {
|
|
456
|
+
errors.push(`spec generation audit file_entry_ref contract_ref must be ${SPEC_GENERATION_AUDIT_CONTRACT_REF}: ${entryRef}`);
|
|
457
|
+
}
|
|
458
|
+
if (String(entryPayload.parent_ref ?? "") !== toPortableProjectPath(projectRoot, absoluteAuditPath)) {
|
|
459
|
+
errors.push(`spec generation audit file_entry_ref parent_ref must point to ${toPortableProjectPath(projectRoot, absoluteAuditPath)}: ${entryRef}`);
|
|
460
|
+
}
|
|
461
|
+
if (!Array.isArray(entryPayload.files)) {
|
|
462
|
+
errors.push(`spec generation audit file_entry_ref files must be an array: ${entryRef}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
referencedFileEntries.push(...entryPayload.files);
|
|
466
|
+
}
|
|
467
|
+
const allFileEntries = [...fileEntries, ...referencedFileEntries];
|
|
266
468
|
|
|
267
469
|
const auditEntryByRelativePath = new Map();
|
|
268
|
-
for (const entry of
|
|
470
|
+
for (const entry of allFileEntries) {
|
|
269
471
|
if (!isPlainObject(entry)) {
|
|
270
472
|
errors.push("spec generation audit file entries must be mappings");
|
|
271
473
|
continue;
|
|
272
474
|
}
|
|
273
475
|
|
|
274
|
-
const
|
|
476
|
+
const requiredEntryFields = auditContract.requiredFileEntryFields?.length > 0
|
|
477
|
+
? auditContract.requiredFileEntryFields
|
|
478
|
+
: SPEC_GENERATION_AUDIT_FILE_REQUIRED_FIELDS;
|
|
479
|
+
const missingEntryFields = requiredEntryFields.filter((field) => !(field in entry));
|
|
275
480
|
if (missingEntryFields.length > 0) {
|
|
276
481
|
errors.push(`spec generation audit file entry is missing required fields: ${missingEntryFields.join(", ")}`);
|
|
277
482
|
continue;
|
|
278
483
|
}
|
|
279
484
|
|
|
280
485
|
const canonicalPath = String(entry.canonical_path ?? "");
|
|
281
|
-
if (!canonicalPath.startsWith(`${
|
|
282
|
-
errors.push(`spec generation audit canonical_path must stay under ${
|
|
486
|
+
if (!canonicalPath.startsWith(`${canonicalRoot}/`) && canonicalPath !== `${canonicalRoot}/INDEX.md`) {
|
|
487
|
+
errors.push(`spec generation audit canonical_path must stay under ${canonicalRoot}: ${canonicalPath}`);
|
|
283
488
|
continue;
|
|
284
489
|
}
|
|
285
490
|
|
|
286
|
-
const relativePath = path.posix.relative(
|
|
491
|
+
const relativePath = path.posix.relative(canonicalRoot, canonicalPath);
|
|
287
492
|
if (relativePath.startsWith("_meta/")) {
|
|
288
493
|
errors.push(`spec generation audit must not record _meta files as generated canonical files: ${canonicalPath}`);
|
|
289
494
|
continue;
|
|
@@ -331,8 +536,11 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
331
536
|
}
|
|
332
537
|
|
|
333
538
|
const missingAuditEntries = [];
|
|
334
|
-
const
|
|
335
|
-
|
|
539
|
+
const requiredFileRefs = specTreeModel.ok
|
|
540
|
+
? (specTreeModel.requiredFilesByProfile[specTreeModel.profile] ?? [])
|
|
541
|
+
: await loadV2RequiredFiles(projectRoot);
|
|
542
|
+
const requiredFiles = requiredFileRefs
|
|
543
|
+
.map((entry) => path.posix.relative(canonicalRoot, entry))
|
|
336
544
|
.filter((entry) => !entry.startsWith("_meta/"));
|
|
337
545
|
|
|
338
546
|
for (const classifiedFile of auditedFiles) {
|
|
@@ -342,7 +550,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
342
550
|
continue;
|
|
343
551
|
}
|
|
344
552
|
|
|
345
|
-
|
|
553
|
+
const auditFileClass = normalizeAuditFileClass(auditEntry);
|
|
554
|
+
if (classifiedFile.classId && auditFileClass !== classifiedFile.classId && !(classifiedFile.path === "INDEX.md" && auditFileClass === "index")) {
|
|
346
555
|
errors.push(`spec generation audit file_class does not match canonical tree classification for ${classifiedFile.path}: expected ${classifiedFile.classId}`);
|
|
347
556
|
}
|
|
348
557
|
}
|
|
@@ -395,8 +604,8 @@ export async function validateSpecAudit(auditPath, options = {}) {
|
|
|
395
604
|
`spec generation audit is invalid: ${path.basename(absoluteAuditPath)}`,
|
|
396
605
|
),
|
|
397
606
|
summary: {
|
|
398
|
-
canonicalRoot
|
|
399
|
-
declaredProfile
|
|
607
|
+
canonicalRoot,
|
|
608
|
+
declaredProfile,
|
|
400
609
|
auditedFiles: auditEntryByRelativePath.size,
|
|
401
610
|
requiredAuditedFiles: requiredFiles.length,
|
|
402
611
|
missingAuditEntries,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
|
|
7
|
+
import { pathExists } from "../fs-helpers.mjs";
|
|
8
|
+
import { isPlainObject } from "../value-helpers.mjs";
|
|
9
|
+
|
|
10
|
+
export const DESIGN_ROOT = ".nimi/local/sweep-design";
|
|
11
|
+
export const DESIGN_STATES = new Set([
|
|
12
|
+
"raw",
|
|
13
|
+
"confirmed",
|
|
14
|
+
"duplicate",
|
|
15
|
+
"superseded",
|
|
16
|
+
"false_positive",
|
|
17
|
+
"needs_more_audit",
|
|
18
|
+
"needs_user_decision",
|
|
19
|
+
"needs_authority_alignment",
|
|
20
|
+
"needs_design",
|
|
21
|
+
"ready_for_implementation_wave",
|
|
22
|
+
"blocked",
|
|
23
|
+
]);
|
|
24
|
+
export const TERMINAL_STATES = new Set(["duplicate", "superseded", "false_positive"]);
|
|
25
|
+
export const TRANSIENT_STATES = new Set(["raw", "confirmed", "needs_design"]);
|
|
26
|
+
export const FINAL_OUTCOME_STATES = new Set([
|
|
27
|
+
"duplicate",
|
|
28
|
+
"superseded",
|
|
29
|
+
"false_positive",
|
|
30
|
+
"needs_more_audit",
|
|
31
|
+
"needs_user_decision",
|
|
32
|
+
"needs_authority_alignment",
|
|
33
|
+
"ready_for_implementation_wave",
|
|
34
|
+
"blocked",
|
|
35
|
+
]);
|
|
36
|
+
export const AUDITOR_FAMILIES = new Set([
|
|
37
|
+
"anthropic_claude",
|
|
38
|
+
"openai_gpt",
|
|
39
|
+
"openai_codex",
|
|
40
|
+
"google_gemini",
|
|
41
|
+
"xai_grok",
|
|
42
|
+
"meta_llama",
|
|
43
|
+
"mistral",
|
|
44
|
+
"other",
|
|
45
|
+
]);
|
|
46
|
+
export const AUDITOR_MODES = new Set(["focused", "all", "degraded"]);
|
|
47
|
+
export const AUDITOR_RESULT_ORIGINS = new Set(["llm_session", "external_llm_session", "synthetic_trial"]);
|
|
48
|
+
export const LLM_AUDITOR_RESULT_ORIGINS = new Set(["llm_session", "external_llm_session"]);
|
|
49
|
+
export const PRIOR_DESIGN_STATE_MARKERS = new Set([
|
|
50
|
+
"empty",
|
|
51
|
+
"present",
|
|
52
|
+
"partial",
|
|
53
|
+
"superseded_by_later_audit",
|
|
54
|
+
"evidence_gap",
|
|
55
|
+
]);
|
|
56
|
+
export const REVISION_TYPES = new Set([
|
|
57
|
+
"finding_state_revision",
|
|
58
|
+
"duplicate_judgement",
|
|
59
|
+
"superseded_judgement",
|
|
60
|
+
"cluster_create",
|
|
61
|
+
"cluster_merge",
|
|
62
|
+
"cluster_split",
|
|
63
|
+
"cluster_retire",
|
|
64
|
+
"cluster_reopen",
|
|
65
|
+
"finding_move",
|
|
66
|
+
"wave_create",
|
|
67
|
+
"wave_merge",
|
|
68
|
+
"wave_split",
|
|
69
|
+
"wave_retract",
|
|
70
|
+
"wave_demote",
|
|
71
|
+
"wave_block",
|
|
72
|
+
"wave_implementation_ready",
|
|
73
|
+
"wave_dependency_rewrite",
|
|
74
|
+
"wave_validation_or_closeout_strengthening",
|
|
75
|
+
"decision_packet_create",
|
|
76
|
+
"extra_audit_request_create",
|
|
77
|
+
"extra_audit_request_close",
|
|
78
|
+
"human_decision_request_create",
|
|
79
|
+
"human_decision_request_resolve",
|
|
80
|
+
"final_state_projection_update",
|
|
81
|
+
"user_decision_queue_rewrite",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
export function toPosix(filePath) {
|
|
85
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function relPath(projectRoot, absolutePath) {
|
|
89
|
+
return toPosix(path.relative(projectRoot, absolutePath));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function safeDesignId(value) {
|
|
93
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{2,140}$/.test(value)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function deriveRunId(sweepId) {
|
|
100
|
+
return `sweep-design-${String(sweepId).replace(/[^a-zA-Z0-9._-]+/g, "-")}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function designRef(runId, ...parts) {
|
|
104
|
+
return path.posix.join(DESIGN_ROOT, runId, ...parts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function artifactPath(projectRoot, ref) {
|
|
108
|
+
return path.join(projectRoot, ...ref.split("/"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function inputError(error) {
|
|
112
|
+
return { ok: false, inputError: true, exitCode: 2, error };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function loadYamlPath(filePath) {
|
|
116
|
+
try {
|
|
117
|
+
const text = await readFile(filePath, "utf8");
|
|
118
|
+
return YAML.parse(text);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function sha256Text(text) {
|
|
125
|
+
return createHash("sha256").update(text).digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function stableStringify(value) {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
131
|
+
}
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
134
|
+
}
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function sha256Object(value) {
|
|
139
|
+
return sha256Text(stableStringify(value));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function loadYamlRef(projectRoot, ref) {
|
|
143
|
+
return loadYamlPath(artifactPath(projectRoot, ref));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function writeYamlRef(projectRoot, ref, value) {
|
|
147
|
+
const destination = artifactPath(projectRoot, ref);
|
|
148
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
149
|
+
await writeFile(destination, YAML.stringify(value, { aliasDuplicateObjects: false }), "utf8");
|
|
150
|
+
return ref;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function assertDesignArtifact(projectRoot, ref, kind, label) {
|
|
154
|
+
const value = await loadYamlRef(projectRoot, ref);
|
|
155
|
+
if (!isPlainObject(value) || value.kind !== kind) {
|
|
156
|
+
return inputError(`nimicoding sweep design refused: ${label} is missing or malformed.\n`);
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, value, ref };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function auditFindingsRef(sweepId) {
|
|
162
|
+
return `.nimi/local/audit/evidence/${sweepId}/findings.yaml`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function loadAuditFindings(projectRoot, sweepId) {
|
|
166
|
+
const ref = auditFindingsRef(sweepId);
|
|
167
|
+
const sourcePath = artifactPath(projectRoot, ref);
|
|
168
|
+
const info = await pathExists(sourcePath);
|
|
169
|
+
if (!info || !info.isFile()) {
|
|
170
|
+
return inputError(`nimicoding sweep design refused: audit findings not found for ${sweepId}.\n`);
|
|
171
|
+
}
|
|
172
|
+
const sourceText = await readFile(sourcePath, "utf8");
|
|
173
|
+
const store = YAML.parse(sourceText);
|
|
174
|
+
if (!isPlainObject(store) || store.kind !== "audit-findings" || store.sweep_id !== sweepId || !Array.isArray(store.findings)) {
|
|
175
|
+
return inputError(`nimicoding sweep design refused: audit findings are malformed for ${sweepId}.\n`);
|
|
176
|
+
}
|
|
177
|
+
return { ok: true, ref, store, sourceSha256: sha256Text(sourceText) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function findingOwnerDomain(finding) {
|
|
181
|
+
if (typeof finding.owner_domain === "string" && finding.owner_domain.length > 0) {
|
|
182
|
+
return finding.owner_domain;
|
|
183
|
+
}
|
|
184
|
+
const file = finding.location?.file;
|
|
185
|
+
if (typeof file === "string" && file.includes("/")) {
|
|
186
|
+
return file.split("/")[0];
|
|
187
|
+
}
|
|
188
|
+
return "root";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function findingAuthorityRef(finding) {
|
|
192
|
+
return finding.root_cause?.authority_ref ?? finding.authority_ref ?? null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function findingRepairTarget(finding) {
|
|
196
|
+
return finding.root_cause?.repair_target ?? finding.location?.file ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function findingCodeRefs(finding) {
|
|
200
|
+
return [
|
|
201
|
+
finding.location?.file,
|
|
202
|
+
...(Array.isArray(finding.implementation_refs) ? finding.implementation_refs : []),
|
|
203
|
+
].filter((value, index, array) => typeof value === "string" && value.length > 0 && array.indexOf(value) === index);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeFindingForDesign(finding, sourceFindingsRef) {
|
|
207
|
+
return {
|
|
208
|
+
finding_id: finding.id,
|
|
209
|
+
source_audit_sweep_id: finding.sweep_id,
|
|
210
|
+
source_chunk_id: finding.chunk_id ?? null,
|
|
211
|
+
source_finding_ref: `${sourceFindingsRef}#${finding.id}`,
|
|
212
|
+
fingerprint: finding.fingerprint ?? null,
|
|
213
|
+
severity: finding.severity ?? null,
|
|
214
|
+
category: finding.category ?? null,
|
|
215
|
+
actionability: finding.actionability ?? null,
|
|
216
|
+
confidence: finding.confidence ?? null,
|
|
217
|
+
title: finding.title ?? null,
|
|
218
|
+
owner_domain: findingOwnerDomain(finding),
|
|
219
|
+
authority_ref: findingAuthorityRef(finding),
|
|
220
|
+
evidence_refs: Array.isArray(finding.implementation_refs) ? finding.implementation_refs : [],
|
|
221
|
+
repair_target: findingRepairTarget(finding),
|
|
222
|
+
location: finding.location ?? null,
|
|
223
|
+
root_cause_key: finding.root_cause?.key ?? null,
|
|
224
|
+
contract_seam: finding.root_cause?.contract_seam ?? null,
|
|
225
|
+
impact: finding.impact ?? null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function requireRunId(options) {
|
|
230
|
+
const runId = safeDesignId(options.runId);
|
|
231
|
+
if (!runId) {
|
|
232
|
+
return inputError("nimicoding sweep design refused: --run-id is required.\n");
|
|
233
|
+
}
|
|
234
|
+
return { ok: true, runId };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function nowIso() {
|
|
238
|
+
return new Date().toISOString();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function slug(value) {
|
|
242
|
+
return String(value ?? "item")
|
|
243
|
+
.toLowerCase()
|
|
244
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
245
|
+
.replace(/^-+|-+$/g, "") || "item";
|
|
246
|
+
}
|