@nimiplatform/nimi-coding 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CODE_OF_CONDUCT.md +28 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/README.md +371 -344
  5. package/README.zh-CN.md +307 -0
  6. package/SECURITY.md +26 -0
  7. package/adapters/oh-my-codex/README.md +8 -9
  8. package/cli/commands/audit-sweep.mjs +10 -10
  9. package/cli/commands/classify-spec-tree.mjs +5 -0
  10. package/cli/commands/closeout.mjs +3 -0
  11. package/cli/commands/generate-spec-derived-docs.mjs +20 -0
  12. package/cli/commands/generate-spec-migration-plan.mjs +30 -0
  13. package/cli/commands/start.mjs +5 -1
  14. package/cli/commands/surface-validator-command.mjs +49 -0
  15. package/cli/commands/sweep-design.mjs +295 -0
  16. package/cli/commands/sweep.mjs +22 -0
  17. package/cli/commands/sync.mjs +132 -0
  18. package/cli/commands/topic-formatters.mjs +8 -8
  19. package/cli/commands/validate-ai-governance.mjs +167 -46
  20. package/cli/commands/validate-domain-admission.mjs +5 -0
  21. package/cli/commands/validate-guidance-bodies.mjs +5 -0
  22. package/cli/commands/validate-placement.mjs +5 -0
  23. package/cli/commands/validate-projection-edges.mjs +5 -0
  24. package/cli/commands/validate-spec-audit.mjs +5 -1
  25. package/cli/commands/validate-table-family.mjs +5 -0
  26. package/cli/commands/validate-tracked-output-admission.mjs +5 -0
  27. package/cli/constants.mjs +5 -49
  28. package/cli/help.mjs +33 -11
  29. package/cli/index.mjs +20 -2
  30. package/cli/lib/audit-sweep-runtime/admissions.mjs +38 -29
  31. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +8 -0
  32. package/cli/lib/audit-sweep-runtime/chunks.mjs +11 -11
  33. package/cli/lib/audit-sweep-runtime/closeout.mjs +8 -8
  34. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +3 -3
  35. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +10 -10
  36. package/cli/lib/audit-sweep-runtime/common.mjs +7 -7
  37. package/cli/lib/audit-sweep-runtime/format.mjs +3 -3
  38. package/cli/lib/audit-sweep-runtime/ingest.mjs +8 -8
  39. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +24 -27
  40. package/cli/lib/audit-sweep-runtime/inventory.mjs +58 -18
  41. package/cli/lib/audit-sweep-runtime/ledger.mjs +1 -1
  42. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +2 -2
  43. package/cli/lib/audit-sweep-runtime/remediation.mjs +6 -6
  44. package/cli/lib/audit-sweep-runtime/rerun.mjs +6 -6
  45. package/cli/lib/audit-sweep-runtime/status.mjs +1 -1
  46. package/cli/lib/audit-sweep-runtime/validators.mjs +2 -2
  47. package/cli/lib/authority-convergence.mjs +397 -2
  48. package/cli/lib/blueprint-audit.mjs +5 -5
  49. package/cli/lib/closeout.mjs +126 -3
  50. package/cli/lib/contracts.mjs +21 -17
  51. package/cli/lib/handoff.mjs +29 -11
  52. package/cli/lib/high-risk-admission.mjs +60 -11
  53. package/cli/lib/high-risk-decision.mjs +31 -2
  54. package/cli/lib/high-risk-ingest.mjs +5 -1
  55. package/cli/lib/high-risk-review.mjs +5 -1
  56. package/cli/lib/internal/contracts-parse.mjs +195 -24
  57. package/cli/lib/internal/contracts-validators.mjs +3 -2
  58. package/cli/lib/internal/doctor-bootstrap-surface.mjs +82 -35
  59. package/cli/lib/internal/doctor-delegated-surface.mjs +1 -1
  60. package/cli/lib/internal/doctor-finalize.mjs +12 -8
  61. package/cli/lib/internal/doctor-inspectors.mjs +34 -1
  62. package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +74 -12
  63. package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +24 -6
  64. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +18 -23
  65. package/cli/lib/internal/surface-taxonomy-validators.mjs +931 -0
  66. package/cli/lib/internal/validators-spec.mjs +229 -20
  67. package/cli/lib/sweep-design-runtime/common.mjs +246 -0
  68. package/cli/lib/sweep-design-runtime/engine.mjs +733 -0
  69. package/cli/lib/sweep-design-runtime/fix-topic.mjs +414 -0
  70. package/cli/lib/sweep-design-runtime/lifecycle.mjs +54 -0
  71. package/cli/lib/sweep-design-runtime/results.mjs +324 -0
  72. package/cli/lib/sweep-design.mjs +8 -0
  73. package/cli/lib/sync.mjs +143 -0
  74. package/cli/lib/topic-artifacts.mjs +186 -0
  75. package/cli/lib/topic-authority-coverage.mjs +73 -0
  76. package/cli/lib/topic-closeout.mjs +560 -0
  77. package/cli/lib/topic-common.mjs +404 -0
  78. package/cli/lib/topic-decisions.mjs +332 -0
  79. package/cli/lib/topic-draft-packets.mjs +126 -7
  80. package/cli/lib/topic-execution.mjs +515 -0
  81. package/cli/lib/topic-goal.mjs +112 -33
  82. package/cli/lib/topic-ledger.mjs +281 -0
  83. package/cli/lib/topic-lifecycle-artifacts.mjs +173 -0
  84. package/cli/lib/topic-root-validation.mjs +288 -0
  85. package/cli/lib/topic-runner-commands.mjs +174 -0
  86. package/cli/lib/topic-runner-deferral.mjs +532 -0
  87. package/cli/lib/topic-runner-stale-gates.mjs +114 -0
  88. package/cli/lib/topic-runner-validation.mjs +138 -0
  89. package/cli/lib/topic-runner.mjs +109 -154
  90. package/cli/lib/topic-scaffold.mjs +252 -0
  91. package/cli/lib/topic-waves.mjs +403 -0
  92. package/cli/lib/topic.mjs +81 -93
  93. package/cli/lib/value-helpers.mjs +6 -1
  94. package/cli/seeds/bootstrap.mjs +96 -20
  95. package/cli/seeds/seed-policy.yaml +67 -0
  96. package/config/bootstrap.yaml +1 -1
  97. package/config/skill-manifest.yaml +4 -2
  98. package/config/spec-generation-inputs.yaml +41 -19
  99. package/contracts/audit-remediation-map.schema.yaml +1 -0
  100. package/contracts/audit-sweep-result.yaml +4 -0
  101. package/contracts/domain-admission.schema.yaml +56 -0
  102. package/contracts/migration-inventory.schema.yaml +80 -0
  103. package/contracts/negative-fixtures.yaml +91 -0
  104. package/contracts/placement-contract.schema.yaml +163 -0
  105. package/contracts/projection-edge.schema.yaml +130 -0
  106. package/contracts/shared-enums.yaml +68 -0
  107. package/contracts/spec-generation-audit.schema.yaml +19 -4
  108. package/contracts/spec-generation-inputs.schema.yaml +130 -29
  109. package/contracts/spec-reconstruction-result.yaml +9 -5
  110. package/contracts/surface-taxonomy.schema.yaml +201 -0
  111. package/contracts/sweep-design-result.yaml +349 -0
  112. package/contracts/table-family.schema.yaml +121 -0
  113. package/contracts/topic-goal.schema.yaml +10 -1
  114. package/contracts/tracked-output-admission.schema.yaml +70 -0
  115. package/contracts/workflow-consumer.schema.yaml +112 -0
  116. package/methodology/audit-sweep-p0p1-recall.yaml +1 -1
  117. package/methodology/spec-reconstruction.yaml +53 -30
  118. package/package.json +19 -4
  119. package/spec/_meta/command-gating-matrix.yaml +33 -0
  120. package/spec/_meta/generate-drift-migration-checklist.yaml +44 -62
  121. package/spec/_meta/governance-routing-cutover-checklist.yaml +3 -3
  122. package/spec/_meta/phase2-impacted-surface-matrix.yaml +14 -14
  123. package/spec/_meta/spec-authority-cutover-readiness.yaml +3 -5
  124. package/spec/_meta/spec-tree-model.yaml +104 -36
  125. package/spec/bootstrap-state.yaml +36 -36
  126. package/spec/product-scope.yaml +13 -10
