@nimiplatform/nimi-coding 0.2.7 → 0.3.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 (207) hide show
  1. package/CHANGELOG.md +8 -80
  2. package/CONTRIBUTING.md +5 -34
  3. package/README.md +47 -350
  4. package/README.zh-CN.md +33 -287
  5. package/cli/commands/start.mjs +48 -688
  6. package/cli/commands/validate-spec-audit.mjs +1 -5
  7. package/cli/constants.mjs +9 -417
  8. package/cli/dev/run-cli-smoke.mjs +24 -0
  9. package/cli/dev/run-suite.mjs +17 -0
  10. package/cli/help.mjs +28 -145
  11. package/cli/index.mjs +5 -33
  12. package/cli/lib/blueprint-audit.mjs +5 -12
  13. package/cli/lib/bootstrap.mjs +0 -9
  14. package/cli/lib/contracts.mjs +0 -163
  15. package/cli/lib/doctor.mjs +155 -10
  16. package/cli/lib/entrypoints.mjs +63 -167
  17. package/cli/lib/internal/contracts-loaders.mjs +0 -85
  18. package/cli/lib/internal/contracts-parse.mjs +84 -520
  19. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +13 -8
  20. package/cli/lib/internal/surface-taxonomy-validators.mjs +316 -66
  21. package/cli/lib/internal/validators-shared.mjs +0 -16
  22. package/cli/lib/internal/validators-spec-helpers.mjs +7 -140
  23. package/cli/lib/internal/validators-spec.mjs +103 -342
  24. package/cli/lib/shared.mjs +1 -42
  25. package/cli/lib/validators.mjs +0 -48
  26. package/cli/seeds/seed-policy.yaml +0 -3
  27. package/config/bootstrap.yaml +4 -4
  28. package/config/spec-generation-inputs.yaml +6 -12
  29. package/contracts/domain-admission.schema.yaml +3 -3
  30. package/contracts/migration-inventory.schema.yaml +12 -75
  31. package/contracts/negative-fixtures.yaml +19 -99
  32. package/contracts/placement-contract.schema.yaml +49 -78
  33. package/contracts/projection-edge.schema.yaml +37 -121
  34. package/contracts/shared-enums.yaml +4 -8
  35. package/contracts/spec-generation-inputs.schema.yaml +15 -112
  36. package/contracts/spec-layout.schema.yaml +32 -0
  37. package/contracts/surface-taxonomy.schema.yaml +43 -94
  38. package/contracts/table-family.schema.yaml +1 -1
  39. package/contracts/tracked-output-admission.schema.yaml +13 -66
  40. package/methodology/core.yaml +33 -25
  41. package/methodology/four-closure-policy.yaml +18 -26
  42. package/methodology/role-separation-policy.yaml +21 -27
  43. package/methodology/spec-reconstruction.yaml +17 -34
  44. package/package.json +4 -6
  45. package/spec/_meta/spec-tree-model.yaml +5 -101
  46. package/spec/product-scope.yaml +25 -57
  47. package/adapters/README.md +0 -25
  48. package/adapters/claude/README.md +0 -89
  49. package/adapters/claude/profile.yaml +0 -70
  50. package/adapters/codex/README.md +0 -53
  51. package/adapters/codex/profile.yaml +0 -78
  52. package/adapters/oh-my-codex/README.md +0 -184
  53. package/adapters/oh-my-codex/profile.yaml +0 -46
  54. package/cli/commands/admit-high-risk-decision.mjs +0 -108
  55. package/cli/commands/audit-sweep.mjs +0 -364
  56. package/cli/commands/closeout.mjs +0 -186
  57. package/cli/commands/decide-high-risk-execution.mjs +0 -124
  58. package/cli/commands/handoff.mjs +0 -123
  59. package/cli/commands/ingest-high-risk-execution.mjs +0 -95
  60. package/cli/commands/review-high-risk-execution.mjs +0 -95
  61. package/cli/commands/sweep-design.mjs +0 -295
  62. package/cli/commands/sweep.mjs +0 -22
  63. package/cli/commands/topic-formatters.mjs +0 -382
  64. package/cli/commands/topic-goal.mjs +0 -33
  65. package/cli/commands/topic-options-shared.mjs +0 -27
  66. package/cli/commands/topic-options-workflow.mjs +0 -767
  67. package/cli/commands/topic-options.mjs +0 -626
  68. package/cli/commands/topic-runner.mjs +0 -169
  69. package/cli/commands/topic.mjs +0 -795
  70. package/cli/commands/validate-acceptance.mjs +0 -5
  71. package/cli/commands/validate-execution-packet.mjs +0 -5
  72. package/cli/commands/validate-orchestration-state.mjs +0 -5
  73. package/cli/commands/validate-prompt.mjs +0 -5
  74. package/cli/commands/validate-worker-output.mjs +0 -5
  75. package/cli/lib/adapter-profiles.mjs +0 -403
  76. package/cli/lib/audit-execution.mjs +0 -52
  77. package/cli/lib/audit-sweep-runtime/admissions.mjs +0 -508
  78. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +0 -356
  79. package/cli/lib/audit-sweep-runtime/chunks.mjs +0 -697
  80. package/cli/lib/audit-sweep-runtime/claude-auditor.mjs +0 -647
  81. package/cli/lib/audit-sweep-runtime/closeout.mjs +0 -144
  82. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +0 -654
  83. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +0 -516
  84. package/cli/lib/audit-sweep-runtime/common.mjs +0 -341
  85. package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +0 -172
  86. package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +0 -155
  87. package/cli/lib/audit-sweep-runtime/format.mjs +0 -57
  88. package/cli/lib/audit-sweep-runtime/ingest.mjs +0 -486
  89. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +0 -425
  90. package/cli/lib/audit-sweep-runtime/inventory.mjs +0 -798
  91. package/cli/lib/audit-sweep-runtime/ledger.mjs +0 -315
  92. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +0 -101
  93. package/cli/lib/audit-sweep-runtime/remediation.mjs +0 -349
  94. package/cli/lib/audit-sweep-runtime/rerun.mjs +0 -129
  95. package/cli/lib/audit-sweep-runtime/risk-budget.mjs +0 -300
  96. package/cli/lib/audit-sweep-runtime/status.mjs +0 -62
  97. package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +0 -215
  98. package/cli/lib/audit-sweep-runtime/validators.mjs +0 -797
  99. package/cli/lib/audit-sweep.mjs +0 -19
  100. package/cli/lib/authority-convergence.mjs +0 -704
  101. package/cli/lib/closeout.mjs +0 -732
  102. package/cli/lib/codex-sdk-runner.mjs +0 -76
  103. package/cli/lib/external-execution.mjs +0 -101
  104. package/cli/lib/handoff.mjs +0 -812
  105. package/cli/lib/high-risk-admission.mjs +0 -512
  106. package/cli/lib/high-risk-decision.mjs +0 -353
  107. package/cli/lib/high-risk-ingest.mjs +0 -321
  108. package/cli/lib/high-risk-review.mjs +0 -267
  109. package/cli/lib/internal/contracts-parse-high-risk.mjs +0 -131
  110. package/cli/lib/internal/contracts-validators.mjs +0 -399
  111. package/cli/lib/internal/doctor-bootstrap-surface.mjs +0 -406
  112. package/cli/lib/internal/doctor-delegated-surface.mjs +0 -256
  113. package/cli/lib/internal/doctor-finalize.mjs +0 -383
  114. package/cli/lib/internal/doctor-format.mjs +0 -286
  115. package/cli/lib/internal/doctor-inspectors.mjs +0 -327
  116. package/cli/lib/internal/doctor-state.mjs +0 -205
  117. package/cli/lib/internal/validators-artifacts.mjs +0 -515
  118. package/cli/lib/sweep-design-runtime/common.mjs +0 -246
  119. package/cli/lib/sweep-design-runtime/engine.mjs +0 -733
  120. package/cli/lib/sweep-design-runtime/fix-topic.mjs +0 -414
  121. package/cli/lib/sweep-design-runtime/lifecycle.mjs +0 -54
  122. package/cli/lib/sweep-design-runtime/results.mjs +0 -324
  123. package/cli/lib/sweep-design.mjs +0 -8
  124. package/cli/lib/topic-artifacts.mjs +0 -186
  125. package/cli/lib/topic-authority-coverage.mjs +0 -73
  126. package/cli/lib/topic-closeout.mjs +0 -560
  127. package/cli/lib/topic-common.mjs +0 -404
  128. package/cli/lib/topic-decisions.mjs +0 -332
  129. package/cli/lib/topic-draft-packets.mjs +0 -167
  130. package/cli/lib/topic-execution.mjs +0 -515
  131. package/cli/lib/topic-goal.mjs +0 -440
  132. package/cli/lib/topic-ledger.mjs +0 -281
  133. package/cli/lib/topic-lifecycle-artifacts.mjs +0 -173
  134. package/cli/lib/topic-root-validation.mjs +0 -288
  135. package/cli/lib/topic-runner-commands.mjs +0 -174
  136. package/cli/lib/topic-runner-deferral.mjs +0 -532
  137. package/cli/lib/topic-runner-stale-gates.mjs +0 -114
  138. package/cli/lib/topic-runner-validation.mjs +0 -138
  139. package/cli/lib/topic-runner.mjs +0 -727
  140. package/cli/lib/topic-scaffold.mjs +0 -252
  141. package/cli/lib/topic-waves.mjs +0 -403
  142. package/cli/lib/topic.mjs +0 -81
  143. package/config/audit-execution-artifacts.yaml +0 -20
  144. package/config/external-execution-artifacts.yaml +0 -16
  145. package/config/host-adapter.yaml +0 -30
  146. package/config/host-profile.yaml +0 -29
  147. package/config/installer-evidence.yaml +0 -31
  148. package/config/skill-installer.yaml +0 -23
  149. package/config/skill-manifest.yaml +0 -48
  150. package/config/skills.yaml +0 -30
  151. package/contracts/acceptance.schema.yaml +0 -16
  152. package/contracts/admission-checklist.schema.yaml +0 -15
  153. package/contracts/audit-chunk.schema.yaml +0 -123
  154. package/contracts/audit-closeout.schema.yaml +0 -51
  155. package/contracts/audit-finding.schema.yaml +0 -61
  156. package/contracts/audit-ledger.schema.yaml +0 -138
  157. package/contracts/audit-plan.schema.yaml +0 -138
  158. package/contracts/audit-remediation-map.schema.yaml +0 -52
  159. package/contracts/audit-rerun.schema.yaml +0 -31
  160. package/contracts/audit-sweep-result.yaml +0 -53
  161. package/contracts/authority-convergence-audit.schema.yaml +0 -19
  162. package/contracts/closeout.schema.yaml +0 -25
  163. package/contracts/decision-review.schema.yaml +0 -16
  164. package/contracts/doc-spec-audit-result.yaml +0 -19
  165. package/contracts/execution-packet.schema.yaml +0 -49
  166. package/contracts/external-host-compatibility.yaml +0 -22
  167. package/contracts/forbidden-shortcuts.catalog.yaml +0 -23
  168. package/contracts/high-risk-admission.schema.yaml +0 -23
  169. package/contracts/high-risk-execution-result.yaml +0 -20
  170. package/contracts/orchestration-state.schema.yaml +0 -41
  171. package/contracts/overflow-continuation.schema.yaml +0 -12
  172. package/contracts/packet.schema.yaml +0 -30
  173. package/contracts/pending-note.schema.yaml +0 -17
  174. package/contracts/prompt.schema.yaml +0 -12
  175. package/contracts/remediation.schema.yaml +0 -16
  176. package/contracts/result.schema.yaml +0 -24
  177. package/contracts/spec-reconstruction-result.yaml +0 -41
  178. package/contracts/sweep-design-result.yaml +0 -349
  179. package/contracts/topic-goal.schema.yaml +0 -87
  180. package/contracts/topic-run-ledger.schema.yaml +0 -72
  181. package/contracts/topic-step-decision.schema.yaml +0 -45
  182. package/contracts/topic.schema.yaml +0 -65
  183. package/contracts/true-close.schema.yaml +0 -15
  184. package/contracts/wave.schema.yaml +0 -29
  185. package/contracts/worker-output.schema.yaml +0 -15
  186. package/contracts/workflow-consumer.schema.yaml +0 -110
  187. package/methodology/audit-sweep-p0p1-recall.yaml +0 -45
  188. package/methodology/authority-convergence-policy.yaml +0 -42
  189. package/methodology/overflow-continuation-policy.yaml +0 -14
  190. package/methodology/skill-exchange-projection.yaml +0 -114
  191. package/methodology/skill-handoff.yaml +0 -34
  192. package/methodology/skill-installer-result.yaml +0 -27
  193. package/methodology/skill-installer-summary-projection.yaml +0 -181
  194. package/methodology/skill-runtime.yaml +0 -23
  195. package/methodology/spec-target-truth-profile.yaml +0 -46
  196. package/methodology/topic-lifecycle-report.yaml +0 -144
  197. package/methodology/topic-lifecycle.yaml +0 -37
  198. package/methodology/topic-naming-ontology.yaml +0 -21
  199. package/methodology/topic-ontology.yaml +0 -38
  200. package/methodology/topic-validation-policy.yaml +0 -9
  201. package/methodology/wave-dag-policy.yaml +0 -14
  202. package/spec/_meta/command-gating-matrix.yaml +0 -143
  203. package/spec/_meta/generate-drift-migration-checklist.yaml +0 -137
  204. package/spec/_meta/governance-routing-cutover-checklist.yaml +0 -35
  205. package/spec/_meta/phase2-impacted-surface-matrix.yaml +0 -44
  206. package/spec/_meta/spec-authority-cutover-readiness.yaml +0 -102
  207. package/spec/bootstrap-state.yaml +0 -99
