@nimiplatform/nimi-coding 0.3.1 → 0.4.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/README.md +285 -41
  4. package/README.zh-CN.md +294 -31
  5. package/bin/nimicoding.mjs +1 -1
  6. package/cli/commands/authority.mjs +849 -0
  7. package/cli/commands/clear.mjs +27 -7
  8. package/cli/commands/doctor.mjs +10 -1
  9. package/cli/commands/start.mjs +23 -27
  10. package/cli/commands/sync.mjs +21 -101
  11. package/cli/constants.mjs +7 -31
  12. package/cli/dev/check-authority-surface.mjs +173 -0
  13. package/cli/dev/check-product-conformance.mjs +917 -0
  14. package/cli/help.mjs +10 -17
  15. package/cli/index.mjs +6 -47
  16. package/cli/lib/authority/anchors.mjs +491 -0
  17. package/cli/lib/authority/audit.mjs +490 -0
  18. package/cli/lib/authority/closed-sets.mjs +646 -0
  19. package/cli/lib/authority/compile.mjs +110 -0
  20. package/cli/lib/authority/consumers.mjs +470 -0
  21. package/cli/lib/authority/diagnostics.mjs +87 -0
  22. package/cli/lib/authority/diff.mjs +287 -0
  23. package/cli/lib/authority/discover.mjs +447 -0
  24. package/cli/lib/authority/evidence-snapshot.mjs +1 -0
  25. package/cli/lib/authority/evidence.mjs +1432 -0
  26. package/cli/lib/authority/format.mjs +197 -0
  27. package/cli/lib/authority/git-snapshot.mjs +1065 -0
  28. package/cli/lib/authority/graph.mjs +351 -0
  29. package/cli/lib/authority/impact.mjs +252 -0
  30. package/cli/lib/authority/lexicon.mjs +55 -0
  31. package/cli/lib/authority/query.mjs +162 -0
  32. package/cli/lib/authority/repository-corpus.mjs +37 -0
  33. package/cli/lib/authority/repository-inventory.mjs +34 -0
  34. package/cli/lib/authority/review.mjs +222 -0
  35. package/cli/lib/authority/sarif.mjs +181 -0
  36. package/cli/lib/authority/scope-bindings.mjs +225 -0
  37. package/cli/lib/authority/source-map.mjs +105 -0
  38. package/cli/lib/authority/source-markdown.mjs +167 -0
  39. package/cli/lib/authority/source-yaml.mjs +312 -0
  40. package/cli/lib/authority/surface.mjs +79 -0
  41. package/cli/lib/authority/terms.mjs +421 -0
  42. package/cli/lib/authority/tracked-text.mjs +70 -0
  43. package/cli/lib/authority/validate.mjs +308 -0
  44. package/cli/lib/bootstrap.mjs +55 -71
  45. package/cli/lib/doctor.mjs +40 -123
  46. package/cli/lib/entrypoints.mjs +103 -105
  47. package/cli/lib/fs-helpers.mjs +139 -15
  48. package/cli/lib/internal/governance/config.mjs +0 -36
  49. package/cli/lib/shared.mjs +8 -21
  50. package/cli/lib/sync.mjs +81 -92
  51. package/cli/lib/value-helpers.mjs +0 -26
  52. package/cli/lib/yaml-helpers.mjs +1 -124
  53. package/cli/seeds/bootstrap.mjs +31 -87
  54. package/cli/seeds/seed-policy.yaml +9 -63
  55. package/contracts/authority-evidence-bindings.schema.yaml +53 -0
  56. package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
  57. package/contracts/authority-impact-dispositions.schema.yaml +13 -0
  58. package/contracts/authority-scope-bindings.schema.yaml +15 -0
  59. package/contracts/authority-source.schema.yaml +64 -0
  60. package/contracts/authority-verifier-bindings.schema.yaml +20 -0
  61. package/methodology/authority-authoring.yaml +144 -0
  62. package/methodology/core.yaml +31 -26
  63. package/package.json +10 -6
  64. package/spec/authority-anchors.yaml +99 -0
  65. package/spec/authority-audit.yaml +85 -0
  66. package/spec/authority-authoring.yaml +150 -0
  67. package/spec/authority-closed-sets.yaml +65 -0
  68. package/spec/authority-consumers.yaml +70 -0
  69. package/spec/authority-context.yaml +50 -0
  70. package/spec/authority-discovery.yaml +119 -0
  71. package/spec/authority-evidence.yaml +200 -0
  72. package/spec/authority-graph.yaml +59 -0
  73. package/spec/authority-impact.yaml +84 -0
  74. package/spec/authority-review.yaml +89 -0
  75. package/spec/authority-terms.yaml +59 -0
  76. package/spec/product-scope.yaml +36 -23
  77. package/cli/commands/blueprint-audit.mjs +0 -91
  78. package/cli/commands/classify-spec-tree.mjs +0 -5
  79. package/cli/commands/generate-spec-derived-docs.mjs +0 -151
  80. package/cli/commands/generate-spec-migration-plan.mjs +0 -30
  81. package/cli/commands/surface-validator-command.mjs +0 -49
  82. package/cli/commands/validate-domain-admission.mjs +0 -5
  83. package/cli/commands/validate-guidance-bodies.mjs +0 -5
  84. package/cli/commands/validate-placement.mjs +0 -5
  85. package/cli/commands/validate-projection-edges.mjs +0 -5
  86. package/cli/commands/validate-spec-audit.mjs +0 -27
  87. package/cli/commands/validate-spec-governance.mjs +0 -144
  88. package/cli/commands/validate-spec-tree.mjs +0 -27
  89. package/cli/commands/validate-table-family.mjs +0 -5
  90. package/cli/commands/validate-tracked-output-admission.mjs +0 -5
  91. package/cli/lib/blueprint-audit.mjs +0 -363
  92. package/cli/lib/contracts.mjs +0 -21
  93. package/cli/lib/internal/contracts-loaders.mjs +0 -47
  94. package/cli/lib/internal/contracts-parse.mjs +0 -192
  95. package/cli/lib/internal/governance/runner.mjs +0 -35
  96. package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
  97. package/cli/lib/internal/validators-shared.mjs +0 -12
  98. package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
  99. package/cli/lib/internal/validators-spec.mjs +0 -380
  100. package/cli/lib/validators.mjs +0 -30
  101. package/config/bootstrap.yaml +0 -6
  102. package/config/spec-generation-inputs.yaml +0 -41
  103. package/contracts/domain-admission.schema.yaml +0 -56
  104. package/contracts/migration-inventory.schema.yaml +0 -15
  105. package/contracts/negative-fixtures.yaml +0 -26
  106. package/contracts/placement-contract.schema.yaml +0 -127
  107. package/contracts/projection-edge.schema.yaml +0 -46
  108. package/contracts/shared-enums.yaml +0 -63
  109. package/contracts/spec-generation-audit.schema.yaml +0 -46
  110. package/contracts/spec-generation-inputs.schema.yaml +0 -43
  111. package/contracts/spec-layout.schema.yaml +0 -32
  112. package/contracts/surface-taxonomy.schema.yaml +0 -139
  113. package/contracts/table-family.schema.yaml +0 -133
  114. package/contracts/tracked-output-admission.schema.yaml +0 -17
  115. package/methodology/four-closure-policy.yaml +0 -20
  116. package/methodology/role-separation-policy.yaml +0 -22
  117. package/methodology/spec-reconstruction.yaml +0 -69
  118. 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)
