@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
|
@@ -23,49 +23,13 @@ function normalizeGovernanceConfig(parsed) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const profileId = typeof parsed.profile_id === "string" ? parsed.profile_id.trim() : null;
|
|
26
|
-
const specGovernance = isPlainObject(parsed.spec_governance) ? parsed.spec_governance : {};
|
|
27
26
|
const aiGovernance = isPlainObject(parsed.ai_governance) ? parsed.ai_governance : {};
|
|
28
27
|
const agentsFreshness = isPlainObject(aiGovernance.agents_freshness)
|
|
29
28
|
? aiGovernance.agents_freshness
|
|
30
29
|
: {};
|
|
31
30
|
|
|
32
|
-
function normalizeCommandMap(section) {
|
|
33
|
-
if (!isPlainObject(section)) {
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const output = {};
|
|
38
|
-
for (const [key, value] of Object.entries(section)) {
|
|
39
|
-
if (Array.isArray(value)) {
|
|
40
|
-
output[key] = value.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
41
|
-
} else if (typeof value === "string" && value.trim().length > 0) {
|
|
42
|
-
output[key] = [value.trim()];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return output;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
const normalized = {
|
|
49
32
|
profileId,
|
|
50
|
-
specGovernance: {
|
|
51
|
-
canonicalRoot: typeof specGovernance.canonical_root === "string"
|
|
52
|
-
? specGovernance.canonical_root.trim()
|
|
53
|
-
: ".nimi/spec",
|
|
54
|
-
domainChecks: isPlainObject(specGovernance.domain_checks)
|
|
55
|
-
? specGovernance.domain_checks
|
|
56
|
-
: {},
|
|
57
|
-
validateCommands: normalizeCommandMap(specGovernance.validate_commands),
|
|
58
|
-
generateCommands: normalizeCommandMap(specGovernance.generate_commands),
|
|
59
|
-
singleSource: isPlainObject(specGovernance.single_source)
|
|
60
|
-
? specGovernance.single_source
|
|
61
|
-
: {},
|
|
62
|
-
realmAlignment: isPlainObject(specGovernance.realm_alignment)
|
|
63
|
-
? specGovernance.realm_alignment
|
|
64
|
-
: {},
|
|
65
|
-
legacyVocabulary: isPlainObject(specGovernance.legacy_vocabulary)
|
|
66
|
-
? specGovernance.legacy_vocabulary
|
|
67
|
-
: {},
|
|
68
|
-
},
|
|
69
33
|
aiGovernance: {
|
|
70
34
|
agentsFreshness: {
|
|
71
35
|
targets: Array.isArray(agentsFreshness.targets)
|
package/cli/lib/shared.mjs
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
formatBlueprintAuditPayload,
|
|
4
|
-
writeBlueprintAuditArtifact,
|
|
5
|
-
} from "./blueprint-audit.mjs";
|
|
6
|
-
export {
|
|
2
|
+
appendGitattributesEntries,
|
|
7
3
|
appendGitignoreEntries,
|
|
4
|
+
hasExactGitignoreRule,
|
|
5
|
+
hasExactTextLine,
|
|
6
|
+
ManagedPathError,
|
|
7
|
+
ManagedTextError,
|
|
8
|
+
normalizeTextToLf,
|
|
8
9
|
pathExists,
|
|
10
|
+
preflightManagedProjectPaths,
|
|
9
11
|
readTextIfFile,
|
|
12
|
+
readUtf8FileFatal,
|
|
10
13
|
} from "./fs-helpers.mjs";
|
|
11
|
-
export {
|
|
12
|
-
mergeOrderedPaths,
|
|
13
|
-
parsePathRequirements,
|
|
14
|
-
parseSkillSection,
|
|
15
|
-
parseYamlText,
|
|
16
|
-
readTopLevelKeys,
|
|
17
|
-
readYamlList,
|
|
18
|
-
readYamlScalar,
|
|
19
|
-
} from "./yaml-helpers.mjs";
|
|
20
14
|
export {
|
|
21
15
|
integrateEntrypoints,
|
|
22
16
|
previewEntrypointIntegration,
|
|
@@ -24,18 +18,11 @@ export {
|
|
|
24
18
|
removeManagedEntrypoints,
|
|
25
19
|
} from "./entrypoints.mjs";
|
|
26
20
|
export {
|
|
27
|
-
inspectBootstrapCompatibility,
|
|
28
21
|
previewBootstrapRemoval,
|
|
29
22
|
previewBootstrapWrites,
|
|
30
23
|
removeManagedBootstrapFiles,
|
|
31
24
|
writeMissingBootstrapFiles,
|
|
32
25
|
} from "./bootstrap.mjs";
|
|
33
|
-
export {
|
|
34
|
-
loadBlueprintReference,
|
|
35
|
-
loadSpecGenerationAuditContract,
|
|
36
|
-
loadSpecGenerationInputsConfig,
|
|
37
|
-
loadSpecGenerationInputsContract,
|
|
38
|
-
} from "./contracts.mjs";
|
|
39
26
|
export {
|
|
40
27
|
formatDoctorResult,
|
|
41
28
|
inspectDoctorState,
|
package/cli/lib/sync.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { getBootstrapSeedEntries } from "../seeds/bootstrap.mjs";
|
|
5
|
-
import {
|
|
4
|
+
import { getBootstrapSeedEntries, loadSeedPolicy } from "../seeds/bootstrap.mjs";
|
|
5
|
+
import { inspectEntrypointIntegration, integrateEntrypoints } from "./entrypoints.mjs";
|
|
6
|
+
import { normalizeTextToLf, pathExists, preflightManagedProjectPaths } from "./fs-helpers.mjs";
|
|
6
7
|
|
|
7
8
|
export const SYNC_MODE = {
|
|
8
9
|
DRY_RUN: "dry_run",
|
|
@@ -16,88 +17,91 @@ const STATUS = {
|
|
|
16
17
|
UPDATED: "updated",
|
|
17
18
|
WOULD_CREATE: "would_create",
|
|
18
19
|
WOULD_UPDATE: "would_update",
|
|
19
|
-
DRIFTED_PRESERVED: "drifted_preserved",
|
|
20
20
|
MISSING_PACKAGE_CANONICAL: "missing_package_canonical",
|
|
21
|
-
MISSING_HOST_STATE_SEED: "missing_host_state_seed",
|
|
22
21
|
DRIFTED_PACKAGE_CANONICAL: "drifted_package_canonical",
|
|
22
|
+
MANAGED_BLOCK_DRIFT: "managed_block_drift",
|
|
23
|
+
DEPRECATED_PROJECTION_PATH: "deprecated_projection_path",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async function evaluateSeedEntry(projectRoot, entry, mode) {
|
|
26
|
+
async function inspectProjection(projectRoot, entry) {
|
|
28
27
|
const absolutePath = path.join(projectRoot, entry.outputRelativePath);
|
|
28
|
+
const projectionContent = normalizeTextToLf(entry.content);
|
|
29
29
|
const info = await pathExists(absolutePath);
|
|
30
|
-
|
|
30
|
+
if (!info) return { ...entry, absolutePath, projectionContent, state: "missing" };
|
|
31
|
+
const actual = await readFile(absolutePath);
|
|
32
|
+
return { ...entry, absolutePath, projectionContent, state: actual.equals(Buffer.from(projectionContent, "utf8")) ? "in_sync" : "drifted" };
|
|
33
|
+
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await writeFile(absolutePath, entry.content, "utf8");
|
|
36
|
-
return {
|
|
37
|
-
outputRelativePath: entry.outputRelativePath,
|
|
38
|
-
ownership: entry.ownership,
|
|
39
|
-
status: STATUS.CREATED,
|
|
40
|
-
detail: "missing host file was seeded from package source",
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
outputRelativePath: entry.outputRelativePath,
|
|
45
|
-
ownership: entry.ownership,
|
|
46
|
-
status: HOST_OWNED_SEED_OWNERSHIPS.has(entry.ownership)
|
|
47
|
-
? STATUS.MISSING_HOST_STATE_SEED
|
|
48
|
-
: STATUS.MISSING_PACKAGE_CANONICAL,
|
|
49
|
-
detail: mode === SYNC_MODE.CHECK
|
|
50
|
-
? "host file is missing"
|
|
51
|
-
: "host file is missing and would be seeded on --apply",
|
|
52
|
-
};
|
|
35
|
+
function projectionResult(entry, mode, applied = false) {
|
|
36
|
+
if (entry.state === "in_sync") {
|
|
37
|
+
return { outputRelativePath: entry.outputRelativePath, ownership: entry.ownership, status: STATUS.IN_SYNC, detail: "projection matches LF-normalized package canonical text" };
|
|
53
38
|
}
|
|
54
|
-
|
|
55
|
-
const actual = await readTextIfFile(absolutePath);
|
|
56
|
-
if (actual === entry.content) {
|
|
39
|
+
if (applied) {
|
|
57
40
|
return {
|
|
58
41
|
outputRelativePath: entry.outputRelativePath,
|
|
59
42
|
ownership: entry.ownership,
|
|
60
|
-
status: STATUS.
|
|
61
|
-
detail: "
|
|
43
|
+
status: entry.state === "missing" ? STATUS.CREATED : STATUS.UPDATED,
|
|
44
|
+
detail: entry.state === "missing" ? "created LF-normalized package-owned projection" : "restored LF-normalized package-owned projection",
|
|
62
45
|
};
|
|
63
46
|
}
|
|
47
|
+
const status = entry.state === "missing"
|
|
48
|
+
? mode === SYNC_MODE.CHECK ? STATUS.MISSING_PACKAGE_CANONICAL : STATUS.WOULD_CREATE
|
|
49
|
+
: mode === SYNC_MODE.CHECK ? STATUS.DRIFTED_PACKAGE_CANONICAL : STATUS.WOULD_UPDATE;
|
|
50
|
+
return { outputRelativePath: entry.outputRelativePath, ownership: entry.ownership, status, detail: entry.state === "missing" ? "LF-normalized package-owned projection is missing" : "projection diverges from LF-normalized package canonical text" };
|
|
51
|
+
}
|
|
64
52
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
status: STATUS.DRIFTED_PRESERVED,
|
|
70
|
-
detail: `${entry.ownership}: host owns canonical content; sync preserves host copy`,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
53
|
+
async function applyProjection(entry) {
|
|
54
|
+
await mkdir(path.dirname(entry.absolutePath), { recursive: true });
|
|
55
|
+
await writeFile(entry.absolutePath, normalizeTextToLf(entry.projectionContent ?? entry.content), "utf8");
|
|
56
|
+
}
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
outputRelativePath: entry.outputRelativePath,
|
|
78
|
-
ownership: entry.ownership,
|
|
79
|
-
status: STATUS.UPDATED,
|
|
80
|
-
detail: "drifted package_canonical file rewritten to package source",
|
|
81
|
-
};
|
|
58
|
+
function managedBlockResult(state, mode, applied = false) {
|
|
59
|
+
if (!state.changed) {
|
|
60
|
+
return { outputRelativePath: `${state.relativePath}#nimicoding-managed-block`, ownership: "package_managed_block", status: STATUS.IN_SYNC, detail: "managed block matches package instructions" };
|
|
82
61
|
}
|
|
83
|
-
|
|
84
62
|
return {
|
|
85
|
-
outputRelativePath:
|
|
86
|
-
ownership:
|
|
87
|
-
status: mode === SYNC_MODE.CHECK
|
|
88
|
-
|
|
89
|
-
: STATUS.WOULD_UPDATE,
|
|
90
|
-
detail: "host file diverges from package_canonical source",
|
|
63
|
+
outputRelativePath: `${state.relativePath}#nimicoding-managed-block`,
|
|
64
|
+
ownership: "package_managed_block",
|
|
65
|
+
status: applied ? STATUS.UPDATED : mode === SYNC_MODE.CHECK ? STATUS.MANAGED_BLOCK_DRIFT : STATUS.WOULD_UPDATE,
|
|
66
|
+
detail: "managed block is missing or drifted; host-owned text outside the block is not inspected",
|
|
91
67
|
};
|
|
92
68
|
}
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
const
|
|
70
|
+
async function inspectDeprecatedPaths(projectRoot) {
|
|
71
|
+
const policy = await loadSeedPolicy();
|
|
96
72
|
const results = [];
|
|
97
|
-
for (const
|
|
98
|
-
|
|
73
|
+
for (const ref of policy.deprecatedProjections) {
|
|
74
|
+
if (await pathExists(path.join(projectRoot, ref))) {
|
|
75
|
+
results.push({
|
|
76
|
+
outputRelativePath: ref,
|
|
77
|
+
ownership: "deprecated_package_projection",
|
|
78
|
+
status: STATUS.DEPRECATED_PROJECTION_PATH,
|
|
79
|
+
detail: "deprecated package projection must be removed by the host; sync does not delete host files",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function runSeedSync(projectRoot, mode = SYNC_MODE.DRY_RUN) {
|
|
87
|
+
await preflightManagedProjectPaths(projectRoot);
|
|
88
|
+
const seedEntries = await getBootstrapSeedEntries();
|
|
89
|
+
const projectionStates = [];
|
|
90
|
+
for (const entry of seedEntries) projectionStates.push(await inspectProjection(projectRoot, entry));
|
|
91
|
+
const blockStates = await inspectEntrypointIntegration(projectRoot);
|
|
92
|
+
const deprecatedResults = await inspectDeprecatedPaths(projectRoot);
|
|
93
|
+
|
|
94
|
+
const mayApply = mode === SYNC_MODE.APPLY && deprecatedResults.length === 0;
|
|
95
|
+
if (mayApply) {
|
|
96
|
+
for (const state of projectionStates) if (state.state !== "in_sync") await applyProjection(state);
|
|
97
|
+
if (blockStates.some((state) => state.changed)) await integrateEntrypoints(projectRoot);
|
|
99
98
|
}
|
|
100
99
|
|
|
100
|
+
const results = [
|
|
101
|
+
...projectionStates.map((state) => projectionResult(state, mode, mayApply && state.state !== "in_sync")),
|
|
102
|
+
...blockStates.map((state) => managedBlockResult(state, mode, mayApply && state.changed)),
|
|
103
|
+
...deprecatedResults,
|
|
104
|
+
];
|
|
101
105
|
const summary = {
|
|
102
106
|
total: results.length,
|
|
103
107
|
in_sync: 0,
|
|
@@ -105,39 +109,24 @@ export async function runSeedSync(projectRoot, mode = SYNC_MODE.DRY_RUN) {
|
|
|
105
109
|
updated: 0,
|
|
106
110
|
would_create: 0,
|
|
107
111
|
would_update: 0,
|
|
108
|
-
drifted_preserved: 0,
|
|
109
|
-
missing_host_state_seed: 0,
|
|
110
112
|
missing_package_canonical: 0,
|
|
111
113
|
drifted_package_canonical: 0,
|
|
114
|
+
managed_block_drift: 0,
|
|
115
|
+
deprecated_projection_path: 0,
|
|
112
116
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const checkFailures = mode === SYNC_MODE.CHECK
|
|
127
|
-
? results.filter((r) =>
|
|
128
|
-
r.status === STATUS.MISSING_PACKAGE_CANONICAL
|
|
129
|
-
|| r.status === STATUS.MISSING_HOST_STATE_SEED
|
|
130
|
-
|| r.status === STATUS.DRIFTED_PACKAGE_CANONICAL,
|
|
131
|
-
)
|
|
132
|
-
: [];
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
mode,
|
|
136
|
-
summary,
|
|
137
|
-
results,
|
|
138
|
-
ok: mode === SYNC_MODE.CHECK ? checkFailures.length === 0 : true,
|
|
139
|
-
checkFailures,
|
|
140
|
-
};
|
|
117
|
+
for (const result of results) summary[result.status] = (summary[result.status] ?? 0) + 1;
|
|
118
|
+
|
|
119
|
+
const checkFailureStatuses = new Set([
|
|
120
|
+
STATUS.MISSING_PACKAGE_CANONICAL,
|
|
121
|
+
STATUS.DRIFTED_PACKAGE_CANONICAL,
|
|
122
|
+
STATUS.MANAGED_BLOCK_DRIFT,
|
|
123
|
+
STATUS.DEPRECATED_PROJECTION_PATH,
|
|
124
|
+
]);
|
|
125
|
+
const checkFailures = results.filter((result) =>
|
|
126
|
+
result.status === STATUS.DEPRECATED_PROJECTION_PATH
|
|
127
|
+
|| (mode === SYNC_MODE.CHECK && checkFailureStatuses.has(result.status)),
|
|
128
|
+
);
|
|
129
|
+
return { mode, summary, results, ok: checkFailures.length === 0, checkFailures };
|
|
141
130
|
}
|
|
142
131
|
|
|
143
132
|
export const SYNC_RESULT_STATUS = STATUS;
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
export function arraysEqual(left, right) {
|
|
2
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
export function isPlainObject(value) {
|
|
6
2
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
3
|
}
|
|
8
|
-
|
|
9
|
-
export function isIsoUtcTimestamp(value) {
|
|
10
|
-
if (typeof value !== "string") {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const parsed = new Date(value);
|
|
19
|
-
const canonicalValue = value.includes(".") ? value : value.replace("Z", ".000Z");
|
|
20
|
-
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === canonicalValue;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function toStringArray(value) {
|
|
24
|
-
if (!Array.isArray(value)) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return value.map((entry) => String(entry));
|
|
29
|
-
}
|
package/cli/lib/yaml-helpers.mjs
CHANGED
|
@@ -1,133 +1,10 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
1
|
import YAML from "yaml";
|
|
4
2
|
|
|
5
|
-
import { pathExists } from "./fs-helpers.mjs";
|
|
6
|
-
import { isPlainObject, toStringArray } from "./value-helpers.mjs";
|
|
7
|
-
|
|
8
|
-
function findFirstMatchingKey(node, targetKey) {
|
|
9
|
-
if (Array.isArray(node)) {
|
|
10
|
-
for (const entry of node) {
|
|
11
|
-
const found = findFirstMatchingKey(entry, targetKey);
|
|
12
|
-
if (found !== undefined) {
|
|
13
|
-
return found;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (!isPlainObject(node)) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (Object.prototype.hasOwnProperty.call(node, targetKey)) {
|
|
24
|
-
return node[targetKey];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
for (const value of Object.values(node)) {
|
|
28
|
-
const found = findFirstMatchingKey(value, targetKey);
|
|
29
|
-
if (found !== undefined) {
|
|
30
|
-
return found;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
3
|
export function parseYamlText(text) {
|
|
38
|
-
if (!text)
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
4
|
+
if (!text) return null;
|
|
42
5
|
try {
|
|
43
6
|
return YAML.parse(text);
|
|
44
7
|
} catch {
|
|
45
8
|
return null;
|
|
46
9
|
}
|
|
47
10
|
}
|
|
48
|
-
|
|
49
|
-
export async function loadYamlFile(filePath) {
|
|
50
|
-
const info = await pathExists(filePath);
|
|
51
|
-
if (!info || !info.isFile()) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
return YAML.parse(await readFile(filePath, "utf8"));
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function readYamlScalar(text, key) {
|
|
63
|
-
const parsed = parseYamlText(text);
|
|
64
|
-
const value = findFirstMatchingKey(parsed, key);
|
|
65
|
-
|
|
66
|
-
if (value === undefined || value === null || isPlainObject(value) || Array.isArray(value)) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return String(value);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function readYamlList(text, key) {
|
|
74
|
-
const parsed = parseYamlText(text);
|
|
75
|
-
const value = findFirstMatchingKey(parsed, key);
|
|
76
|
-
return toStringArray(value);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function mergeOrderedPaths(...groups) {
|
|
80
|
-
const merged = [];
|
|
81
|
-
|
|
82
|
-
for (const group of groups) {
|
|
83
|
-
for (const entry of group) {
|
|
84
|
-
if (entry && !merged.includes(entry)) {
|
|
85
|
-
merged.push(entry);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return merged;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function parseSkillSection(text, sectionKey) {
|
|
94
|
-
const parsed = parseYamlText(text);
|
|
95
|
-
const section = parsed?.[sectionKey];
|
|
96
|
-
|
|
97
|
-
if (!Array.isArray(section)) {
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return section.map((entry) => ({
|
|
102
|
-
...entry,
|
|
103
|
-
id: typeof entry?.id === "string" ? entry.id : null,
|
|
104
|
-
inputs: Array.isArray(entry?.inputs) ? entry.inputs.map((item) => String(item)) : [],
|
|
105
|
-
required: entry?.required === undefined ? undefined : String(entry.required),
|
|
106
|
-
source: entry?.source === undefined ? undefined : String(entry.source),
|
|
107
|
-
purpose: entry?.purpose === undefined ? undefined : String(entry.purpose),
|
|
108
|
-
result_contract_ref: entry?.result_contract_ref === undefined ? undefined : String(entry.result_contract_ref),
|
|
109
|
-
})).filter((entry) => entry.id);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function parsePathRequirements(text, sectionKey) {
|
|
113
|
-
const parsed = parseYamlText(text);
|
|
114
|
-
const section = parsed?.[sectionKey];
|
|
115
|
-
|
|
116
|
-
if (!Array.isArray(section)) {
|
|
117
|
-
return [];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return section
|
|
121
|
-
.filter((entry) => isPlainObject(entry) && typeof entry.path === "string")
|
|
122
|
-
.map((entry) => ({
|
|
123
|
-
path: entry.path,
|
|
124
|
-
required_top_level_keys: Array.isArray(entry.required_top_level_keys)
|
|
125
|
-
? entry.required_top_level_keys.map((item) => String(item))
|
|
126
|
-
: [],
|
|
127
|
-
}));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function readTopLevelKeys(text) {
|
|
131
|
-
const parsed = parseYamlText(text);
|
|
132
|
-
return isPlainObject(parsed) ? Object.keys(parsed) : [];
|
|
133
|
-
}
|
package/cli/seeds/bootstrap.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
@@ -6,96 +6,46 @@ import YAML from "yaml";
|
|
|
6
6
|
|
|
7
7
|
const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
8
8
|
const POLICY_PATH = fileURLToPath(new URL("./seed-policy.yaml", import.meta.url));
|
|
9
|
-
|
|
10
|
-
const SUPPORTED_OWNERSHIP = new Set(["package_canonical", "host_state_seed", "host_profile_override"]);
|
|
11
|
-
|
|
12
9
|
let cachedPolicy = null;
|
|
13
10
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
11
|
+
function portable(value) {
|
|
12
|
+
return value.split(path.sep).join(path.posix.sep);
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const text = await readFile(POLICY_PATH, "utf8");
|
|
24
|
-
const parsed = YAML.parse(text);
|
|
25
|
-
|
|
26
|
-
if (!parsed || typeof parsed !== "object") {
|
|
27
|
-
throw new Error(`seed policy at ${POLICY_PATH} is not a valid YAML object`);
|
|
15
|
+
function admittedRelativePath(value, label) {
|
|
16
|
+
if (typeof value !== "string" || !value || path.posix.isAbsolute(value) || value.startsWith("./") || value.split("/").includes("..") || path.posix.normalize(value) !== value) {
|
|
17
|
+
throw new Error(`seed policy ${label} must be a portable relative path`);
|
|
28
18
|
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
async function loadPolicy() {
|
|
23
|
+
if (cachedPolicy) return cachedPolicy;
|
|
24
|
+
const parsed = YAML.parse(await readFile(POLICY_PATH, "utf8"));
|
|
25
|
+
if (parsed?.policy_id !== "nimicoding.seed-projection.v3" || !Array.isArray(parsed.projections) || parsed.projections.length !== 1) {
|
|
26
|
+
throw new Error("seed policy must declare the v3 exact projection registry");
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
const seenOutput = new Set();
|
|
30
|
+
const projections = parsed.projections.map((entry) => {
|
|
31
|
+
const sourceRelativePath = admittedRelativePath(entry?.source, "source");
|
|
32
|
+
const outputRelativePath = admittedRelativePath(entry?.output, "output");
|
|
33
|
+
if (!outputRelativePath.startsWith(".nimi/")) throw new Error(`seed policy output must stay below .nimi: ${outputRelativePath}`);
|
|
34
|
+
if (entry?.ownership !== "package_canonical") throw new Error(`seed policy ownership is unsupported: ${entry?.ownership}`);
|
|
35
|
+
if (typeof entry?.downstream_consumer !== "string" || !entry.downstream_consumer.trim()) throw new Error(`seed policy entry requires a downstream consumer: ${sourceRelativePath}`);
|
|
36
|
+
if (seenOutput.has(outputRelativePath)) throw new Error(`seed policy contains duplicate output: ${outputRelativePath}`);
|
|
37
|
+
seenOutput.add(outputRelativePath);
|
|
38
|
+
return { sourceRelativePath, outputRelativePath, ownership: entry.ownership, downstreamConsumer: entry.downstream_consumer };
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
throw new Error(
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const excludedRaw = Array.isArray(parsed.excluded_projection) ? parsed.excluded_projection : [];
|
|
47
|
-
const excludedSourceRelativePaths = new Set(excludedRaw.map((entry) => String(entry)));
|
|
48
|
-
|
|
49
|
-
const overrideEntries = Array.isArray(parsed.ownership_overrides) ? parsed.ownership_overrides : [];
|
|
50
|
-
const ownershipByOutputPath = new Map();
|
|
51
|
-
for (const entry of overrideEntries) {
|
|
52
|
-
if (!entry || typeof entry.path !== "string" || typeof entry.ownership !== "string") {
|
|
53
|
-
throw new Error("seed policy ownership_overrides entries must declare path and ownership strings");
|
|
54
|
-
}
|
|
55
|
-
if (!SUPPORTED_OWNERSHIP.has(entry.ownership)) {
|
|
56
|
-
throw new Error(`seed policy ownership override at ${entry.path} declares unsupported ownership: ${entry.ownership}`);
|
|
57
|
-
}
|
|
58
|
-
ownershipByOutputPath.set(entry.path, entry.ownership);
|
|
41
|
+
const deprecatedProjections = (parsed.deprecated_projections ?? []).map((entry) => admittedRelativePath(entry, "deprecated projection"));
|
|
42
|
+
if (new Set(deprecatedProjections).size !== deprecatedProjections.length || deprecatedProjections.some((entry) => seenOutput.has(entry))) {
|
|
43
|
+
throw new Error("seed policy deprecated projections must be unique and disjoint from current projections");
|
|
59
44
|
}
|
|
60
|
-
|
|
61
|
-
cachedPolicy = {
|
|
62
|
-
policyId: parsed.policy_id ?? null,
|
|
63
|
-
projections,
|
|
64
|
-
defaultOwnership,
|
|
65
|
-
excludedSourceRelativePaths,
|
|
66
|
-
ownershipByOutputPath,
|
|
67
|
-
};
|
|
45
|
+
cachedPolicy = { policyId: parsed.policy_id, projections, deprecatedProjections: deprecatedProjections.sort() };
|
|
68
46
|
return cachedPolicy;
|
|
69
47
|
}
|
|
70
48
|
|
|
71
|
-
async function collectProjectedEntries(policy, projection, rootPath, currentPath, entries) {
|
|
72
|
-
const directoryEntries = await readdir(currentPath, { withFileTypes: true });
|
|
73
|
-
for (const entry of directoryEntries) {
|
|
74
|
-
const absolutePath = path.join(currentPath, entry.name);
|
|
75
|
-
if (entry.isDirectory()) {
|
|
76
|
-
await collectProjectedEntries(policy, projection, rootPath, absolutePath, entries);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const relativeFromSourceRoot = toPortableRelativePath(path.relative(rootPath, absolutePath));
|
|
81
|
-
const sourceRelativePath = `${projection.sourceDir}/${relativeFromSourceRoot}`;
|
|
82
|
-
if (policy.excludedSourceRelativePaths.has(sourceRelativePath)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const outputRelativePath = `${projection.outputDir}/${relativeFromSourceRoot}`;
|
|
87
|
-
const ownership = policy.ownershipByOutputPath.get(outputRelativePath) ?? policy.defaultOwnership;
|
|
88
|
-
const content = await readFile(absolutePath, "utf8");
|
|
89
|
-
entries.push({
|
|
90
|
-
outputRelativePath,
|
|
91
|
-
sourceRelativePath,
|
|
92
|
-
sourceAbsolutePath: absolutePath,
|
|
93
|
-
content,
|
|
94
|
-
ownership,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
49
|
export async function loadSeedPolicy() {
|
|
100
50
|
return loadPolicy();
|
|
101
51
|
}
|
|
@@ -103,21 +53,15 @@ export async function loadSeedPolicy() {
|
|
|
103
53
|
export async function getBootstrapSeedEntries() {
|
|
104
54
|
const policy = await loadPolicy();
|
|
105
55
|
const entries = [];
|
|
106
|
-
|
|
107
56
|
for (const projection of policy.projections) {
|
|
108
|
-
const
|
|
109
|
-
|
|
57
|
+
const sourceAbsolutePath = path.resolve(PACKAGE_ROOT, projection.sourceRelativePath);
|
|
58
|
+
if (portable(path.relative(PACKAGE_ROOT, sourceAbsolutePath)) !== projection.sourceRelativePath) throw new Error(`seed source escapes package: ${projection.sourceRelativePath}`);
|
|
59
|
+
entries.push({ ...projection, sourceAbsolutePath, content: await readFile(sourceAbsolutePath, "utf8") });
|
|
110
60
|
}
|
|
111
|
-
|
|
112
|
-
entries.sort((a, b) => a.outputRelativePath.localeCompare(b.outputRelativePath));
|
|
113
61
|
return entries;
|
|
114
62
|
}
|
|
115
63
|
|
|
116
64
|
export async function createBootstrapSeedFileMap() {
|
|
117
65
|
const entries = await getBootstrapSeedEntries();
|
|
118
|
-
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
seedMap.set(entry.outputRelativePath, entry.content);
|
|
121
|
-
}
|
|
122
|
-
return seedMap;
|
|
66
|
+
return new Map(entries.map((entry) => [entry.outputRelativePath, entry.content]));
|
|
123
67
|
}
|