@nimiplatform/nimi-coding 0.3.1 → 0.5.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 +32 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +297 -41
- package/README.zh-CN.md +306 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +804 -0
- package/cli/commands/clear.mjs +27 -7
- package/cli/commands/doctor.mjs +10 -1
- package/cli/commands/start.mjs +23 -27
- package/cli/commands/sync.mjs +21 -101
- package/cli/constants.mjs +7 -31
- package/cli/dev/check-authority-surface.mjs +182 -0
- package/cli/dev/check-product-conformance.mjs +889 -0
- package/cli/help.mjs +10 -17
- package/cli/index.mjs +6 -47
- package/cli/lib/authority/anchors.mjs +491 -0
- package/cli/lib/authority/audit.mjs +567 -0
- package/cli/lib/authority/change-candidates.mjs +807 -0
- package/cli/lib/authority/closed-sets.mjs +645 -0
- package/cli/lib/authority/compile.mjs +92 -0
- package/cli/lib/authority/compiled-snapshot.mjs +143 -0
- package/cli/lib/authority/consumers.mjs +520 -0
- package/cli/lib/authority/diagnostics.mjs +93 -0
- package/cli/lib/authority/diff.mjs +310 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/format.mjs +169 -0
- package/cli/lib/authority/git-snapshot.mjs +720 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +125 -0
- package/cli/lib/authority/lexicon.mjs +55 -0
- package/cli/lib/authority/query.mjs +162 -0
- package/cli/lib/authority/repository-corpus.mjs +37 -0
- package/cli/lib/authority/repository-inventory.mjs +34 -0
- package/cli/lib/authority/review.mjs +212 -0
- package/cli/lib/authority/sarif.mjs +174 -0
- package/cli/lib/authority/scope-bindings.mjs +225 -0
- package/cli/lib/authority/source-map.mjs +105 -0
- package/cli/lib/authority/source-markdown.mjs +167 -0
- package/cli/lib/authority/source-yaml.mjs +308 -0
- package/cli/lib/authority/surface.mjs +97 -0
- package/cli/lib/authority/terms.mjs +422 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +300 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +104 -105
- package/cli/lib/fs-helpers.mjs +139 -15
- package/cli/lib/internal/governance/config.mjs +0 -36
- package/cli/lib/shared.mjs +8 -21
- package/cli/lib/sync.mjs +81 -92
- package/cli/lib/value-helpers.mjs +0 -26
- package/cli/lib/yaml-helpers.mjs +1 -124
- package/cli/seeds/bootstrap.mjs +31 -87
- package/cli/seeds/seed-policy.yaml +9 -63
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +58 -0
- package/contracts/authority-verifier-bindings.schema.yaml +31 -0
- package/methodology/authority-authoring.yaml +127 -0
- package/methodology/core.yaml +34 -27
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +89 -0
- package/spec/authority-authoring.yaml +137 -0
- package/spec/authority-change-candidates.yaml +146 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +77 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +61 -0
- package/spec/authority-review.yaml +71 -0
- package/spec/authority-terms.yaml +63 -0
- package/spec/product-scope.yaml +37 -23
- package/cli/commands/blueprint-audit.mjs +0 -91
- package/cli/commands/classify-spec-tree.mjs +0 -5
- package/cli/commands/generate-spec-derived-docs.mjs +0 -151
- package/cli/commands/generate-spec-migration-plan.mjs +0 -30
- package/cli/commands/surface-validator-command.mjs +0 -49
- package/cli/commands/validate-domain-admission.mjs +0 -5
- package/cli/commands/validate-guidance-bodies.mjs +0 -5
- package/cli/commands/validate-placement.mjs +0 -5
- package/cli/commands/validate-projection-edges.mjs +0 -5
- package/cli/commands/validate-spec-audit.mjs +0 -27
- package/cli/commands/validate-spec-governance.mjs +0 -144
- package/cli/commands/validate-spec-tree.mjs +0 -27
- package/cli/commands/validate-table-family.mjs +0 -5
- package/cli/commands/validate-tracked-output-admission.mjs +0 -5
- package/cli/lib/blueprint-audit.mjs +0 -363
- package/cli/lib/contracts.mjs +0 -21
- package/cli/lib/internal/contracts-loaders.mjs +0 -47
- package/cli/lib/internal/contracts-parse.mjs +0 -192
- package/cli/lib/internal/governance/runner.mjs +0 -35
- package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
- package/cli/lib/internal/validators-shared.mjs +0 -12
- package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
- package/cli/lib/internal/validators-spec.mjs +0 -380
- package/cli/lib/validators.mjs +0 -30
- package/config/bootstrap.yaml +0 -6
- package/config/spec-generation-inputs.yaml +0 -41
- package/contracts/domain-admission.schema.yaml +0 -56
- package/contracts/migration-inventory.schema.yaml +0 -15
- package/contracts/negative-fixtures.yaml +0 -26
- package/contracts/placement-contract.schema.yaml +0 -127
- package/contracts/projection-edge.schema.yaml +0 -46
- package/contracts/shared-enums.yaml +0 -63
- package/contracts/spec-generation-audit.schema.yaml +0 -46
- package/contracts/spec-generation-inputs.schema.yaml +0 -43
- package/contracts/spec-layout.schema.yaml +0 -32
- package/contracts/surface-taxonomy.schema.yaml +0 -139
- package/contracts/table-family.schema.yaml +0 -133
- package/contracts/tracked-output-admission.schema.yaml +0 -17
- package/methodology/four-closure-policy.yaml +0 -20
- package/methodology/role-separation-policy.yaml +0 -22
- package/methodology/spec-reconstruction.yaml +0 -69
- package/spec/_meta/spec-tree-model.yaml +0 -44
|
@@ -1,1147 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
|
|
5
|
-
import { pathExists } from "../fs-helpers.mjs";
|
|
6
|
-
import { parseYamlText } from "../yaml-helpers.mjs";
|
|
7
|
-
import { isPlainObject } from "../value-helpers.mjs";
|
|
8
|
-
|
|
9
|
-
const SURFACE_RESULT_CONTRACT = "nimicoding.surface-validator-result.v2";
|
|
10
|
-
|
|
11
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
12
|
-
|
|
13
|
-
const CONTRACT_REFS = {
|
|
14
|
-
sharedEnums: "contracts/shared-enums.yaml",
|
|
15
|
-
surfaceTaxonomy: "contracts/surface-taxonomy.schema.yaml",
|
|
16
|
-
placement: "contracts/placement-contract.schema.yaml",
|
|
17
|
-
projectionEdge: "contracts/projection-edge.schema.yaml",
|
|
18
|
-
tableFamily: "contracts/table-family.schema.yaml",
|
|
19
|
-
domainAdmission: "contracts/domain-admission.schema.yaml",
|
|
20
|
-
trackedOutputAdmission: "contracts/tracked-output-admission.schema.yaml",
|
|
21
|
-
specLayout: "contracts/spec-layout.schema.yaml",
|
|
22
|
-
negativeFixtures: "contracts/negative-fixtures.yaml",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const TEXT_RULE_BODY_PATTERN = /\bMUST(?:\s+NOT)?\b|\bmust\s+not\b|必须|不得|fail(?:s|ed)?\s+closed/i;
|
|
26
|
-
const PRODUCT_RULE_DEFINITION_PATTERN = /^\s*(?:[-*]\s*)?(?:rule_id|rule id)\s*[:|]/im;
|
|
27
|
-
const GENERATED_REF_PATTERN = /\.nimi\/spec\/[^)\s]+\/kernel\/generated\/|\.nimi\/spec\/generated\/|kernel\/generated\//;
|
|
28
|
-
|
|
29
|
-
function toPosix(value) {
|
|
30
|
-
return value.split(path.sep).join(path.posix.sep);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function relativeRef(projectRoot, absolutePath) {
|
|
34
|
-
return toPosix(path.relative(projectRoot, absolutePath));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function readYamlAt(relativePath) {
|
|
38
|
-
const absolutePath = path.join(PACKAGE_ROOT, relativePath);
|
|
39
|
-
const text = await readFile(absolutePath, "utf8");
|
|
40
|
-
return {
|
|
41
|
-
path: path.relative(PACKAGE_ROOT, absolutePath),
|
|
42
|
-
text,
|
|
43
|
-
data: parseYamlText(text),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function readHostYamlAt(projectRoot, relativePath) {
|
|
48
|
-
const absolutePath = path.join(projectRoot, relativePath);
|
|
49
|
-
const exists = await pathExists(absolutePath);
|
|
50
|
-
if (!exists?.isFile()) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
const text = await readFile(absolutePath, "utf8");
|
|
54
|
-
return {
|
|
55
|
-
path: relativePath,
|
|
56
|
-
text,
|
|
57
|
-
data: parseYamlText(text),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function loadSurfaceContracts(projectRoot) {
|
|
62
|
-
const entries = await Promise.all(Object.entries(CONTRACT_REFS).map(async ([key, ref]) => [key, await readYamlAt(ref)]));
|
|
63
|
-
const contracts = Object.fromEntries(entries);
|
|
64
|
-
const hostDomainAdmission = await readHostYamlAt(projectRoot, ".nimi/contracts/domain-admission.schema.yaml");
|
|
65
|
-
if (hostDomainAdmission) {
|
|
66
|
-
contracts.domainAdmission = hostDomainAdmission;
|
|
67
|
-
}
|
|
68
|
-
const hostSpecLayout = await readHostYamlAt(projectRoot, ".nimi/config/spec-layout.yaml");
|
|
69
|
-
contracts.hostSpecLayout = parseHostSpecLayout(
|
|
70
|
-
hostSpecLayout,
|
|
71
|
-
contracts.specLayout,
|
|
72
|
-
contracts.tableFamily,
|
|
73
|
-
);
|
|
74
|
-
return contracts;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isPortableRef(ref) {
|
|
78
|
-
return typeof ref === "string"
|
|
79
|
-
&& ref.length > 0
|
|
80
|
-
&& !path.posix.isAbsolute(ref)
|
|
81
|
-
&& !ref.startsWith("./")
|
|
82
|
-
&& !ref.split("/").includes("..")
|
|
83
|
-
&& path.posix.normalize(ref) === ref;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isRefWithin(ref, root) {
|
|
87
|
-
return ref === root || ref.startsWith(`${root}/`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function hasExactFields(value, requiredFields) {
|
|
91
|
-
if (!isPlainObject(value)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
const actual = Object.keys(value).sort();
|
|
95
|
-
const expected = [...requiredFields].sort();
|
|
96
|
-
return actual.length === expected.length
|
|
97
|
-
&& actual.every((field, index) => field === expected[index]);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isLiteralPortableRef(ref) {
|
|
101
|
-
return isPortableRef(ref) && !/[\*{}]/.test(ref);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function isGeneratedProjectionRoot(ref, canonicalRoot) {
|
|
105
|
-
return ref === `${canonicalRoot}/generated`
|
|
106
|
-
|| ref.startsWith(`${canonicalRoot}/generated/`)
|
|
107
|
-
|| /^\.nimi\/spec\/[^/]+\/kernel\/generated(?:\/.*)?$/.test(ref);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseHostSpecLayout(hostConfig, schemaContract, tableFamilyContract) {
|
|
111
|
-
if (!hostConfig) {
|
|
112
|
-
return {
|
|
113
|
-
ok: true,
|
|
114
|
-
present: false,
|
|
115
|
-
errors: [],
|
|
116
|
-
canonicalRoot: ".nimi/spec",
|
|
117
|
-
hostInstructionPaths: [],
|
|
118
|
-
trackedDerivedProjections: [],
|
|
119
|
-
tableFamilyExtensions: [],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const errors = [];
|
|
124
|
-
const parsed = hostConfig.data;
|
|
125
|
-
const layout = parsed?.spec_layout;
|
|
126
|
-
const requiredDocumentFields = Array.isArray(schemaContract.data?.required_top_level_fields)
|
|
127
|
-
? schemaContract.data.required_top_level_fields.map(String)
|
|
128
|
-
: [];
|
|
129
|
-
const requiredLayoutFields = Array.isArray(schemaContract.data?.layout_required_fields)
|
|
130
|
-
? schemaContract.data.layout_required_fields.map(String)
|
|
131
|
-
: [];
|
|
132
|
-
const requiredProjectionFields = Array.isArray(schemaContract.data?.tracked_derived_projection_required_fields)
|
|
133
|
-
? schemaContract.data.tracked_derived_projection_required_fields.map(String)
|
|
134
|
-
: [];
|
|
135
|
-
const requiredFamilyFields = Array.isArray(schemaContract.data?.table_family_extension_required_fields)
|
|
136
|
-
? schemaContract.data.table_family_extension_required_fields.map(String)
|
|
137
|
-
: [];
|
|
138
|
-
const packageFamilyIds = new Set(
|
|
139
|
-
(tableFamilyContract.data?.table_families ?? []).map((entry) => String(entry.table_family)),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if (parsed?.version !== 1 || parsed?.contract_ref !== ".nimi/contracts/spec-layout.schema.yaml" || !isPlainObject(layout)) {
|
|
143
|
-
errors.push("invalid_spec_layout_config: .nimi/config/spec-layout.yaml");
|
|
144
|
-
}
|
|
145
|
-
if (!hasExactFields(parsed, requiredDocumentFields)) {
|
|
146
|
-
errors.push("invalid_spec_layout_document_fields: .nimi/config/spec-layout.yaml");
|
|
147
|
-
}
|
|
148
|
-
if (!hasExactFields(layout, requiredLayoutFields)) {
|
|
149
|
-
errors.push("invalid_spec_layout_fields: .nimi/config/spec-layout.yaml");
|
|
150
|
-
}
|
|
151
|
-
for (const field of requiredLayoutFields) {
|
|
152
|
-
if (!(field in (layout ?? {}))) {
|
|
153
|
-
errors.push(`missing_spec_layout_field: .nimi/config/spec-layout.yaml: ${field}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const canonicalRoot = typeof layout?.canonical_root === "string" ? layout.canonical_root : ".nimi/spec";
|
|
158
|
-
if (!isPortableRef(canonicalRoot) || canonicalRoot !== ".nimi/spec") {
|
|
159
|
-
errors.push(`invalid_spec_layout_canonical_root: ${canonicalRoot}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!Array.isArray(layout?.host_instruction_paths)) {
|
|
163
|
-
errors.push("invalid_host_instruction_paths: .nimi/config/spec-layout.yaml");
|
|
164
|
-
}
|
|
165
|
-
const hostInstructionPaths = Array.isArray(layout?.host_instruction_paths)
|
|
166
|
-
? layout.host_instruction_paths.map(String)
|
|
167
|
-
: [];
|
|
168
|
-
const seenInstructionPaths = new Set();
|
|
169
|
-
for (const ref of hostInstructionPaths) {
|
|
170
|
-
if (!isPortableRef(ref)
|
|
171
|
-
|| !ref.startsWith(`${canonicalRoot}/`)
|
|
172
|
-
|| ref === `${canonicalRoot}/INDEX.md`
|
|
173
|
-
|| ref.includes("/kernel/")
|
|
174
|
-
|| !ref.endsWith(".md")
|
|
175
|
-
|| /[\*{}]/.test(path.posix.basename(ref))) {
|
|
176
|
-
errors.push(`invalid_host_instruction_path: ${ref}`);
|
|
177
|
-
}
|
|
178
|
-
if (seenInstructionPaths.has(ref)) {
|
|
179
|
-
errors.push(`duplicate_host_instruction_path: ${ref}`);
|
|
180
|
-
}
|
|
181
|
-
seenInstructionPaths.add(ref);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!Array.isArray(layout?.tracked_derived_projections)) {
|
|
185
|
-
errors.push("invalid_tracked_derived_projections: .nimi/config/spec-layout.yaml");
|
|
186
|
-
}
|
|
187
|
-
const trackedDerivedProjections = Array.isArray(layout?.tracked_derived_projections)
|
|
188
|
-
? layout.tracked_derived_projections
|
|
189
|
-
: [];
|
|
190
|
-
const seenProjectionIds = new Set();
|
|
191
|
-
const seenProjectionRoots = new Set();
|
|
192
|
-
for (const projection of trackedDerivedProjections) {
|
|
193
|
-
if (!isPlainObject(projection)) {
|
|
194
|
-
errors.push("invalid_tracked_derived_projection: non-object entry");
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (!hasExactFields(projection, requiredProjectionFields)) {
|
|
198
|
-
errors.push(`invalid_tracked_derived_projection_fields: ${String(projection.projection_id ?? "unknown")}`);
|
|
199
|
-
}
|
|
200
|
-
for (const field of requiredProjectionFields) {
|
|
201
|
-
if (!(field in projection)) {
|
|
202
|
-
errors.push(`missing_tracked_derived_projection_field: ${String(projection.projection_id ?? "unknown")}: ${field}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const projectionId = String(projection.projection_id ?? "");
|
|
206
|
-
const root = String(projection.root ?? "");
|
|
207
|
-
const sourceRoots = Array.isArray(projection.source_roots) ? projection.source_roots.map(String) : [];
|
|
208
|
-
if (!projectionId || seenProjectionIds.has(projectionId)) {
|
|
209
|
-
errors.push(`invalid_or_duplicate_projection_id: ${projectionId || "empty"}`);
|
|
210
|
-
}
|
|
211
|
-
const overlapsExistingRoot = [...seenProjectionRoots].some((existingRoot) => (
|
|
212
|
-
isRefWithin(root, existingRoot) || isRefWithin(existingRoot, root)
|
|
213
|
-
));
|
|
214
|
-
if (!isLiteralPortableRef(root)
|
|
215
|
-
|| !isGeneratedProjectionRoot(root, canonicalRoot)
|
|
216
|
-
|| overlapsExistingRoot) {
|
|
217
|
-
errors.push(`invalid_or_duplicate_projection_root: ${root || "empty"}`);
|
|
218
|
-
}
|
|
219
|
-
if (sourceRoots.length === 0 || sourceRoots.some((ref) => (
|
|
220
|
-
!isLiteralPortableRef(ref)
|
|
221
|
-
|| !ref.startsWith(`${canonicalRoot}/`)
|
|
222
|
-
|| isRefWithin(ref, root)
|
|
223
|
-
))) {
|
|
224
|
-
errors.push(`invalid_projection_source_roots: ${projectionId || "unknown"}`);
|
|
225
|
-
}
|
|
226
|
-
for (const field of ["generate_command", "drift_check_command", "required_marker"]) {
|
|
227
|
-
if (typeof projection[field] !== "string" || projection[field].trim().length === 0) {
|
|
228
|
-
errors.push(`invalid_tracked_derived_projection_field: ${projectionId || "unknown"}: ${field}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
seenProjectionIds.add(projectionId);
|
|
232
|
-
seenProjectionRoots.add(root);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (!Array.isArray(layout?.table_family_extensions)) {
|
|
236
|
-
errors.push("invalid_table_family_extensions: .nimi/config/spec-layout.yaml");
|
|
237
|
-
}
|
|
238
|
-
const tableFamilyExtensions = Array.isArray(layout?.table_family_extensions)
|
|
239
|
-
? layout.table_family_extensions
|
|
240
|
-
: [];
|
|
241
|
-
const seenFamilies = new Set();
|
|
242
|
-
for (const family of tableFamilyExtensions) {
|
|
243
|
-
if (!isPlainObject(family)) {
|
|
244
|
-
errors.push("invalid_table_family_extension: non-object entry");
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (!hasExactFields(family, requiredFamilyFields)) {
|
|
248
|
-
errors.push(`invalid_table_family_extension_fields: ${String(family.table_family ?? "unknown")}`);
|
|
249
|
-
}
|
|
250
|
-
for (const field of requiredFamilyFields) {
|
|
251
|
-
if (!(field in family)) {
|
|
252
|
-
errors.push(`missing_table_family_extension_field: ${String(family.table_family ?? "unknown")}: ${field}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const familyId = String(family.table_family ?? "");
|
|
256
|
-
if (!/^[a-z][a-z0-9_]*$/.test(familyId) || seenFamilies.has(familyId)) {
|
|
257
|
-
errors.push(`invalid_or_duplicate_table_family_extension: ${familyId || "empty"}`);
|
|
258
|
-
}
|
|
259
|
-
if (packageFamilyIds.has(familyId)) {
|
|
260
|
-
errors.push(`table_family_extension_shadows_package_family: ${familyId}`);
|
|
261
|
-
}
|
|
262
|
-
if (family.authority_class !== "product_authority_table"
|
|
263
|
-
|| !Array.isArray(family.required_fields)
|
|
264
|
-
|| family.required_fields.length === 0
|
|
265
|
-
|| family.required_fields.some((field) => typeof field !== "string" || field.length === 0)
|
|
266
|
-
|| !family.required_fields.includes("table_family")
|
|
267
|
-
|| !family.required_fields.includes("owner")
|
|
268
|
-
|| new Set(family.required_fields).size !== family.required_fields.length
|
|
269
|
-
|| !Array.isArray(family.forbidden_fields)
|
|
270
|
-
|| family.forbidden_fields.some((field) => typeof field !== "string" || field.length === 0)
|
|
271
|
-
|| new Set(family.forbidden_fields).size !== family.forbidden_fields.length
|
|
272
|
-
|| family.required_fields.some((field) => family.forbidden_fields.includes(field))) {
|
|
273
|
-
errors.push(`invalid_table_family_extension_shape: ${familyId || "unknown"}`);
|
|
274
|
-
}
|
|
275
|
-
seenFamilies.add(familyId);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
ok: errors.length === 0,
|
|
280
|
-
present: true,
|
|
281
|
-
errors,
|
|
282
|
-
canonicalRoot,
|
|
283
|
-
hostInstructionPaths,
|
|
284
|
-
trackedDerivedProjections,
|
|
285
|
-
tableFamilyExtensions,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function collectFilesUnder(rootPath) {
|
|
290
|
-
const exists = await pathExists(rootPath);
|
|
291
|
-
if (!exists || !exists.isDirectory()) {
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function walk(currentPath) {
|
|
296
|
-
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
297
|
-
const files = [];
|
|
298
|
-
for (const entry of entries) {
|
|
299
|
-
const childPath = path.join(currentPath, entry.name);
|
|
300
|
-
if (entry.isDirectory()) {
|
|
301
|
-
files.push(...await walk(childPath));
|
|
302
|
-
} else if (entry.isFile()) {
|
|
303
|
-
files.push(childPath);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return files.sort();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return walk(rootPath);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function collectCandidateFiles(projectRoot, rootRef = ".nimi/spec") {
|
|
313
|
-
const roots = [
|
|
314
|
-
rootRef,
|
|
315
|
-
".nimi/contracts",
|
|
316
|
-
".nimi/methodology",
|
|
317
|
-
".nimi/config",
|
|
318
|
-
".nimi/derived",
|
|
319
|
-
".nimi/state",
|
|
320
|
-
".nimi/audit",
|
|
321
|
-
];
|
|
322
|
-
const uniqueRoots = [...new Set(roots)];
|
|
323
|
-
const files = [];
|
|
324
|
-
for (const root of uniqueRoots) {
|
|
325
|
-
files.push(...await collectFilesUnder(path.resolve(projectRoot, root)));
|
|
326
|
-
}
|
|
327
|
-
return [...new Set(files)].sort();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function pathMatchesGlob(candidate, pattern) {
|
|
331
|
-
const tokens = [];
|
|
332
|
-
let source = pattern
|
|
333
|
-
.replaceAll("{spec,methodology,contracts,config}", "__BRACE_PACKAGE_ROOTS__")
|
|
334
|
-
.replaceAll("{config,contracts,methodology,spec}", "__BRACE_PACKAGE_ROOTS__")
|
|
335
|
-
.replaceAll("<domain>", "__DOMAIN__")
|
|
336
|
-
.replaceAll("**", "__DOUBLE_STAR__")
|
|
337
|
-
.replaceAll("*", "__STAR__");
|
|
338
|
-
|
|
339
|
-
source = source.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
340
|
-
source = source
|
|
341
|
-
.replaceAll("__BRACE_PACKAGE_ROOTS__", "(?:spec|methodology|contracts|config)")
|
|
342
|
-
.replaceAll("__DOMAIN__", "[^/]+")
|
|
343
|
-
.replaceAll("__DOUBLE_STAR__", ".*")
|
|
344
|
-
.replaceAll("__STAR__", "[^/]*");
|
|
345
|
-
tokens.push(new RegExp(`^${source}$`));
|
|
346
|
-
return tokens.some((regex) => regex.test(candidate));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function basenameNoExt(ref) {
|
|
350
|
-
return path.posix.basename(ref).replace(/\.(yaml|yml|md)$/i, "");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function firstPathSegment(ref) {
|
|
354
|
-
return ref.split("/")[0] ?? "";
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function domainForSpecRef(ref) {
|
|
358
|
-
const parts = ref.split("/");
|
|
359
|
-
if (parts[0] !== ".nimi" || parts[1] !== "spec" || parts.length < 4) {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
if (parts[2] === "_meta" || parts[2] === "generated") {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
return parts[2] ?? null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function walkObjectKeys(value, callback) {
|
|
369
|
-
if (Array.isArray(value)) {
|
|
370
|
-
for (const entry of value) {
|
|
371
|
-
walkObjectKeys(entry, callback);
|
|
372
|
-
}
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (!isPlainObject(value)) {
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
379
|
-
callback(String(key), entry);
|
|
380
|
-
walkObjectKeys(entry, callback);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function containsAnyKey(value, keys) {
|
|
385
|
-
const wanted = new Set(keys);
|
|
386
|
-
let found = false;
|
|
387
|
-
walkObjectKeys(value, (key) => {
|
|
388
|
-
if (wanted.has(key)) {
|
|
389
|
-
found = true;
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
return found;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function yamlForText(text) {
|
|
396
|
-
return parseYamlText(text);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function isYamlRef(ref) {
|
|
400
|
-
return /\.(yaml|yml)$/i.test(ref);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function isMarkdownRef(ref) {
|
|
404
|
-
return /\.md$/i.test(ref);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function tableFamilyContractMap(contracts) {
|
|
408
|
-
const families = Array.isArray(contracts.tableFamily.data?.table_families)
|
|
409
|
-
? contracts.tableFamily.data.table_families
|
|
410
|
-
: [];
|
|
411
|
-
const extensions = contracts.hostSpecLayout?.ok
|
|
412
|
-
? contracts.hostSpecLayout.tableFamilyExtensions
|
|
413
|
-
: [];
|
|
414
|
-
return new Map([...families, ...extensions].map((entry) => [String(entry.table_family), entry]));
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function domainAdmissionMap(contracts) {
|
|
418
|
-
const admissions = Array.isArray(contracts.domainAdmission.data?.domain_admissions)
|
|
419
|
-
? contracts.domainAdmission.data.domain_admissions
|
|
420
|
-
: [];
|
|
421
|
-
return new Map(admissions.map((entry) => [String(entry.domain_id), entry]));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function trackedAdmissionRoots(contracts) {
|
|
425
|
-
const admissions = Array.isArray(contracts.trackedOutputAdmission.data?.admissions)
|
|
426
|
-
? contracts.trackedOutputAdmission.data.admissions
|
|
427
|
-
: [];
|
|
428
|
-
return admissions.map((entry) => String(entry.root)).filter(Boolean);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function isTrackedNonProductRef(ref) {
|
|
432
|
-
return ref.startsWith(".nimi/derived/")
|
|
433
|
-
|| ref.startsWith(".nimi/state/")
|
|
434
|
-
|| ref.startsWith(".nimi/audit/");
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function isAdmittedTrackedOutput(ref, admittedRoots) {
|
|
438
|
-
return admittedRoots.some((root) => pathMatchesGlob(ref, root));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function projectionForRef(ref, contracts) {
|
|
442
|
-
if (!contracts.hostSpecLayout?.ok) {
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
return contracts.hostSpecLayout.trackedDerivedProjections.find((entry) => isRefWithin(ref, String(entry.root))) ?? null;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isHostInstructionRef(ref, contracts) {
|
|
449
|
-
return contracts.hostSpecLayout?.ok
|
|
450
|
-
&& contracts.hostSpecLayout.hostInstructionPaths.some((pattern) => pathMatchesGlob(ref, pattern));
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function classifyRef(ref, text, parsedYaml, contracts) {
|
|
454
|
-
if (ref.startsWith(".nimi/spec/future/")) {
|
|
455
|
-
return "unclassified";
|
|
456
|
-
}
|
|
457
|
-
if (isHostInstructionRef(ref, contracts)) {
|
|
458
|
-
return "host_instruction";
|
|
459
|
-
}
|
|
460
|
-
if (projectionForRef(ref, contracts)) {
|
|
461
|
-
return "tracked_derived_projection";
|
|
462
|
-
}
|
|
463
|
-
if (ref.startsWith(".nimi/spec/generated/") || ref.includes("/kernel/generated/")) {
|
|
464
|
-
return "derived_view";
|
|
465
|
-
}
|
|
466
|
-
if (ref === ".nimi/spec/_meta/spec-generation-audit.yaml" || ref.startsWith(".nimi/spec/_meta/spec-generation-audit/")) {
|
|
467
|
-
return "spec_generation_state";
|
|
468
|
-
}
|
|
469
|
-
if (ref.startsWith(".nimi/spec/_meta/")) {
|
|
470
|
-
if (ref.endsWith("-admission-anchor.yaml")) {
|
|
471
|
-
return "host_projection_anchor";
|
|
472
|
-
}
|
|
473
|
-
return "methodology_authority";
|
|
474
|
-
}
|
|
475
|
-
if (ref === ".nimi/spec/product-scope.yaml") {
|
|
476
|
-
return "methodology_authority";
|
|
477
|
-
}
|
|
478
|
-
if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/tables/") && isYamlRef(ref)) {
|
|
479
|
-
if (isPlainObject(parsedYaml) && parsedYaml.table_family === "support_registry") {
|
|
480
|
-
return "support_registry";
|
|
481
|
-
}
|
|
482
|
-
if (isPlainObject(parsedYaml) && typeof parsedYaml.table_family === "string") {
|
|
483
|
-
return "product_authority_table";
|
|
484
|
-
}
|
|
485
|
-
const stateKeys = ["done", "covered", "coverage_status", "audit_date", "evidence_report", "current", "proposed", "backlog_status", "migration_status", "mapping_status", "run_id", "ledger_ref"];
|
|
486
|
-
if (containsAnyKey(parsedYaml, stateKeys) || /rule-evidence|backlog|migration/i.test(basenameNoExt(ref))) {
|
|
487
|
-
return "audit_evidence_state";
|
|
488
|
-
}
|
|
489
|
-
return "product_authority_table";
|
|
490
|
-
}
|
|
491
|
-
if (ref.startsWith(".nimi/spec/") && isYamlRef(ref) && basenameNoExt(ref) === "spec-exempt-modules") {
|
|
492
|
-
return "support_registry";
|
|
493
|
-
}
|
|
494
|
-
if (ref.startsWith(".nimi/spec/") && ref.includes("/kernel/") && isMarkdownRef(ref)) {
|
|
495
|
-
return "product_authority";
|
|
496
|
-
}
|
|
497
|
-
if (ref === ".nimi/spec/INDEX.md" || (ref.startsWith(".nimi/spec/") && isMarkdownRef(ref))) {
|
|
498
|
-
return "thin_guidance";
|
|
499
|
-
}
|
|
500
|
-
if (ref.startsWith(".nimi/local/derived/")) {
|
|
501
|
-
return "derived_view";
|
|
502
|
-
}
|
|
503
|
-
if (ref.startsWith(".nimi/local/state/spec-generation/")) {
|
|
504
|
-
return "spec_generation_state";
|
|
505
|
-
}
|
|
506
|
-
if (ref.startsWith(".nimi/local/audit/")) {
|
|
507
|
-
return "audit_evidence_state";
|
|
508
|
-
}
|
|
509
|
-
if (ref.startsWith(".nimi/local/") || ref.startsWith(".local/")) {
|
|
510
|
-
return "operational_local_artifact";
|
|
511
|
-
}
|
|
512
|
-
if (ref.startsWith(".nimi/contracts/") || ref.startsWith(".nimi/methodology/")) {
|
|
513
|
-
return "nimicoding_managed_projection";
|
|
514
|
-
}
|
|
515
|
-
if (ref.startsWith(".nimi/config/")) {
|
|
516
|
-
return ref === ".nimi/config/host-overlay.yaml" ? "host_projection_anchor" : "nimicoding_managed_projection";
|
|
517
|
-
}
|
|
518
|
-
if (ref.startsWith("package://@nimiplatform/nimi-coding/")) {
|
|
519
|
-
return "methodology_authority";
|
|
520
|
-
}
|
|
521
|
-
if (isTrackedNonProductRef(ref)) {
|
|
522
|
-
if (ref.startsWith(".nimi/derived/")) return "derived_view";
|
|
523
|
-
if (ref.startsWith(".nimi/state/")) return "spec_generation_state";
|
|
524
|
-
if (ref.startsWith(".nimi/audit/")) return "audit_evidence_state";
|
|
525
|
-
}
|
|
526
|
-
return "unclassified";
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function dispositionFor(ref, surfaceClass, errors) {
|
|
530
|
-
if (errors.length > 0 && surfaceClass === "unclassified") {
|
|
531
|
-
return "block";
|
|
532
|
-
}
|
|
533
|
-
if (surfaceClass === "methodology_authority") {
|
|
534
|
-
return "move_package";
|
|
535
|
-
}
|
|
536
|
-
if (surfaceClass === "nimicoding_managed_projection") {
|
|
537
|
-
return "keep";
|
|
538
|
-
}
|
|
539
|
-
if (surfaceClass === "derived_view") {
|
|
540
|
-
return "delete";
|
|
541
|
-
}
|
|
542
|
-
if (surfaceClass === "host_instruction" || surfaceClass === "tracked_derived_projection") {
|
|
543
|
-
return errors.length > 0 ? "block" : "keep";
|
|
544
|
-
}
|
|
545
|
-
if (["spec_generation_state", "audit_evidence_state", "operational_local_artifact"].includes(surfaceClass)) {
|
|
546
|
-
return "move_local";
|
|
547
|
-
}
|
|
548
|
-
if (errors.length > 0) {
|
|
549
|
-
return "block";
|
|
550
|
-
}
|
|
551
|
-
return "keep";
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function targetRootFor(surfaceClass, ref) {
|
|
555
|
-
if (surfaceClass === "methodology_authority") {
|
|
556
|
-
return "nimi-coding";
|
|
557
|
-
}
|
|
558
|
-
if (surfaceClass === "nimicoding_managed_projection") {
|
|
559
|
-
return ".nimi";
|
|
560
|
-
}
|
|
561
|
-
if (surfaceClass === "derived_view") {
|
|
562
|
-
return "stdout_view";
|
|
563
|
-
}
|
|
564
|
-
if (surfaceClass === "host_instruction" || surfaceClass === "tracked_derived_projection") {
|
|
565
|
-
return ".nimi/spec";
|
|
566
|
-
}
|
|
567
|
-
if (surfaceClass === "spec_generation_state" || surfaceClass === "operational_local_artifact") {
|
|
568
|
-
return ".nimi/local/state";
|
|
569
|
-
}
|
|
570
|
-
if (surfaceClass === "audit_evidence_state") {
|
|
571
|
-
return ".nimi/local/audit";
|
|
572
|
-
}
|
|
573
|
-
if (surfaceClass === "unclassified") {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
if (ref.startsWith(".nimi/spec/")) {
|
|
577
|
-
return ".nimi/spec";
|
|
578
|
-
}
|
|
579
|
-
return firstPathSegment(ref);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function ownerFor(surfaceClass, ref) {
|
|
583
|
-
if (surfaceClass === "methodology_authority" || surfaceClass === "nimicoding_managed_projection") {
|
|
584
|
-
return "nimi-coding";
|
|
585
|
-
}
|
|
586
|
-
if (surfaceClass === "derived_view") {
|
|
587
|
-
return "generator";
|
|
588
|
-
}
|
|
589
|
-
if (surfaceClass === "tracked_derived_projection") {
|
|
590
|
-
return "generator";
|
|
591
|
-
}
|
|
592
|
-
if (surfaceClass === "host_instruction") {
|
|
593
|
-
return "product_host";
|
|
594
|
-
}
|
|
595
|
-
if (surfaceClass === "spec_generation_state") {
|
|
596
|
-
return "spec_generator";
|
|
597
|
-
}
|
|
598
|
-
if (surfaceClass === "audit_evidence_state") {
|
|
599
|
-
return "verification_process";
|
|
600
|
-
}
|
|
601
|
-
if (surfaceClass === "operational_local_artifact") {
|
|
602
|
-
return "external_tooling";
|
|
603
|
-
}
|
|
604
|
-
return domainForSpecRef(ref) ?? "product_domain";
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function confirmationFor(errors) {
|
|
608
|
-
if (errors.some((error) => error.includes("unadmitted_domain_retained_as_spec_input"))) {
|
|
609
|
-
return "product_semantic_fork";
|
|
610
|
-
}
|
|
611
|
-
if (errors.some((error) => error.includes("missing_domain_admission"))) {
|
|
612
|
-
return "owner_ambiguity";
|
|
613
|
-
}
|
|
614
|
-
if (errors.some((error) => error.includes("package_methodology_under_host_spec") || error.includes("package_body_promoted_to_product_authority"))) {
|
|
615
|
-
return "package_boundary_ambiguity";
|
|
616
|
-
}
|
|
617
|
-
return "none";
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function ambiguityFor(requiredConfirmation, surfaceClass) {
|
|
621
|
-
if (requiredConfirmation === "none") {
|
|
622
|
-
return {
|
|
623
|
-
posture: "none",
|
|
624
|
-
reason: null,
|
|
625
|
-
candidate_owners: [],
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
return {
|
|
629
|
-
posture: "blocks_migration",
|
|
630
|
-
reason: requiredConfirmation,
|
|
631
|
-
candidate_owners: surfaceClass === "unclassified" ? [] : [ownerFor(surfaceClass, "")],
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function validationCommandsFor(entryClass) {
|
|
636
|
-
const commands = {
|
|
637
|
-
product_authority_table: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
|
|
638
|
-
support_registry: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
|
|
639
|
-
thin_guidance: ["pnpm exec nimicoding validate-guidance-bodies --profile nimi --root .nimi/spec"],
|
|
640
|
-
host_instruction: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
|
|
641
|
-
tracked_derived_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
|
|
642
|
-
host_projection_anchor: ["pnpm exec nimicoding validate-projection-edges --profile nimi --root .nimi/spec"],
|
|
643
|
-
nimicoding_managed_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
|
|
644
|
-
};
|
|
645
|
-
return commands[entryClass] ?? ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"];
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function addError(errors, code, ref, detail) {
|
|
649
|
-
errors.push(detail ? `${code}: ${ref}: ${detail}` : `${code}: ${ref}`);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
|
|
653
|
-
const errors = [];
|
|
654
|
-
|
|
655
|
-
if (surfaceClass === "unclassified") {
|
|
656
|
-
addError(errors, "unclassified_file", ref);
|
|
657
|
-
}
|
|
658
|
-
if (surfaceClass === "derived_view" && ref.startsWith(".nimi/spec/")) {
|
|
659
|
-
addError(errors, "derived_view_under_product_authority_root", ref);
|
|
660
|
-
}
|
|
661
|
-
if (surfaceClass === "tracked_derived_projection") {
|
|
662
|
-
const projection = projectionForRef(ref, contracts);
|
|
663
|
-
if (!projection) {
|
|
664
|
-
addError(errors, "tracked_derived_projection_without_layout_admission", ref);
|
|
665
|
-
} else if (!text.includes(String(projection.required_marker))) {
|
|
666
|
-
addError(errors, "tracked_derived_projection_missing_generated_marker", ref, String(projection.required_marker));
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (surfaceClass === "host_instruction" && PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
|
|
670
|
-
addError(errors, "host_instruction_defines_product_rule", ref);
|
|
671
|
-
}
|
|
672
|
-
if (surfaceClass === "derived_view" && ref.startsWith(".nimi/local/derived/")) {
|
|
673
|
-
addError(errors, "derived_view_written_to_local_derived", ref);
|
|
674
|
-
}
|
|
675
|
-
if (surfaceClass === "spec_generation_state" && ref.startsWith(".nimi/spec/")) {
|
|
676
|
-
addError(errors, "spec_generation_state_under_spec", ref);
|
|
677
|
-
}
|
|
678
|
-
if (surfaceClass === "audit_evidence_state" && ref.startsWith(".nimi/spec/")) {
|
|
679
|
-
addError(errors, "audit_evidence_state_under_spec", ref);
|
|
680
|
-
}
|
|
681
|
-
if (surfaceClass === "methodology_authority" && ref.startsWith(".nimi/spec/")) {
|
|
682
|
-
addError(errors, "package_methodology_under_host_spec", ref);
|
|
683
|
-
}
|
|
684
|
-
if (isTrackedNonProductRef(ref) && !isAdmittedTrackedOutput(ref, trackedAdmissionRoots(contracts))) {
|
|
685
|
-
addError(errors, "tracked_non_product_without_admission", ref);
|
|
686
|
-
}
|
|
687
|
-
if (surfaceClass === "thin_guidance" && isMarkdownRef(ref)) {
|
|
688
|
-
if (TEXT_RULE_BODY_PATTERN.test(text)) {
|
|
689
|
-
addError(errors, "guidance_defines_rule_body", ref);
|
|
690
|
-
}
|
|
691
|
-
if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
|
|
692
|
-
addError(errors, "guidance_defines_product_rule", ref);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (surfaceClass === "product_authority" && isMarkdownRef(ref) && GENERATED_REF_PATTERN.test(text)) {
|
|
696
|
-
addError(errors, "derived_view_referenced_as_authority", ref);
|
|
697
|
-
}
|
|
698
|
-
return errors;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function validateTableRef(ref, parsedYaml, contracts) {
|
|
702
|
-
const errors = [];
|
|
703
|
-
if (!ref.startsWith(".nimi/spec/") || !ref.includes("/kernel/tables/") || !isYamlRef(ref)) {
|
|
704
|
-
return errors;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (!isPlainObject(parsedYaml)) {
|
|
708
|
-
addError(errors, "invalid_yaml_table", ref);
|
|
709
|
-
return errors;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const family = typeof parsedYaml.table_family === "string" ? parsedYaml.table_family : null;
|
|
713
|
-
const familyMap = tableFamilyContractMap(contracts);
|
|
714
|
-
if (!family) {
|
|
715
|
-
addError(errors, "missing_table_family", ref);
|
|
716
|
-
} else if (!familyMap.has(family)) {
|
|
717
|
-
addError(errors, "unknown_table_family", ref, family);
|
|
718
|
-
} else {
|
|
719
|
-
const familyContract = familyMap.get(family);
|
|
720
|
-
for (const field of familyContract.required_fields ?? []) {
|
|
721
|
-
if (!(field in parsedYaml)) {
|
|
722
|
-
addError(errors, "missing_table_family_required_field", ref, String(field));
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (containsAnyKey(parsedYaml, familyContract.forbidden_fields ?? [])) {
|
|
726
|
-
addError(errors, "table_contains_family_forbidden_field", ref, family);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const forbiddenFields = contracts.tableFamily.data?.forbidden_fields_by_authority_class?.product_authority_table ?? [];
|
|
731
|
-
if (containsAnyKey(parsedYaml, forbiddenFields)) {
|
|
732
|
-
addError(errors, "table_contains_forbidden_state_or_audit_field", ref);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return errors;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function validateDomainRef(ref, contracts) {
|
|
739
|
-
const errors = [];
|
|
740
|
-
const domain = domainForSpecRef(ref);
|
|
741
|
-
if (!domain) {
|
|
742
|
-
return errors;
|
|
743
|
-
}
|
|
744
|
-
const admissions = domainAdmissionMap(contracts);
|
|
745
|
-
const admission = admissions.get(domain);
|
|
746
|
-
if (!admission) {
|
|
747
|
-
addError(errors, "missing_domain_admission", ref, domain);
|
|
748
|
-
return errors;
|
|
749
|
-
}
|
|
750
|
-
if (admission.authority_class === "excluded_from_spec") {
|
|
751
|
-
addError(errors, "excluded_domain_retained_under_spec", ref, domain);
|
|
752
|
-
}
|
|
753
|
-
if (admission.authority_class === "migration_input_only") {
|
|
754
|
-
addError(errors, "unadmitted_domain_retained_as_spec_input", ref, domain);
|
|
755
|
-
}
|
|
756
|
-
return errors;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
async function buildInventory(projectRoot, options = {}) {
|
|
760
|
-
const contracts = await loadSurfaceContracts(projectRoot);
|
|
761
|
-
const rootRef = options.rootRef ?? ".nimi/spec";
|
|
762
|
-
const absoluteFiles = await collectCandidateFiles(projectRoot, rootRef);
|
|
763
|
-
const entries = [];
|
|
764
|
-
const layoutErrors = [...(contracts.hostSpecLayout?.errors ?? [])];
|
|
765
|
-
const errors = [...layoutErrors];
|
|
766
|
-
|
|
767
|
-
for (const absolutePath of absoluteFiles) {
|
|
768
|
-
const ref = relativeRef(projectRoot, absolutePath);
|
|
769
|
-
const text = await readFile(absolutePath, "utf8");
|
|
770
|
-
const parsedYaml = isYamlRef(ref) ? yamlForText(text) : null;
|
|
771
|
-
const surfaceClass = classifyRef(ref, text, parsedYaml, contracts);
|
|
772
|
-
const validationErrors = {
|
|
773
|
-
placement: validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts),
|
|
774
|
-
table_family: validateTableRef(ref, parsedYaml, contracts),
|
|
775
|
-
domain_admission: validateDomainRef(ref, contracts),
|
|
776
|
-
};
|
|
777
|
-
const entryErrors = Object.values(validationErrors).flat();
|
|
778
|
-
errors.push(...entryErrors);
|
|
779
|
-
const requiredConfirmation = confirmationFor(entryErrors);
|
|
780
|
-
entries.push({
|
|
781
|
-
source_path: ref,
|
|
782
|
-
current_inferred_class: surfaceClass,
|
|
783
|
-
target_class: surfaceClass,
|
|
784
|
-
disposition: dispositionFor(ref, surfaceClass, entryErrors),
|
|
785
|
-
target_root: targetRootFor(surfaceClass, ref),
|
|
786
|
-
owner: ownerFor(surfaceClass, ref),
|
|
787
|
-
required_confirmation: requiredConfirmation,
|
|
788
|
-
ambiguity: ambiguityFor(requiredConfirmation, surfaceClass),
|
|
789
|
-
evidence: entryErrors.length > 0 ? entryErrors : [`classified_as:${surfaceClass}`],
|
|
790
|
-
validation_commands: validationCommandsFor(surfaceClass),
|
|
791
|
-
validation_errors: validationErrors,
|
|
792
|
-
errors: entryErrors,
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
return {
|
|
797
|
-
contracts,
|
|
798
|
-
entries,
|
|
799
|
-
layoutErrors,
|
|
800
|
-
errors,
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function summarize(entries) {
|
|
805
|
-
const byClass = {};
|
|
806
|
-
const byDisposition = {};
|
|
807
|
-
for (const entry of entries) {
|
|
808
|
-
byClass[entry.current_inferred_class] = (byClass[entry.current_inferred_class] ?? 0) + 1;
|
|
809
|
-
byDisposition[entry.disposition] = (byDisposition[entry.disposition] ?? 0) + 1;
|
|
810
|
-
}
|
|
811
|
-
return {
|
|
812
|
-
total_files: entries.length,
|
|
813
|
-
by_surface_class: byClass,
|
|
814
|
-
by_disposition: byDisposition,
|
|
815
|
-
blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function reportFor(validator, ok, errors, warnings, entries, extra = {}) {
|
|
820
|
-
return {
|
|
821
|
-
contract: SURFACE_RESULT_CONTRACT,
|
|
822
|
-
validator,
|
|
823
|
-
ok,
|
|
824
|
-
errors,
|
|
825
|
-
warnings,
|
|
826
|
-
summary: summarize(entries),
|
|
827
|
-
...extra,
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function countBy(entries, field) {
|
|
832
|
-
const counts = {};
|
|
833
|
-
for (const entry of entries) {
|
|
834
|
-
const value = String(entry[field] ?? "unknown");
|
|
835
|
-
counts[value] = (counts[value] ?? 0) + 1;
|
|
836
|
-
}
|
|
837
|
-
return counts;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function groupInventoryByDisposition(entries) {
|
|
841
|
-
const groups = {};
|
|
842
|
-
for (const entry of entries) {
|
|
843
|
-
const disposition = String(entry.disposition);
|
|
844
|
-
const group = groups[disposition] ?? [];
|
|
845
|
-
group.push(entry.source_path);
|
|
846
|
-
groups[disposition] = group;
|
|
847
|
-
}
|
|
848
|
-
return Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, value.sort()]));
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function buildMigrationGroups(entries) {
|
|
852
|
-
const groupKinds = [
|
|
853
|
-
{
|
|
854
|
-
group_id: "move-package-methodology-copied-under-spec",
|
|
855
|
-
dispositions: ["move_package"],
|
|
856
|
-
requires_confirmation: false,
|
|
857
|
-
},
|
|
858
|
-
{
|
|
859
|
-
group_id: "move-local-derived-and-audit-state",
|
|
860
|
-
dispositions: ["move_local"],
|
|
861
|
-
requires_confirmation: false,
|
|
862
|
-
},
|
|
863
|
-
{
|
|
864
|
-
group_id: "rewrite-product-authority-tables-and-guidance",
|
|
865
|
-
dispositions: ["rewrite"],
|
|
866
|
-
requires_confirmation: true,
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
group_id: "resolve-blocked-semantic-forks",
|
|
870
|
-
dispositions: ["block"],
|
|
871
|
-
requires_confirmation: true,
|
|
872
|
-
},
|
|
873
|
-
];
|
|
874
|
-
return groupKinds
|
|
875
|
-
.map((group) => {
|
|
876
|
-
const matching = entries.filter((entry) => group.dispositions.includes(entry.disposition));
|
|
877
|
-
return {
|
|
878
|
-
...group,
|
|
879
|
-
entry_count: matching.length,
|
|
880
|
-
source_paths: matching.map((entry) => entry.source_path).sort(),
|
|
881
|
-
};
|
|
882
|
-
})
|
|
883
|
-
.filter((group) => group.entry_count > 0);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function enumValidation(entries, inventory) {
|
|
887
|
-
const targetClasses = new Set(inventory.target_class_enum);
|
|
888
|
-
const dispositions = new Set(inventory.disposition_enum);
|
|
889
|
-
return {
|
|
890
|
-
unknown_target_classes: entries
|
|
891
|
-
.filter((entry) => !targetClasses.has(entry.target_class))
|
|
892
|
-
.map((entry) => entry.source_path),
|
|
893
|
-
unknown_dispositions: entries
|
|
894
|
-
.filter((entry) => !dispositions.has(entry.disposition))
|
|
895
|
-
.map((entry) => entry.source_path),
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function migrationPlanForInventory(rootRef, inventory) {
|
|
900
|
-
const entries = inventory.inventory;
|
|
901
|
-
const enumStatus = enumValidation(entries, inventory);
|
|
902
|
-
const requiredConfirmationEntries = entries.filter((entry) => entry.required_confirmation !== "none");
|
|
903
|
-
return {
|
|
904
|
-
contract: "nimicoding.spec-migration-plan.v2",
|
|
905
|
-
ok: enumStatus.unknown_target_classes.length === 0 && enumStatus.unknown_dispositions.length === 0,
|
|
906
|
-
version: inventory.version,
|
|
907
|
-
root: rootRef,
|
|
908
|
-
inventory: entries,
|
|
909
|
-
disposition_enum: inventory.disposition_enum,
|
|
910
|
-
target_class_enum: inventory.target_class_enum,
|
|
911
|
-
semantic_constraints: [
|
|
912
|
-
...inventory.semantic_constraints,
|
|
913
|
-
"migration_plan_must_not_modify_files",
|
|
914
|
-
"required_confirmation_entries_must_be_preserved",
|
|
915
|
-
"local_only_plan_artifact_required",
|
|
916
|
-
],
|
|
917
|
-
summary: {
|
|
918
|
-
total_files: entries.length,
|
|
919
|
-
by_surface_class: countBy(entries, "current_inferred_class"),
|
|
920
|
-
by_disposition: countBy(entries, "disposition"),
|
|
921
|
-
by_required_confirmation: countBy(entries, "required_confirmation"),
|
|
922
|
-
required_confirmation_count: requiredConfirmationEntries.length,
|
|
923
|
-
blocking_entries: entries.filter((entry) => entry.disposition === "block").length,
|
|
924
|
-
},
|
|
925
|
-
enum_validation: enumStatus,
|
|
926
|
-
required_confirmations: requiredConfirmationEntries.map((entry) => ({
|
|
927
|
-
source_path: entry.source_path,
|
|
928
|
-
required_confirmation: entry.required_confirmation,
|
|
929
|
-
ambiguity: entry.ambiguity,
|
|
930
|
-
evidence: entry.evidence,
|
|
931
|
-
recommendation: entry.required_confirmation === "product_semantic_fork"
|
|
932
|
-
? "stop_for_user_decision_before_migration"
|
|
933
|
-
: "resolve_owner_or_package_boundary_before_migration",
|
|
934
|
-
})),
|
|
935
|
-
groups: groupInventoryByDisposition(entries),
|
|
936
|
-
migration_groups: buildMigrationGroups(entries),
|
|
937
|
-
mutation_policy: {
|
|
938
|
-
mutates_source_tree: false,
|
|
939
|
-
allowed_output_roots: [".nimi/local/state/spec-surface"],
|
|
940
|
-
forbidden_output_roots: [".nimi/spec", ".nimi/contracts", ".nimi/methodology", ".nimi/config"],
|
|
941
|
-
},
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
export async function classifySpecSurface(projectRoot, options = {}) {
|
|
946
|
-
const { entries, errors } = await buildInventory(projectRoot, options);
|
|
947
|
-
return reportFor("classify-spec-tree", errors.length === 0, errors, [], entries, {
|
|
948
|
-
inventory: {
|
|
949
|
-
version: 2,
|
|
950
|
-
inventory: entries,
|
|
951
|
-
disposition_enum: ["keep", "move_package", "move_local", "split", "rewrite", "delete", "block"],
|
|
952
|
-
target_class_enum: [
|
|
953
|
-
"product_authority",
|
|
954
|
-
"product_authority_table",
|
|
955
|
-
"thin_guidance",
|
|
956
|
-
"host_instruction",
|
|
957
|
-
"tracked_derived_projection",
|
|
958
|
-
"derived_view",
|
|
959
|
-
"spec_generation_state",
|
|
960
|
-
"audit_evidence_state",
|
|
961
|
-
"operational_local_artifact",
|
|
962
|
-
"methodology_authority",
|
|
963
|
-
"nimicoding_managed_projection",
|
|
964
|
-
"host_projection_anchor",
|
|
965
|
-
"support_registry",
|
|
966
|
-
],
|
|
967
|
-
semantic_constraints: [
|
|
968
|
-
"inventory_must_not_modify_files",
|
|
969
|
-
"future_under_spec_must_fail_classification",
|
|
970
|
-
"nimicoding_managed_projection_must_not_be_promoted_to_product_authority",
|
|
971
|
-
],
|
|
972
|
-
},
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
export async function generateSpecMigrationPlan(projectRoot, options = {}) {
|
|
977
|
-
const classification = await classifySpecSurface(projectRoot, options);
|
|
978
|
-
return migrationPlanForInventory(options.rootRef ?? ".nimi/spec", classification.inventory);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
export async function buildSpecSurfaceInventory(projectRoot, options = {}) {
|
|
982
|
-
const { entries, errors } = await buildInventory(projectRoot, options);
|
|
983
|
-
return {
|
|
984
|
-
contract: SURFACE_RESULT_CONTRACT,
|
|
985
|
-
entries,
|
|
986
|
-
errors,
|
|
987
|
-
summary: summarize(entries),
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
export function isProductAuthoritySurfaceClass(surfaceClass) {
|
|
992
|
-
return [
|
|
993
|
-
"product_authority",
|
|
994
|
-
"product_authority_table",
|
|
995
|
-
"thin_guidance",
|
|
996
|
-
"host_projection_anchor",
|
|
997
|
-
"support_registry",
|
|
998
|
-
].includes(surfaceClass);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
export async function validatePlacement(projectRoot, options = {}) {
|
|
1002
|
-
const { entries, layoutErrors } = await buildInventory(projectRoot, options);
|
|
1003
|
-
const errors = [
|
|
1004
|
-
...layoutErrors,
|
|
1005
|
-
...entries.flatMap((entry) => entry.validation_errors?.placement ?? []),
|
|
1006
|
-
];
|
|
1007
|
-
return reportFor("validate-placement", errors.length === 0, errors, [], entries);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
export async function validateTableFamily(projectRoot, options = {}) {
|
|
1011
|
-
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
1012
|
-
const errors = [];
|
|
1013
|
-
for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/")
|
|
1014
|
-
&& item.source_path.includes("/kernel/tables/")
|
|
1015
|
-
&& isYamlRef(item.source_path)
|
|
1016
|
-
&& ["product_authority_table", "support_registry"].includes(item.current_inferred_class))) {
|
|
1017
|
-
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
1018
|
-
errors.push(...validateTableRef(entry.source_path, yamlForText(text), contracts));
|
|
1019
|
-
}
|
|
1020
|
-
return reportFor("validate-table-family", errors.length === 0, errors, [], entries);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
export async function validateProjectionEdges(projectRoot, options = {}) {
|
|
1024
|
-
const { entries, contracts } = await buildInventory(projectRoot, options);
|
|
1025
|
-
const errors = [];
|
|
1026
|
-
const edgeTargets = new Set((contracts.projectionEdge.data?.projection_edges ?? []).map((entry) => String(entry.target_ref)));
|
|
1027
|
-
const anchorRef = ".nimi/spec/_meta/nimi-coding-admission-anchor.yaml";
|
|
1028
|
-
const overlayRef = ".nimi/config/host-overlay.yaml";
|
|
1029
|
-
if (entries.some((entry) => entry.source_path === anchorRef) && !edgeTargets.has(anchorRef)) {
|
|
1030
|
-
addError(errors, "missing_projection_edge_for_anchor", anchorRef);
|
|
1031
|
-
}
|
|
1032
|
-
if (entries.some((entry) => entry.source_path === overlayRef) && !edgeTargets.has(overlayRef)) {
|
|
1033
|
-
addError(errors, "missing_projection_edge_for_overlay", overlayRef);
|
|
1034
|
-
}
|
|
1035
|
-
for (const entry of entries) {
|
|
1036
|
-
if (entry.current_inferred_class === "methodology_authority" && entry.source_path.startsWith(".nimi/spec/")) {
|
|
1037
|
-
addError(errors, "package_body_promoted_to_product_authority", entry.source_path);
|
|
1038
|
-
}
|
|
1039
|
-
if (entry.current_inferred_class === "product_authority") {
|
|
1040
|
-
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
1041
|
-
if (GENERATED_REF_PATTERN.test(text)) {
|
|
1042
|
-
addError(errors, "derived_view_referenced_as_authority", entry.source_path);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
return reportFor("validate-projection-edges", errors.length === 0, errors, [], entries);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
export async function validateGuidanceBodies(projectRoot, options = {}) {
|
|
1050
|
-
const { entries } = await buildInventory(projectRoot, options);
|
|
1051
|
-
const errors = [];
|
|
1052
|
-
for (const entry of entries.filter((item) => item.current_inferred_class === "thin_guidance")) {
|
|
1053
|
-
const text = await readFile(path.join(projectRoot, entry.source_path), "utf8");
|
|
1054
|
-
if (TEXT_RULE_BODY_PATTERN.test(text)) {
|
|
1055
|
-
addError(errors, "guidance_defines_rule_body", entry.source_path);
|
|
1056
|
-
}
|
|
1057
|
-
if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
|
|
1058
|
-
addError(errors, "guidance_defines_product_rule", entry.source_path);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
return reportFor("validate-guidance-bodies", errors.length === 0, errors, [], entries);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
export async function validateDomainAdmission(projectRoot, options = {}) {
|
|
1065
|
-
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
1066
|
-
const errors = [];
|
|
1067
|
-
for (const entry of entries.filter((item) => item.source_path.startsWith(".nimi/spec/"))) {
|
|
1068
|
-
errors.push(...validateDomainRef(entry.source_path, contracts));
|
|
1069
|
-
}
|
|
1070
|
-
return reportFor("validate-domain-admission", errors.length === 0, errors, [], entries);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
export async function validateTrackedOutputAdmission(projectRoot, options = {}) {
|
|
1074
|
-
const { contracts, entries } = await buildInventory(projectRoot, options);
|
|
1075
|
-
const admittedRoots = trackedAdmissionRoots(contracts);
|
|
1076
|
-
const errors = [];
|
|
1077
|
-
for (const entry of entries.filter((item) => isTrackedNonProductRef(item.source_path))) {
|
|
1078
|
-
if (!isAdmittedTrackedOutput(entry.source_path, admittedRoots)) {
|
|
1079
|
-
addError(errors, "tracked_non_product_without_admission", entry.source_path);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return reportFor("validate-tracked-output-admission", errors.length === 0, errors, [], entries);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
export function parseSurfaceValidatorOptions(args) {
|
|
1086
|
-
const options = {
|
|
1087
|
-
profile: "default",
|
|
1088
|
-
rootRef: ".nimi/spec",
|
|
1089
|
-
emit: null,
|
|
1090
|
-
check: false,
|
|
1091
|
-
json: false,
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1095
|
-
const arg = args[index];
|
|
1096
|
-
if (arg === "--profile") {
|
|
1097
|
-
options.profile = args[index + 1] ?? "";
|
|
1098
|
-
index += 1;
|
|
1099
|
-
} else if (arg === "--root") {
|
|
1100
|
-
options.rootRef = args[index + 1] ?? "";
|
|
1101
|
-
index += 1;
|
|
1102
|
-
} else if (arg === "--emit") {
|
|
1103
|
-
options.emit = args[index + 1] ?? "";
|
|
1104
|
-
index += 1;
|
|
1105
|
-
} else if (arg === "--check") {
|
|
1106
|
-
options.check = true;
|
|
1107
|
-
} else if (arg === "--json") {
|
|
1108
|
-
options.json = true;
|
|
1109
|
-
} else if (arg === "--strict") {
|
|
1110
|
-
options.strict = true;
|
|
1111
|
-
} else {
|
|
1112
|
-
return { ok: false, error: `unknown option: ${arg}` };
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (!options.rootRef) {
|
|
1117
|
-
return { ok: false, error: "--root must not be empty" };
|
|
1118
|
-
}
|
|
1119
|
-
return { ok: true, options };
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
export async function writeInventoryIfRequested(report, emitRef, projectRoot) {
|
|
1123
|
-
if (!emitRef) {
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
const absolutePath = path.resolve(projectRoot, emitRef);
|
|
1127
|
-
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1128
|
-
await writeFile(absolutePath, `${JSON.stringify(report.inventory, null, 2)}\n`, "utf8");
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
export async function writeMigrationPlanIfRequested(report, emitRef, projectRoot) {
|
|
1132
|
-
if (!emitRef) {
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const allowedRoot = path.resolve(projectRoot, ".nimi/local/state/spec-surface");
|
|
1136
|
-
const absolutePath = path.resolve(projectRoot, emitRef);
|
|
1137
|
-
const relativeToAllowedRoot = path.relative(allowedRoot, absolutePath);
|
|
1138
|
-
if (
|
|
1139
|
-
relativeToAllowedRoot === "" ||
|
|
1140
|
-
relativeToAllowedRoot.startsWith("..") ||
|
|
1141
|
-
path.isAbsolute(relativeToAllowedRoot)
|
|
1142
|
-
) {
|
|
1143
|
-
throw new Error("--emit must target .nimi/local/state/spec-surface/**");
|
|
1144
|
-
}
|
|
1145
|
-
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1146
|
-
await writeFile(absolutePath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
1147
|
-
}
|