@@ -1,22 +1,16 @@
1
1
  export {
2
- buildBlueprintAuditPayload,
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 { pathExists, readTextIfFile } from "./fs-helpers.mjs";
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
- const HOST_OWNED_SEED_OWNERSHIPS = new Set(["host_state_seed", "host_profile_override"]);
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
- const fileExists = Boolean(info && info.isFile());
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
- if (!fileExists) {
33
- if (mode === SYNC_MODE.APPLY) {
34
- await mkdir(path.dirname(absolutePath), { recursive: true });
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.IN_SYNC,
61
- detail: "host file matches package source byte-for-byte",
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
- if (HOST_OWNED_SEED_OWNERSHIPS.has(entry.ownership)) {
66
- return {
67
- outputRelativePath: entry.outputRelativePath,
68
- ownership: entry.ownership,
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
- if (mode === SYNC_MODE.APPLY) {
75
- await writeFile(absolutePath, entry.content, "utf8");
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: entry.outputRelativePath,
86
- ownership: entry.ownership,
87
- status: mode === SYNC_MODE.CHECK
88
- ? STATUS.DRIFTED_PACKAGE_CANONICAL
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
- export async function runSeedSync(projectRoot, mode = SYNC_MODE.DRY_RUN) {
95
- const entries = await getBootstrapSeedEntries();
70
+ async function inspectDeprecatedPaths(projectRoot) {
71
+ const policy = await loadSeedPolicy();
96
72
  const results = [];
97
- for (const entry of entries) {
98
- results.push(await evaluateSeedEntry(projectRoot, entry, mode));
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
- for (const result of results) {
115
- summary[result.status] = (summary[result.status] ?? 0) + 1;
116
- }
117
-
118
- // Re-derive dry-run status counters when no apply happened.
119
- if (mode === SYNC_MODE.DRY_RUN) {
120
- summary.would_create = results.filter((r) =>
121
- r.status === STATUS.MISSING_HOST_STATE_SEED || r.status === STATUS.MISSING_PACKAGE_CANONICAL,
122
- ).length;
123
- summary.would_update = results.filter((r) => r.status === STATUS.WOULD_UPDATE).length;
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
- }
@@ -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
- }
@@ -1,4 +1,4 @@
1
- import { readFile, readdir } from "node:fs/promises";
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 toPortableRelativePath(filePath) {
15
- return filePath.split(path.sep).join("/");
11
+ function portable(value) {
12
+ return value.split(path.sep).join(path.posix.sep);
16
13
  }
17
14
 
18
- async function loadPolicy() {
19
- if (cachedPolicy) {
20
- return cachedPolicy;
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
- if (!Array.isArray(parsed.projections) || parsed.projections.length === 0) {
31
- throw new Error("seed policy is missing required `projections` list");
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 projections = parsed.projections.map((projection) => {
35
- if (!projection || typeof projection.source_dir !== "string" || typeof projection.output_dir !== "string") {
36
- throw new Error("seed policy projection entries must declare source_dir and output_dir strings");
37
- }
38
- return { sourceDir: projection.source_dir, outputDir: projection.output_dir };
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 defaultOwnership = parsed.default_ownership ?? "package_canonical";
42
- if (!SUPPORTED_OWNERSHIP.has(defaultOwnership)) {
43
- throw new Error(`seed policy declares unsupported default_ownership: ${defaultOwnership}`);
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 sourceRoot = path.join(PACKAGE_ROOT, projection.sourceDir);
109
- await collectProjectedEntries(policy, projection, sourceRoot, sourceRoot, entries);
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
- const seedMap = new Map();
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
  }