@@ -39,6 +39,7 @@ const PACKAGE_REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
39
39
 
40
40
  export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegatedSurface) {
41
41
  const checks = [...bootstrapSurface.checks, ...delegatedSurface.checks];
42
+ const usesV2SurfaceModel = bootstrapSurface.specGenerationInputs?.mode === "class_filtered";
42
43
 
43
44
  const specContract = await loadSpecReconstructionContract(projectRoot);
44
45
  const auditContract = await loadDocSpecAuditContract(projectRoot);
@@ -170,7 +171,7 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
170
171
  ),
171
172
  );
172
173
 
173
- const lifecycleAligned = (
174
+ const lifecycleAligned = usesV2SurfaceModel || (
174
175
  bootstrapSurface.bootstrapStateContract.treeState === "bootstrap_only"
175
176
  && bootstrapSurface.canonicalTree.ready === false
176
177
  ) || (
@@ -187,7 +188,9 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
187
188
  buildCheck(
188
189
  "bootstrap_lifecycle_alignment",
189
190
  lifecycleAligned,
190
- lifecycleAligned
191
+ usesV2SurfaceModel
192
+ ? "v2 host-local surface model does not require bootstrap-state lifecycle alignment"
193
+ : lifecycleAligned
191
194
  ? `bootstrap-state lifecycle ${bootstrapSurface.bootstrapStateContract.treeState ?? "unknown"} is aligned with the current canonical tree readiness`
192
195
  : bootstrapSurface.bootstrapStateContract.treeState === "canonical_tree_ready"
193
196
  ? "bootstrap-state declares canonical_tree_ready but required canonical files are still missing"
@@ -203,7 +206,8 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
203
206
  detail: auditArtifact.reason,
204
207
  });
205
208
 
206
- const auditArtifactConsistent = !auditArtifact.present
209
+ const auditArtifactConsistent = usesV2SurfaceModel
210
+ || !auditArtifact.present
207
211
  || auditArtifact.outcome !== "completed"
208
212
  || bootstrapSurface.canonicalTree.requiredFilesValid;
209
213
  checks.push(
@@ -268,7 +272,7 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
268
272
  && delegatedSurface.contractRuntimeOwnersAligned
269
273
  && delegatedSurface.delegatedModeAligned
270
274
  && delegatedSurface.selfHostedAligned
271
- && bootstrapSurface.completionTruth.ok
275
+ && (usesV2SurfaceModel || bootstrapSurface.completionTruth.ok)
272
276
  && delegatedSurface.resultContractAlignment
273
277
  && delegatedSurface.adapterSelectionValid
274
278
  && adapterProfilesValid
@@ -281,12 +285,12 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
281
285
  && highRiskAdmissionsTruthValid
282
286
  && externalExecutionArtifacts.ok
283
287
  && packageBoundaryTruthOk
284
- && bootstrapSurface.specTreeModel.ok
288
+ && (usesV2SurfaceModel || bootstrapSurface.specTreeModel.ok)
285
289
  && bootstrapSurface.specGenerationInputsContract.ok
286
290
  && bootstrapSurface.specGenerationAuditContract.ok
287
291
  && bootstrapSurface.specGenerationInputs.ok
288
292
  && (!bootstrapSurface.canonicalTree.requiredFilesValid || bootstrapSurface.specGenerationAudit.ok)
289
- && bootstrapSurface.commandGatingMatrix.ok
293
+ && (usesV2SurfaceModel || bootstrapSurface.commandGatingMatrix.ok)
290
294
  && bootstrapSurface.blueprintReferenceAligned
291
295
  && lifecycleAligned,
292
296
  requiredContextOrder: delegatedSurface.handoffRequiredContext,
@@ -304,7 +308,7 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
304
308
  nextSteps.push("Run `nimicoding blueprint-audit --write-local` after canonical tree generation when a benchmark blueprint is declared.");
305
309
  }
306
310
  if (bootstrapSurface.canonicalTree.requiredFilesValid && !bootstrapSurface.specGenerationAudit.ok) {
307
- nextSteps.push("Run `nimicoding validate-spec-audit` after generating `.nimi/spec/_meta/spec-generation-audit.yaml` for the canonical tree.");
311
+ nextSteps.push("Run `nimicoding validate-spec-audit` after generating the local spec generation audit for the canonical tree.");
308
312
  }
309
313
  if (!auditArtifact.present && bootstrapSurface.canonicalTree.requiredFilesValid) {
310
314
  nextSteps.push("Run `nimicoding handoff --skill doc_spec_audit` and close out the result locally when the audit is complete.");
@@ -328,7 +332,7 @@ export async function finalizeDoctorState(projectRoot, bootstrapSurface, delegat
328
332
  })),