@@ -28,13 +28,18 @@ const GENERIC_PNPM_COMMANDS = new Set([
28
28
  "run",
29
29
  ]);
30
30
 
31
- function readJson(filePath) {
32
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
33
- }
34
-
35
- function collectKnownPnpmScripts(projectRoot) {
36
- const rootPkg = readJson(path.join(projectRoot, "package.json"));
37
- return new Set(Object.keys(rootPkg.scripts || {}));
31
+ function collectKnownPnpmScripts(projectRoot, failures) {
32
+ const packagePath = path.join(projectRoot, "package.json");
33
+ if (!fs.existsSync(packagePath)) {
34
+ return new Set();
35
+ }
36
+ try {
37
+ const rootPkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
38
+ return new Set(Object.keys(rootPkg?.scripts || {}));
39
+ } catch (error) {
40
+ failures.push(`package.json: invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
41
+ return new Set();
42
+ }
38
43
  }
39
44
 
40
45
  function validatePnpmCommand(command, knownScripts, failures, relPath) {
@@ -91,7 +96,7 @@ export function evaluateAgentsFreshnessCheck(options = {}) {
91
96
  : DEFAULT_STALE_TOKENS;
92
97
 
93
98
  const failures = [];
94
- const knownScripts = collectKnownPnpmScripts(projectRoot);
99
+ const knownScripts = collectKnownPnpmScripts(projectRoot, failures);
95
100
 
96
101
  for (const target of targets) {
97
102
  const abs = path.join(projectRoot, target.rel);
@@ -6,7 +6,7 @@ import { pathExists } from "../fs-helpers.mjs";
6
6
  import { parseYamlText } from "../yaml-helpers.mjs";
7
7
  import { isPlainObject } from "../value-helpers.mjs";
8
8
 
9
- const SURFACE_RESULT_CONTRACT = "nimicoding.surface-validator-result.v1";
9
+ const SURFACE_RESULT_CONTRACT = "nimicoding.surface-validator-result.v2";
10
10
 
11
11
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
12
12
 
@@ -18,12 +18,12 @@ const CONTRACT_REFS = {
18
18
  tableFamily: "contracts/table-family.schema.yaml",
19
19
  domainAdmission: "contracts/domain-admission.schema.yaml",
20
20
  trackedOutputAdmission: "contracts/tracked-output-admission.schema.yaml",
21
- highRiskAdmission: "contracts/high-risk-admission.schema.yaml",
21
+ specLayout: "contracts/spec-layout.schema.yaml",
22
22
  negativeFixtures: "contracts/negative-fixtures.yaml",
23
23
  };
24
24
 
25
25
  const TEXT_RULE_BODY_PATTERN = /\bMUST(?:\s+NOT)?\b|\bmust\s+not\b|必须|不得|fail(?:s|ed)?\s+closed/i;
26
- const PRODUCT_RULE_ID_PATTERN = /\b[A-Z][A-Z0-9]*-[A-Z0-9]+-[A-Z0-9-]+\b/;
26
+ const PRODUCT_RULE_DEFINITION_PATTERN = /^\s*(?:[-*]\s*)?(?:rule_id|rule id)\s*[:|]/im;
27
27
  const GENERATED_REF_PATTERN = /\.nimi\/spec\/[^)\s]+\/kernel\/generated\/|\.nimi\/spec\/generated\/|kernel\/generated\//;
28
28
 
29
29
  function toPosix(value) {
@@ -65,9 +65,227 @@ async function loadSurfaceContracts(projectRoot) {
65
65
  if (hostDomainAdmission) {
66
66
  contracts.domainAdmission = hostDomainAdmission;
67
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
+ );
68
74
  return contracts;
69
75
  }
70
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
+
71
289
  async function collectFilesUnder(rootPath) {
72
290
  const exists = await pathExists(rootPath);
73
291
  if (!exists || !exists.isDirectory()) {
@@ -100,7 +318,6 @@ async function collectCandidateFiles(projectRoot, rootRef = ".nimi/spec") {
100
318
  ".nimi/derived",
101
319
  ".nimi/state",
102
320
  ".nimi/audit",
103
- ".nimi/roadmap",
104
321
  ];
105
322
  const uniqueRoots = [...new Set(roots)];
106
323
  const files = [];
@@ -191,7 +408,10 @@ function tableFamilyContractMap(contracts) {
191
408
  const families = Array.isArray(contracts.tableFamily.data?.table_families)
192
409
  ? contracts.tableFamily.data.table_families
193
410
  : [];
194
- return new Map(families.map((entry) => [String(entry.table_family), entry]));
411
+ const extensions = contracts.hostSpecLayout?.ok
412
+ ? contracts.hostSpecLayout.tableFamilyExtensions
413
+ : [];
414
+ return new Map([...families, ...extensions].map((entry) => [String(entry.table_family), entry]));
195
415
  }
196
416
 
197
417
  function domainAdmissionMap(contracts) {
@@ -211,17 +431,34 @@ function trackedAdmissionRoots(contracts) {
211
431
  function isTrackedNonProductRef(ref) {
212
432
  return ref.startsWith(".nimi/derived/")
213
433
  || ref.startsWith(".nimi/state/")
214
- || ref.startsWith(".nimi/audit/")
215
- || ref.startsWith(".nimi/roadmap/");
434
+ || ref.startsWith(".nimi/audit/");
216
435
  }
217
436
 
218
437
  function isAdmittedTrackedOutput(ref, admittedRoots) {
219
438
  return admittedRoots.some((root) => pathMatchesGlob(ref, root));
220
439
  }
221
440
 
222
- function classifyRef(ref, text, parsedYaml) {
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) {
223
454
  if (ref.startsWith(".nimi/spec/future/")) {
224
- return "candidate_roadmap";
455
+ return "unclassified";
456
+ }
457
+ if (isHostInstructionRef(ref, contracts)) {
458
+ return "host_instruction";
459
+ }
460
+ if (projectionForRef(ref, contracts)) {
461
+ return "tracked_derived_projection";
225
462
  }
226
463
  if (ref.startsWith(".nimi/spec/generated/") || ref.includes("/kernel/generated/")) {
227
464
  return "derived_view";
@@ -233,14 +470,8 @@ function classifyRef(ref, text, parsedYaml) {
233
470
  if (ref.endsWith("-admission-anchor.yaml")) {
234
471
  return "host_projection_anchor";
235
472
  }
236
- if (/cutover|checklist|matrix|readiness|migration/i.test(ref)) {
237
- return "lifecycle_progress_state";
238
- }
239
473
  return "methodology_authority";
240
474
  }
241
- if (ref === ".nimi/spec/bootstrap-state.yaml") {
242
- return "lifecycle_progress_state";
243
- }
244
475
  if (ref === ".nimi/spec/product-scope.yaml") {
245
476
  return "methodology_authority";
246
477
  }
@@ -278,9 +509,6 @@ function classifyRef(ref, text, parsedYaml) {
278
509
  if (ref.startsWith(".nimi/local/") || ref.startsWith(".local/")) {
279
510
  return "operational_local_artifact";
280
511
  }
281
- if (ref.startsWith(".nimi/topics/")) {
282
- return "lifecycle_progress_state";
283
- }
284
512
  if (ref.startsWith(".nimi/contracts/") || ref.startsWith(".nimi/methodology/")) {
285
513
  return "nimicoding_managed_projection";
286
514
  }
@@ -294,7 +522,6 @@ function classifyRef(ref, text, parsedYaml) {
294
522
  if (ref.startsWith(".nimi/derived/")) return "derived_view";
295
523
  if (ref.startsWith(".nimi/state/")) return "spec_generation_state";
296
524
  if (ref.startsWith(".nimi/audit/")) return "audit_evidence_state";
297
- return "candidate_roadmap";
298
525
  }
299
526
  return "unclassified";
300
527
  }
@@ -312,7 +539,10 @@ function dispositionFor(ref, surfaceClass, errors) {
312
539
  if (surfaceClass === "derived_view") {
313
540
  return "delete";
314
541
  }
315
- if (["spec_generation_state", "audit_evidence_state", "candidate_roadmap", "lifecycle_progress_state", "operational_local_artifact"].includes(surfaceClass)) {
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)) {
316
546
  return "move_local";
317
547
  }
318
548
  if (errors.length > 0) {
@@ -331,15 +561,15 @@ function targetRootFor(surfaceClass, ref) {
331
561
  if (surfaceClass === "derived_view") {
332
562
  return "stdout_view";
333
563
  }
334
- if (surfaceClass === "spec_generation_state" || surfaceClass === "lifecycle_progress_state" || surfaceClass === "operational_local_artifact") {
564
+ if (surfaceClass === "host_instruction" || surfaceClass === "tracked_derived_projection") {
565
+ return ".nimi/spec";
566
+ }
567
+ if (surfaceClass === "spec_generation_state" || surfaceClass === "operational_local_artifact") {
335
568
  return ".nimi/local/state";
336
569
  }
337
570
  if (surfaceClass === "audit_evidence_state") {
338
571
  return ".nimi/local/audit";
339
572
  }
340
- if (surfaceClass === "candidate_roadmap") {
341
- return ".nimi/topics";
342
- }
343
573
  if (surfaceClass === "unclassified") {
344
574
  return null;
345
575
  }
@@ -356,17 +586,20 @@ function ownerFor(surfaceClass, ref) {
356
586
  if (surfaceClass === "derived_view") {
357
587
  return "generator";
358
588
  }
589
+ if (surfaceClass === "tracked_derived_projection") {
590
+ return "generator";
591
+ }
592
+ if (surfaceClass === "host_instruction") {
593
+ return "product_host";
594
+ }
359
595
  if (surfaceClass === "spec_generation_state") {
360
596
  return "spec_generator";
361
597
  }
362
598
  if (surfaceClass === "audit_evidence_state") {
363
- return "audit_workflow";
599
+ return "verification_process";
364
600
  }
365
- if (surfaceClass === "candidate_roadmap") {
366
- return "planning_owner";
367
- }
368
- if (surfaceClass === "lifecycle_progress_state") {
369
- return "topic_or_execution_workflow";
601
+ if (surfaceClass === "operational_local_artifact") {
602
+ return "external_tooling";
370
603
  }
371
604
  return domainForSpecRef(ref) ?? "product_domain";
372
605
  }
@@ -404,9 +637,10 @@ function validationCommandsFor(entryClass) {
404
637
  product_authority_table: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
405
638
  support_registry: ["pnpm exec nimicoding validate-table-family --profile nimi --root .nimi/spec"],
406
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"],
407
642
  host_projection_anchor: ["pnpm exec nimicoding validate-projection-edges --profile nimi --root .nimi/spec"],
408
643
  nimicoding_managed_projection: ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"],
409
- candidate_roadmap: ["pnpm exec nimicoding validate-domain-admission --profile nimi --root .nimi/spec"],
410
644
  };
411
645
  return commands[entryClass] ?? ["pnpm exec nimicoding validate-placement --profile nimi --root .nimi/spec"];
412
646
  }
@@ -424,6 +658,17 @@ function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
424
658
  if (surfaceClass === "derived_view" && ref.startsWith(".nimi/spec/")) {
425
659
  addError(errors, "derived_view_under_product_authority_root", ref);
426
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
+ }
427
672
  if (surfaceClass === "derived_view" && ref.startsWith(".nimi/local/derived/")) {
428
673
  addError(errors, "derived_view_written_to_local_derived", ref);
429
674
  }
@@ -433,15 +678,9 @@ function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
433
678
  if (surfaceClass === "audit_evidence_state" && ref.startsWith(".nimi/spec/")) {
434
679
  addError(errors, "audit_evidence_state_under_spec", ref);
435
680
  }
436
- if (surfaceClass === "lifecycle_progress_state" && ref.startsWith(".nimi/spec/")) {
437
- addError(errors, "lifecycle_progress_state_under_spec", ref);
438
- }
439
681
  if (surfaceClass === "methodology_authority" && ref.startsWith(".nimi/spec/")) {
440
682
  addError(errors, "package_methodology_under_host_spec", ref);
441
683
  }
442
- if (surfaceClass === "candidate_roadmap" && ref.startsWith(".nimi/spec/")) {
443
- addError(errors, "candidate_roadmap_under_spec", ref);
444
- }
445
684
  if (isTrackedNonProductRef(ref) && !isAdmittedTrackedOutput(ref, trackedAdmissionRoots(contracts))) {
446
685
  addError(errors, "tracked_non_product_without_admission", ref);
447
686
  }
@@ -449,8 +688,8 @@ function validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts) {
449
688
  if (TEXT_RULE_BODY_PATTERN.test(text)) {
450
689
  addError(errors, "guidance_defines_rule_body", ref);
451
690
  }
452
- if (PRODUCT_RULE_ID_PATTERN.test(text) && !ref.endsWith("INDEX.md")) {
453
- addError(errors, "guidance_defines_or_restates_rule_id", ref);
691
+ if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
692
+ addError(errors, "guidance_defines_product_rule", ref);
454
693
  }
455
694
  }
456
695
  if (surfaceClass === "product_authority" && isMarkdownRef(ref) && GENERATED_REF_PATTERN.test(text)) {
@@ -483,6 +722,9 @@ function validateTableRef(ref, parsedYaml, contracts) {
483
722
  addError(errors, "missing_table_family_required_field", ref, String(field));
484
723
  }
485
724
  }
725
+ if (containsAnyKey(parsedYaml, familyContract.forbidden_fields ?? [])) {
726
+ addError(errors, "table_contains_family_forbidden_field", ref, family);
727
+ }
486
728
  }
487
729
 
488
730
  const forbiddenFields = contracts.tableFamily.data?.forbidden_fields_by_authority_class?.product_authority_table ?? [];
@@ -519,18 +761,20 @@ async function buildInventory(projectRoot, options = {}) {
519
761
  const rootRef = options.rootRef ?? ".nimi/spec";
520
762
  const absoluteFiles = await collectCandidateFiles(projectRoot, rootRef);
521
763
  const entries = [];
522
- const errors = [];
764
+ const layoutErrors = [...(contracts.hostSpecLayout?.errors ?? [])];
765
+ const errors = [...layoutErrors];
523
766
 
524
767
  for (const absolutePath of absoluteFiles) {
525
768
  const ref = relativeRef(projectRoot, absolutePath);
526
769
  const text = await readFile(absolutePath, "utf8");
527
770
  const parsedYaml = isYamlRef(ref) ? yamlForText(text) : null;
528
- const surfaceClass = classifyRef(ref, text, parsedYaml);
529
- const entryErrors = [
530
- ...validateRefPlacement(ref, surfaceClass, parsedYaml, text, contracts),
531
- ...validateTableRef(ref, parsedYaml, contracts),
532
- ...validateDomainRef(ref, contracts),
533
- ];
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();
534
778
  errors.push(...entryErrors);
535
779
  const requiredConfirmation = confirmationFor(entryErrors);
536
780
  entries.push({
@@ -544,6 +788,7 @@ async function buildInventory(projectRoot, options = {}) {
544
788
  ambiguity: ambiguityFor(requiredConfirmation, surfaceClass),
545
789
  evidence: entryErrors.length > 0 ? entryErrors : [`classified_as:${surfaceClass}`],
546
790
  validation_commands: validationCommandsFor(surfaceClass),
791
+ validation_errors: validationErrors,
547
792
  errors: entryErrors,
548
793
  });
549
794
  }
@@ -551,6 +796,7 @@ async function buildInventory(projectRoot, options = {}) {
551
796
  return {
552
797
  contracts,
553
798
  entries,
799
+ layoutErrors,
554
800
  errors,
555
801
  };
556
802
  }
@@ -602,39 +848,39 @@ function groupInventoryByDisposition(entries) {
602
848
  return Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, value.sort()]));
603
849
  }
604
850
 
605
- function buildMigrationExecutionPackets(entries) {
606
- const packetKinds = [
851
+ function buildMigrationGroups(entries) {
852
+ const groupKinds = [
607
853
  {
608
- packet_id: "move-package-methodology-copied-under-spec",
854
+ group_id: "move-package-methodology-copied-under-spec",
609
855
  dispositions: ["move_package"],
610
856
  requires_confirmation: false,
611
857
  },
612
858
  {
613
- packet_id: "move-local-derived-audit-state-and-lifecycle",
859
+ group_id: "move-local-derived-and-audit-state",
614
860
  dispositions: ["move_local"],
615
861
  requires_confirmation: false,
616
862
  },
617
863
  {
618
- packet_id: "rewrite-product-authority-tables-and-guidance",
864
+ group_id: "rewrite-product-authority-tables-and-guidance",
619
865
  dispositions: ["rewrite"],
620
866
  requires_confirmation: true,
621
867
  },
622
868
  {
623
- packet_id: "resolve-blocked-semantic-forks",
869
+ group_id: "resolve-blocked-semantic-forks",
624
870
  dispositions: ["block"],
625
871
  requires_confirmation: true,
626
872
  },
627
873
  ];
628
- return packetKinds
629
- .map((packet) => {
630
- const matching = entries.filter((entry) => packet.dispositions.includes(entry.disposition));
874
+ return groupKinds
875
+ .map((group) => {
876
+ const matching = entries.filter((entry) => group.dispositions.includes(entry.disposition));
631
877
  return {
632
- ...packet,
878
+ ...group,
633
879
  entry_count: matching.length,
634
880
  source_paths: matching.map((entry) => entry.source_path).sort(),
635
881
  };
636
882
  })
637
- .filter((packet) => packet.entry_count > 0);
883
+ .filter((group) => group.entry_count > 0);
638
884
  }
639
885
 
640
886
  function enumValidation(entries, inventory) {
@@ -655,7 +901,7 @@ function migrationPlanForInventory(rootRef, inventory) {
655
901
  const enumStatus = enumValidation(entries, inventory);
656
902
  const requiredConfirmationEntries = entries.filter((entry) => entry.required_confirmation !== "none");
657
903
  return {
658
- contract: "nimicoding.spec-migration-plan.v1",
904
+ contract: "nimicoding.spec-migration-plan.v2",
659
905
  ok: enumStatus.unknown_target_classes.length === 0 && enumStatus.unknown_dispositions.length === 0,
660
906
  version: inventory.version,
661
907
  root: rootRef,
@@ -687,7 +933,7 @@ function migrationPlanForInventory(rootRef, inventory) {
687
933
  : "resolve_owner_or_package_boundary_before_migration",
688
934
  })),
689
935
  groups: groupInventoryByDisposition(entries),
690
- execution_packets: buildMigrationExecutionPackets(entries),
936
+ migration_groups: buildMigrationGroups(entries),
691
937
  mutation_policy: {
692
938
  mutates_source_tree: false,
693
939
  allowed_output_roots: [".nimi/local/state/spec-surface"],
@@ -700,13 +946,15 @@ export async function classifySpecSurface(projectRoot, options = {}) {
700
946
  const { entries, errors } = await buildInventory(projectRoot, options);
701
947
  return reportFor("classify-spec-tree", errors.length === 0, errors, [], entries, {
702
948
  inventory: {
703
- version: 1,
949
+ version: 2,
704
950
  inventory: entries,
705
951
  disposition_enum: ["keep", "move_package", "move_local", "split", "rewrite", "delete", "block"],
706
952
  target_class_enum: [
707
953
  "product_authority",
708
954
  "product_authority_table",
709
955
  "thin_guidance",
956
+ "host_instruction",
957
+ "tracked_derived_projection",
710
958
  "derived_view",
711
959
  "spec_generation_state",
712
960
  "audit_evidence_state",
@@ -714,13 +962,11 @@ export async function classifySpecSurface(projectRoot, options = {}) {
714
962
  "methodology_authority",
715
963
  "nimicoding_managed_projection",
716
964
  "host_projection_anchor",
717
- "candidate_roadmap",
718
965
  "support_registry",
719
- "lifecycle_progress_state",
720
966
  ],
721
967
  semantic_constraints: [
722
968
  "inventory_must_not_modify_files",
723
- "future_under_spec_must_not_have_keep_disposition",
969
+ "future_under_spec_must_fail_classification",
724
970
  "nimicoding_managed_projection_must_not_be_promoted_to_product_authority",
725
971
  ],
726
972
  },
@@ -753,7 +999,11 @@ export function isProductAuthoritySurfaceClass(surfaceClass) {
753
999
  }
754
1000
 
755
1001
  export async function validatePlacement(projectRoot, options = {}) {
756
- const { entries, errors } = await buildInventory(projectRoot, options);
1002
+ const { entries, layoutErrors } = await buildInventory(projectRoot, options);
1003
+ const errors = [
1004
+ ...layoutErrors,
1005
+ ...entries.flatMap((entry) => entry.validation_errors?.placement ?? []),
1006
+ ];
757
1007
  return reportFor("validate-placement", errors.length === 0, errors, [], entries);
758
1008
  }
759
1009
 
@@ -804,8 +1054,8 @@ export async function validateGuidanceBodies(projectRoot, options = {}) {
804
1054
  if (TEXT_RULE_BODY_PATTERN.test(text)) {
805
1055
  addError(errors, "guidance_defines_rule_body", entry.source_path);
806
1056
  }
807
- if (PRODUCT_RULE_ID_PATTERN.test(text) && !entry.source_path.endsWith("INDEX.md")) {
808
- addError(errors, "guidance_defines_or_restates_rule_id", entry.source_path);
1057
+ if (PRODUCT_RULE_DEFINITION_PATTERN.test(text)) {
1058
+ addError(errors, "guidance_defines_product_rule", entry.source_path);
809
1059
  }
810
1060
  }
811
1061
  return reportFor("validate-guidance-bodies", errors.length === 0, errors, [], entries);