329
333
  };
330
334
 
331
- const completionStatus = !bootstrapSurface.completionTruth.ok || !packageBoundaryTruthOk
335
+ const completionStatus = (!usesV2SurfaceModel && !bootstrapSurface.completionTruth.ok) || !packageBoundaryTruthOk
332
336
  ? STANDALONE_COMPLETION_STATUS.DRIFTED
333
337
  : !hasErrors
334
338
  ? STANDALONE_COMPLETION_STATUS.COMPLETE
@@ -265,7 +265,40 @@ export function inspectBootstrapStateContract(bootstrapStateText) {
265
265
 
266
266
  export async function inspectCanonicalTree(projectRoot, specTreeModel) {
267
267
  if (!specTreeModel.ok) {
268
- return emptyCanonicalTree();
268
+ const reconstructionText = await readTextIfFile(path.join(projectRoot, ".nimi", "methodology", "spec-reconstruction.yaml"));
269
+ const reconstruction = parseYamlText(reconstructionText);
270
+ const requiredFiles = Array.isArray(reconstruction?.reconstruction?.target_tree_shape?.minimal_required_outputs)
271
+ ? reconstruction.reconstruction.target_tree_shape.minimal_required_outputs.map(String)
272
+ : [];
273
+ const canonicalRoot = typeof reconstruction?.reconstruction?.target_root === "string"
274
+ ? reconstruction.reconstruction.target_root
275
+ : ".nimi/spec";
276
+ const present = [];
277
+ const missing = [];
278
+
279
+ for (const relativePath of requiredFiles) {
280
+ const info = await pathExists(path.join(projectRoot, relativePath));
281
+ if (info && info.isFile()) {
282
+ present.push(relativePath);
283
+ } else {
284
+ missing.push(relativePath);
285
+ }
286
+ }
287
+
288
+ if (requiredFiles.length === 0) {
289
+ return emptyCanonicalTree();
290
+ }
291
+
292
+ return {
293
+ profile: "surface_taxonomy_v1",
294
+ canonicalRoot,
295
+ requiredFiles,
296
+ present,
297
+ missing,
298
+ invalid: [],
299
+ requiredFilesValid: missing.length === 0,
300
+ ready: missing.length === 0,
301
+ };
269
302
  }
270
303
 
271
304
  const requiredFiles = specTreeModel.requiredFilesByProfile[specTreeModel.profile] ?? [];
@@ -4,6 +4,9 @@ import path from 'node:path';
4
4
  import { execSync } from 'node:child_process';
5
5
  import YAML from 'yaml';
6
6
 
7
+ const GOVERNANCE_CONFIG_RELATIVE_PATH = '.nimi/config/governance.yaml';
8
+ const GOVERNANCE_CONTEXT_BUDGET_SECTION = 'ai_governance.context_budget';
9
+
7
10
  function escapeRegex(input) {
8
11
  return input.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
9
12
  }
@@ -89,21 +92,50 @@ function parseDateMaybe(input) {
89
92
  return parsed;
90
93
  }
91
94
 
92
- function countLines(buffer) {
95
+ function measureLineShape(buffer) {
93
96
  if (!buffer || buffer.length === 0) {
94
- return 0;
97
+ return {
98
+ lines: 0,
99
+ maxLineBytes: 0,
100
+ averageLineBytes: 0,
101
+ };
95
102
  }
103
+
96
104
  let lines = 1;
105
+ let currentLineBytes = 0;
106
+ let maxLineBytes = 0;
107
+
97
108
  for (const byte of buffer) {
98
109
  if (byte === 10) {
110
+ maxLineBytes = Math.max(maxLineBytes, currentLineBytes);
111
+ currentLineBytes = 0;
99
112
  lines += 1;
113
+ continue;
114
+ }
115
+ currentLineBytes += 1;
116
+ }
117
+ maxLineBytes = Math.max(maxLineBytes, currentLineBytes);
118
+
119
+ return {
120
+ lines,
121
+ maxLineBytes,
122
+ averageLineBytes: buffer.byteLength / lines,
123
+ };
124
+ }
125
+
126
+ function getPathSection(source, sectionPath) {
127
+ let current = source;
128
+ for (const segment of sectionPath.split('.')) {
129
+ if (!current || typeof current !== 'object') {
130
+ return null;
100
131
  }
132
+ current = current[segment];
101
133
  }
102
- return lines;
134
+ return current && typeof current === 'object' ? current : null;
103
135
  }
104
136
 
105
- function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel) {
106
- const parsed = inlineConfig ?? (() => {
137
+ function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel, configSection) {
138
+ const rawParsed = inlineConfig ?? (() => {
107
139
  const configPath = path.join(cwd, relativePath);
108
140
  if (!fs.existsSync(configPath)) {
109
141
  throw new Error(`budget config not found: ${relativePath}`);
@@ -111,18 +143,19 @@ function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel) {
111
143
  const raw = fs.readFileSync(configPath, 'utf8');
112
144
  return YAML.parse(raw);
113
145
  })();
146
+ const parsed = configSection ? getPathSection(rawParsed, configSection) : rawParsed;
114
147
  if (!parsed || typeof parsed !== 'object') {
115
- throw new Error(`invalid budget config format: ${relativePath}`);
148
+ throw new Error(`invalid budget config format: ${relativePath}${configSection ? `#${configSection}` : ''}`);
116
149
  }
117
150
  if (!parsed.profiles || typeof parsed.profiles !== 'object') {
118
- throw new Error(`budget config missing profiles: ${relativePath}`);
151
+ throw new Error(`budget config missing profiles: ${relativePath}${configSection ? `#${configSection}` : ''}`);
119
152
  }
120
153
  const defaultProfile = String(parsed.default_profile || 'production');
121
154
  if (!parsed.profiles[defaultProfile]) {
122
155
  throw new Error(`default profile not found in profiles: ${defaultProfile}`);
123
156
  }
124
157
  return {
125
- configPath: configPathLabel || relativePath,
158
+ configPath: configPathLabel || (configSection ? `${relativePath}#${configSection}` : relativePath),
126
159
  parsed,
127
160
  defaultProfile,
128
161
  };
@@ -174,12 +207,15 @@ function waiverAllowedForProfile(profileId) {
174
207
 
175
208
  export function evaluateAiContextBudget(options = {}) {
176
209
  const cwd = options.cwd || process.cwd();
177
- const configRelativePath = options.configRelativePath || 'config/ai/ai-context-budget.yaml';
210
+ const configRelativePath = options.configRelativePath || GOVERNANCE_CONFIG_RELATIVE_PATH;
211
+ const configSection = options.configSection
212
+ ?? (options.config || options.configRelativePath ? null : GOVERNANCE_CONTEXT_BUDGET_SECTION);
178
213
  const { parsed, defaultProfile, configPath } = loadBudgetConfig(
179
214
  cwd,
180
215
  configRelativePath,
181
216
  options.config || null,
182
217
  options.configPathLabel || null,
218
+ configSection,
183
219
  );
184
220
 
185
221
  const excludeMatchers = compileMatchers(parsed.exclude || []);
@@ -247,12 +283,27 @@ export function evaluateAiContextBudget(options = {}) {
247
283
  }
248
284
 
249
285
  const buffer = fs.readFileSync(absolutePath);
250
- const lines = countLines(buffer);
286
+ const { lines, maxLineBytes, averageLineBytes } = measureLineShape(buffer);
251
287
  const bytes = buffer.byteLength;
252
288
 
253
289
  const linesSeverity = toSeverity(lines, profile.warning_lines, profile.error_lines);
254
290
  const bytesSeverity = toSeverity(bytes, profile.warning_bytes, profile.error_bytes);
255
- const severity = maxSeverity(linesSeverity, bytesSeverity);
291
+ const maxLineBytesSeverity = toSeverity(
292
+ maxLineBytes,
293
+ profile.warning_max_line_bytes,
294
+ profile.error_max_line_bytes,
295
+ );
296
+ const averageLineBytesSeverity = toSeverity(
297
+ averageLineBytes,
298
+ profile.warning_average_line_bytes,
299
+ profile.error_average_line_bytes,
300
+ );
301
+ const severity = [
302
+ linesSeverity,
303
+ bytesSeverity,
304
+ maxLineBytesSeverity,
305
+ averageLineBytesSeverity,
306
+ ].reduce((current, next) => maxSeverity(current, next), "none");
256
307
 
257
308
  const waiver = waiverMap.get(relativePath) || null;
258
309
  const waiverExpired = waiver?.until ? waiver.until.getTime() < Date.now() : false;
@@ -263,13 +314,21 @@ export function evaluateAiContextBudget(options = {}) {
263
314
  profile: profileId,
264
315
  lines,
265
316
  bytes,
317
+ maxLineBytes,
318
+ averageLineBytes,
266
319
  severity,
267
320
  linesSeverity,
268
321
  bytesSeverity,
322
+ maxLineBytesSeverity,
323
+ averageLineBytesSeverity,
269
324
  warningLines: profile.warning_lines,
270
325
  errorLines: profile.error_lines,
271
326
  warningBytes: profile.warning_bytes,
272
327
  errorBytes: profile.error_bytes,
328
+ warningMaxLineBytes: profile.warning_max_line_bytes,
329
+ errorMaxLineBytes: profile.error_max_line_bytes,
330
+ warningAverageLineBytes: profile.warning_average_line_bytes,
331
+ errorAverageLineBytes: profile.error_average_line_bytes,
273
332
  waiver,
274
333
  waiverExpired,
275
334
  waived,
@@ -285,7 +344,10 @@ export function evaluateAiContextBudget(options = {}) {
285
344
  if (right.lines !== left.lines) {
286
345
  return right.lines - left.lines;
287
346
  }
288
- return right.bytes - left.bytes;
347
+ if (right.bytes !== left.bytes) {
348
+ return right.bytes - left.bytes;
349
+ }
350
+ return right.maxLineBytes - left.maxLineBytes;
289
351
  });
290
352
 
291
353
  return {
@@ -4,6 +4,9 @@ import path from 'node:path';
4
4
  import { execSync } from 'node:child_process';
5
5
  import YAML from 'yaml';
6
6
 
7
+ const GOVERNANCE_CONFIG_RELATIVE_PATH = '.nimi/config/governance.yaml';
8
+ const GOVERNANCE_STRUCTURE_BUDGET_SECTION = 'ai_governance.structure_budget';
9
+
7
10
  function escapeRegex(input) {
8
11
  return input.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
9
12
  }
@@ -77,8 +80,19 @@ function parseDateMaybe(input) {
77
80
  return parsed;
78
81
  }
79
82
 
80
- function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel) {
81
- const parsed = inlineConfig ?? (() => {
83
+ function getPathSection(source, sectionPath) {
84
+ let current = source;
85
+ for (const segment of sectionPath.split('.')) {
86
+ if (!current || typeof current !== 'object') {
87
+ return null;
88
+ }
89
+ current = current[segment];
90
+ }
91
+ return current && typeof current === 'object' ? current : null;
92
+ }
93
+
94
+ function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel, configSection) {
95
+ const rawParsed = inlineConfig ?? (() => {
82
96
  const configPath = path.join(cwd, relativePath);
83
97
  if (!fs.existsSync(configPath)) {
84
98
  throw new Error(`budget config not found: ${relativePath}`);
@@ -86,14 +100,15 @@ function loadBudgetConfig(cwd, relativePath, inlineConfig, configPathLabel) {
86
100
  const raw = fs.readFileSync(configPath, 'utf8');
87
101
  return YAML.parse(raw);
88
102
  })();
103
+ const parsed = configSection ? getPathSection(rawParsed, configSection) : rawParsed;
89
104
  if (!parsed || typeof parsed !== 'object') {
90
- throw new Error(`invalid budget config format: ${relativePath}`);
105
+ throw new Error(`invalid budget config format: ${relativePath}${configSection ? `#${configSection}` : ''}`);
91
106
  }
92
107
  if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
93
- throw new Error(`budget config missing rules: ${relativePath}`);
108
+ throw new Error(`budget config missing rules: ${relativePath}${configSection ? `#${configSection}` : ''}`);
94
109
  }
95
110
  return {
96
- configPath: configPathLabel || relativePath,
111
+ configPath: configPathLabel || (configSection ? `${relativePath}#${configSection}` : relativePath),
97
112
  parsed,
98
113
  };
99
114
  }
@@ -244,12 +259,15 @@ function compareRows(left, right) {
244
259
 
245
260
  export function evaluateAiStructureBudget(options = {}) {
246
261
  const cwd = options.cwd || process.cwd();
247
- const configRelativePath = options.configRelativePath || 'config/ai/ai-structure-budget.yaml';
262
+ const configRelativePath = options.configRelativePath || GOVERNANCE_CONFIG_RELATIVE_PATH;
263
+ const configSection = options.configSection
264
+ ?? (options.config || options.configRelativePath ? null : GOVERNANCE_STRUCTURE_BUDGET_SECTION);
248
265
  const { parsed, configPath } = loadBudgetConfig(
249
266
  cwd,
250
267
  configRelativePath,
251
268
  options.config || null,
252
269
  options.configPathLabel || null,
270
+ configSection,
253
271
  );
254
272
  const excludeMatchers = compileMatchers(parsed.exclude || []);
255
273
  const compiledRules = (parsed.rules || []).map((rule) => ({
@@ -2,12 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
4
  const DEFAULT_TARGETS = [
5
- { rel: "AGENTS.md", maxLines: 100 },
6
- { rel: "nimi-backend/AGENTS.md", maxLines: 80 },
7
- { rel: "nimi-dashboard/AGENTS.md", maxLines: 65 },
8
- { rel: ".nimi/spec/AGENTS.md", maxLines: 70 },
9
- { rel: "scripts/AGENTS.md", maxLines: 45 },
10
- { rel: "tests/AGENTS.md", maxLines: 35 },
5
+ { rel: "AGENTS.md", maxLines: 120 },
11
6
  ];
12
7
 
13
8
  const DEFAULT_REQUIRED_SECTIONS = [
@@ -17,7 +12,7 @@ const DEFAULT_REQUIRED_SECTIONS = [
17
12
  "## Verification Commands",
18
13
  ];
19
14
 
20
- const DEFAULT_STALE_TOKENS = ["AISC-", "R-WORLD-", "R-AGENT-", "SOCIAL-013", "ECON-014"];
15
+ const DEFAULT_STALE_TOKENS = [];
21
16
 
22
17
  const GENERIC_PNPM_COMMANDS = new Set([
23
18
  "install",
@@ -39,18 +34,7 @@ function readJson(filePath) {
39
34
 
40
35
  function collectKnownPnpmScripts(projectRoot) {
41
36
  const rootPkg = readJson(path.join(projectRoot, "package.json"));
42
- const scripts = new Set(Object.keys(rootPkg.scripts || {}));
43
-
44
- for (const rel of ["nimi-backend/package.json", "nimi-dashboard/package.json"]) {
45
- const abs = path.join(projectRoot, rel);
46
- if (!fs.existsSync(abs)) continue;
47
- const pkg = readJson(abs);
48
- for (const name of Object.keys(pkg.scripts || {})) {
49
- scripts.add(name);
50
- }
51
- }
52
-
53
- return scripts;
37
+ return new Set(Object.keys(rootPkg.scripts || {}));
54
38
  }
55
39
 
56
40
  function validatePnpmCommand(command, knownScripts, failures, relPath) {
@@ -93,7 +77,7 @@ function validatePathToken(token, failures, relPath, agentsDir, projectRoot) {
93
77
  }
94
78
  }
95
79
 
96
- export function runAgentsFreshnessCheck(options = {}) {
80
+ export function evaluateAgentsFreshnessCheck(options = {}) {
97
81
  const projectRoot = options.projectRoot || process.cwd();
98
82
  const config = options.config || {};
99
83
  const targets = Array.isArray(config.targets) && config.targets.length > 0
@@ -143,13 +127,24 @@ export function runAgentsFreshnessCheck(options = {}) {
143
127
  }
144
128
  }
145
129
 
146
- if (failures.length > 0) {
130
+ return {
131
+ targets,
132
+ requiredSections,
133
+ staleTokens,
134
+ failures,
135
+ };
136
+ }
137
+
138
+ export function runAgentsFreshnessCheck(options = {}) {
139
+ const report = evaluateAgentsFreshnessCheck(options);
140
+
141
+ if (report.failures.length > 0) {
147
142
  process.stderr.write(
148
- `agents freshness check failed:\n${failures.map((entry) => `- ${entry}`).join("\n")}\n`,
143
+ `agents freshness check failed:\n${report.failures.map((entry) => `- ${entry}`).join("\n")}\n`,
149
144
  );
150
145
  return 1;
151
146
  }
152
147
 
153
- process.stdout.write(`agents freshness check passed (${targets.length} files)\n`);
148
+ process.stdout.write(`agents freshness check passed (${report.targets.length} files)\n`);
154
149
  return 0;
155
150
  }