@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -16,13 +16,15 @@ import { load as loadYaml } from 'js-yaml';
16
16
  import { validateDecisionDocument } from '../decision/validate.js';
17
17
  import { parseDecisionJson, parseDecisionYaml } from '../decision/entry.js';
18
18
  import { validateGateDocument } from '../gates/schema.js';
19
- import { DEFAULT_INDEX_EXTENSIONS, INDEX_EXTRACTOR_VERSION, INDEX_VERSION, INDEX_DIR, OPERATIONAL_DISCOVERY_TYPES, PHASE_DIRECTORIES, GRAPH_CONFIGS, TEMPLATE_INDEX_EXTENSIONS, resolveGraphScanDir, loadUserGraphConfigs, loadGlobalGraphConfigs, } from './types.js';
19
+ import { INDEX_EXTRACTOR_VERSION, INDEX_VERSION, INDEX_DIR, OPERATIONAL_DISCOVERY_TYPES, PHASE_DIRECTORIES, GRAPH_CONFIGS, TEMPLATE_INDEX_EXTENSIONS, resolveGraphScanDir, loadUserGraphConfigs, loadGlobalGraphConfigs, resolveGraphBackendType, } from './types.js';
20
20
  import { parseCitationSidecar, citationResultToEdges, buildRefToPathMap } from './citation-parser.js';
21
21
  import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
22
22
  import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-manifest.js';
23
+ import { FRAMEWORK_RELATIONS_REVISION } from './framework-source-digest.js';
23
24
  import { normalizeOperationalState } from './operational-state.js';
24
25
  import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
25
26
  import { normalizeStateTransferProjection } from './state-transfer.js';
27
+ import { resolveIndexWorkers, runIndexPreparation } from './index-preparation.js';
26
28
  import { collectGraphIndexFiles, findArtifactFiles, indexPathFor } from './index-files.js';
27
29
  function absoluteEntryPath(cwd, entryPath, graph) {
28
30
  if ((!graph || graph === 'project') && entryPath.startsWith(`${DEFAULT_PROJECT_AIWG_DIR}/`)) {
@@ -56,6 +58,30 @@ export function parseFrontmatter(content) {
56
58
  return { data: {}, body: content };
57
59
  }
58
60
  }
61
+ /** Read the target rule schema tolerantly while older rules remain untagged. */
62
+ export function extractRuleBindings(data, body = '') {
63
+ const bound_to = {};
64
+ const raw = data.bound_to;
65
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
66
+ for (const key of ['skills', 'agents', 'flows']) {
67
+ const targets = raw[key];
68
+ if (Array.isArray(targets)) {
69
+ const names = [...new Set(targets.filter((name) => typeof name === 'string' && name.trim().length > 0))];
70
+ if (names.length)
71
+ bound_to[key] = names;
72
+ }
73
+ }
74
+ }
75
+ const enforcement = typeof data.enforcement === 'string' ? data.enforcement
76
+ : body.match(/^\*\*Enforcement Level\*\*:\s*([A-Za-z]+)/m)?.[1];
77
+ return {
78
+ ...(enforcement ? { ruleEnforcement: enforcement.trim().toLowerCase() } : {}),
79
+ ...(Array.isArray(data.paths) ? { rulePaths: data.paths.filter((value) => typeof value === 'string') } : {}),
80
+ ...(typeof data.activation === 'string' ? { activation: data.activation } : {}),
81
+ ...(typeof data.summary === 'string' ? { ruleSummary: data.summary } : {}),
82
+ ...(Object.keys(bound_to).length ? { bound_to } : {}),
83
+ };
84
+ }
59
85
  /**
60
86
  * Extract @-mention references from content
61
87
  */
@@ -810,29 +836,100 @@ function resolveInstallRootHint() {
810
836
  catch { /* fall through */ }
811
837
  return '<aiwg install root>';
812
838
  }
839
+ /** Shared serial/worker read, checksum and extraction. No index output or global reduction. */
840
+ export function prepareIndexArtifact(job) {
841
+ const { fullPath, relativePath } = job;
842
+ const content = fs.readFileSync(fullPath, 'utf8');
843
+ const checksum = computeChecksum(content);
844
+ if (job.existingChecksum === checksum)
845
+ return { checksum };
846
+ const { data, body } = parseFrontmatter(content);
847
+ // YAML Flow documents (workflow.aiwg.io/v1) are pure YAML, not
848
+ // markdown+frontmatter — detect them up front so they classify and
849
+ // become discoverable (#1540).
850
+ const flow = parseFlowDoc(content, relativePath);
851
+ const decision = flow ? null : parseDecisionDoc(content, relativePath);
852
+ const gatePack = flow || decision ? null : parseGatePackDoc(content, relativePath);
853
+ const inferredType = inferType(data, relativePath);
854
+ const physicalType = inferType({ ...data, type: undefined }, relativePath);
855
+ const runbook = flow || decision || gatePack ? null : parseRunbookDoc(data, body, relativePath);
856
+ const schemaDoc = inferredType === 'schema' ? parseSchemaDoc(content, relativePath) : null;
857
+ const title = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.title ?? extractTitle(data, body);
858
+ const phase = typeof data.phase === 'string' ? data.phase : inferPhase(relativePath);
859
+ const type = flow?.type ?? decision?.type ?? gatePack?.type ?? (runbook ? 'runbook' : inferredType);
860
+ const tags = flow?.tags ?? decision?.tags ?? gatePack?.tags ?? (Array.isArray(data.tags) ? data.tags.map(String) : []);
861
+ const summary = flow?.description ?? decision?.description ?? gatePack?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
862
+ const dependencies = extractMentions(content);
863
+ const markdownLinks = extractMarkdownLinks(content);
864
+ // Discovery metadata (#1214, #1540, #1792) — meaningful for operational
865
+ // AIWG artifact kinds. Kept undefined on document types so the index file
866
+ // stays small for the common case.
867
+ const isDiscoverable = OPERATIONAL_DISCOVERY_TYPES.includes(type);
868
+ // Declarative processes have no trigger phrases — they rely on their
869
+ // capability and structure-aware search terms.
870
+ const triggers = isDiscoverable && !flow ? extractTriggers(body, data) : undefined;
871
+ const capability = flow?.capability ?? decision?.capability ?? gatePack?.capability ?? schemaDoc?.capability ?? runbook?.capability ?? (isDiscoverable ? extractCapability(data, body) : undefined);
872
+ const kind = flow?.kind ?? decision?.kind ?? gatePack?.kind ?? runbook?.kind;
873
+ const sourceType = runbook && physicalType !== 'runbook' ? physicalType : undefined;
874
+ const searchTerms = flow?.searchTerms ?? decision?.searchTerms ?? gatePack?.searchTerms ?? schemaDoc?.searchTerms ?? runbook?.searchTerms;
875
+ const kernel = data.kernel === true || data.kernel === 'true' ? true : undefined;
876
+ // Script entrypoint metadata is meaningful for skills only (#1227).
877
+ const script = type === 'skill' ? extractSkillScript(data) : undefined;
878
+ const operationalState = normalizeOperationalState(data.operational_state);
879
+ const stateTransfer = normalizeStateTransferProjection(data.state_transfer);
880
+ // Canonical short name (#1233) — used by the scorer to floor exact-name
881
+ // queries to 1.0 so hyphenated kernel-skill names like `aiwg-doctor`
882
+ // remain searchable even when the rendered title strips the hyphen.
883
+ const name = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.name ?? (isDiscoverable ? extractCanonicalName(data, relativePath) : undefined);
884
+ const entry = {
885
+ path: relativePath,
886
+ type,
887
+ ...(kind ? { kind } : {}),
888
+ ...(sourceType ? { sourceType } : {}),
889
+ phase,
890
+ title,
891
+ tags,
892
+ created: typeof data.created === 'string' ? data.created : new Date(job.birthtimeMs).toISOString(),
893
+ updated: new Date(job.mtimeMs).toISOString(),
894
+ checksum,
895
+ summary,
896
+ dependencies,
897
+ ...(markdownLinks.length > 0 ? { markdownLinks } : {}),
898
+ dependents: [], // Computed after all entries are processed
899
+ ...(name ? { name } : {}),
900
+ ...(triggers && triggers.length > 0 ? { triggers } : {}),
901
+ ...(capability ? { capability } : {}),
902
+ ...(searchTerms && searchTerms.length > 0 ? { searchTerms } : {}),
903
+ ...(kernel ? { kernel } : {}),
904
+ ...(script ? { script } : {}),
905
+ ...(type === 'rule' || type === 'behavior' ? extractRuleBindings(data, body) :
906
+ Array.isArray(data.paths) ? { governedPaths: data.paths.filter((value) => typeof value === 'string') } : {}),
907
+ ...(operationalState ? { operationalState } : {}),
908
+ ...(stateTransfer ? { stateTransfer } : {}),
909
+ };
910
+ return { checksum, entry };
911
+ }
813
912
  export async function buildIndex(cwd, options = {}) {
814
- const { force = false, verbose = false, scope, outputDir, graph, explicit = true } = options;
913
+ const { force = false, verbose = false, scope, includeIgnored = false, outputDir, graph, explicit = true } = options;
914
+ const workers = resolveIndexWorkers(options.workers);
815
915
  const startTime = Date.now();
916
+ const log = options.quiet ? (..._args) => { } : console.log;
816
917
  // Ensure user-defined graphs are loaded
817
918
  loadUserGraphConfigs(cwd);
818
919
  loadGlobalGraphConfigs();
819
920
  // Determine scan directories based on graph type
820
921
  const graphConfig = graph ? GRAPH_CONFIGS[graph] : undefined;
821
922
  let scanDirs;
822
- let fileExtensions;
823
923
  if (scope) {
824
924
  // Explicit scope overrides graph config
825
925
  scanDirs = [path.join(cwd, scope)];
826
- fileExtensions = [...DEFAULT_INDEX_EXTENSIONS];
827
926
  }
828
927
  else if (graphConfig) {
829
928
  scanDirs = graphConfig.scanDirs.map(d => resolveGraphScanDir(cwd, d));
830
- fileExtensions = graphConfig.extensions;
831
929
  }
832
930
  else {
833
931
  // Default: scan .aiwg/ (backward compatible)
834
932
  scanDirs = [resolveProjectAiwgDir(cwd)];
835
- fileExtensions = [...DEFAULT_INDEX_EXTENSIONS];
836
933
  }
837
934
  // Verify at least one scan directory exists
838
935
  const existingDirs = scanDirs.filter(d => fs.existsSync(d));
@@ -851,16 +948,16 @@ export async function buildIndex(cwd, options = {}) {
851
948
  // required directories" sends the operator looking in the wrong place (#2530).
852
949
  if (graph === 'framework') {
853
950
  const installRoot = resolveInstallRootHint();
854
- console.log('The framework graph indexes the AIWG corpus and can only be built at the');
855
- console.log('install root, not in a consumer project. Build it there, then sync here:');
856
- console.log('');
857
- console.log(` cd ${installRoot} && aiwg index build --graph framework --force`);
858
- console.log(` cd ${cwd} && aiwg index sync --backend fortemi-core --graph framework`);
859
- console.log('');
860
- console.log("As an immediate workaround, discovery also works with '--backend local'.");
951
+ log('The framework graph indexes the AIWG corpus and can only be built at the');
952
+ log('install root, not in a consumer project. Build it there, then sync here:');
953
+ log('');
954
+ log(` cd ${installRoot} && aiwg index build --graph framework --force`);
955
+ log(` cd ${cwd} && aiwg index sync --backend fortemi-core --graph framework`);
956
+ log('');
957
+ log("As an immediate workaround, discovery also works with '--backend local'.");
861
958
  }
862
959
  else {
863
- console.log('Run this command from a project with the required directories.');
960
+ log('Run this command from a project with the required directories.');
864
961
  }
865
962
  process.exit(1);
866
963
  }
@@ -890,24 +987,21 @@ export async function buildIndex(cwd, options = {}) {
890
987
  verbose,
891
988
  });
892
989
  const buildTimeMs = Date.now() - startTime;
893
- console.log(`Source graph built in ${buildTimeMs}ms`);
894
- console.log(` Output: ${INDEX_DIR}/source/`);
990
+ log(`Source graph built in ${buildTimeMs}ms`);
991
+ log(` Output: ${INDEX_DIR}/source/`);
895
992
  return;
896
993
  }
897
994
  // Load existing index for incremental updates
898
- const existingIndex = force
899
- ? null
900
- : outputDir
901
- ? loadIndexFromDir(indexOutputDir, 'metadata.json')
902
- : loadGraphIndexFile(effectiveOutputCwd, 'metadata.json', graph);
903
- const canReuseExisting = existingIndex?.version === INDEX_VERSION
904
- && existingIndex.extractorVersion === INDEX_EXTRACTOR_VERSION;
905
- const existingEntries = canReuseExisting ? existingIndex.entries : {};
906
- // Load checksum manifest for fast stat-based change detection (#794).
907
- // When --force is set we skip the manifest entirely and rebuild everything.
908
- const manifest = force || !canReuseExisting
995
+ const backendType = resolveGraphBackendType(graph);
996
+ const buildConfigHash = createHash('sha256')
997
+ .update(JSON.stringify({ graphConfig, backendType }))
998
+ .digest('hex');
999
+ const storedManifest = force
909
1000
  ? { version: 1, generated: '', entries: {} }
910
1001
  : loadManifest(indexOutputDir);
1002
+ const useFilenameMetadata = graphConfig?.nodeStrategy === 'filename-metadata';
1003
+ const reusable = !useFilenameMetadata && !graphConfig?.metadataSupplements?.length && !graphConfig?.edgeExtraction;
1004
+ const fileStats = !force && reusable ? new Map() : undefined;
911
1005
  const nextManifestEntries = {};
912
1006
  const manifestStats = {
913
1007
  checked: 0,
@@ -917,16 +1011,117 @@ export async function buildIndex(cwd, options = {}) {
917
1011
  pruned: 0,
918
1012
  };
919
1013
  // Collect files from all scan directories
920
- const files = scope
921
- ? existingDirs.flatMap(dir => findArtifactFiles(dir, fileExtensions))
922
- : await collectGraphIndexFiles(cwd, graph);
1014
+ const files = await collectGraphIndexFiles(cwd, graph, { scope, includeIgnored, fileStats });
923
1015
  const entries = {};
924
1016
  const tagIndex = {};
925
1017
  const depGraph = {};
926
1018
  let newCount = 0;
927
1019
  let updatedCount = 0;
928
1020
  let unchangedCount = 0;
929
- const useFilenameMetadata = graphConfig?.nodeStrategy === 'filename-metadata';
1021
+ const reportBuild = (buildTimeMs, total) => {
1022
+ log(`Artifact index built in ${buildTimeMs}ms`);
1023
+ log(` Indexed ${newCount} new, updated ${updatedCount}, unchanged ${unchangedCount}`);
1024
+ log(` Total: ${total} artifacts`);
1025
+ if (!force && manifestStats.checked > 0) {
1026
+ const fastPath = manifestStats.statSkipped;
1027
+ const slowPath = manifestStats.checked - manifestStats.statSkipped;
1028
+ log(` Change detection: ${manifestStats.checked} checked, ${fastPath} skipped via stat, ${slowPath} content-read, ${manifestStats.reindexed} re-indexed`);
1029
+ if (manifestStats.pruned > 0) {
1030
+ log(` Pruned ${manifestStats.pruned} stale manifest entries (files no longer on disk)`);
1031
+ }
1032
+ }
1033
+ const displayDir = graph ? indexOutputDir : `${INDEX_DIR}/`;
1034
+ log(` Output: ${displayDir}`);
1035
+ };
1036
+ const requiredFiles = ['tags.json', 'dependencies.json'];
1037
+ if (backendType === 'sqlite')
1038
+ requiredFiles.push('graph.db');
1039
+ const fileSignature = (filename) => {
1040
+ try {
1041
+ const stat = fs.statSync(path.join(indexOutputDir, filename), { bigint: true });
1042
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':');
1043
+ }
1044
+ catch {
1045
+ return '';
1046
+ }
1047
+ };
1048
+ const relationsReceiptMatches = () => {
1049
+ const projection = storedManifest.receipt?.relations;
1050
+ return graph !== 'framework' || Boolean(projection?.revision === FRAMEWORK_RELATIONS_REVISION
1051
+ && projection.signature && projection.signature === fileSignature('relations.json'));
1052
+ };
1053
+ const entriesDigest = (values) => createHash('sha256').update(JSON.stringify(values)).digest('hex');
1054
+ const writeBuildManifest = (values) => {
1055
+ writeManifest(indexOutputDir, { version: 1, generated: new Date().toISOString(), entries: values,
1056
+ ...(reusable ? { receipt: {
1057
+ indexVersion: INDEX_VERSION, extractorVersion: INDEX_EXTRACTOR_VERSION, buildConfigHash,
1058
+ sourceRoot: path.resolve(cwd), metadataSignature: fileSignature('metadata.json'), entriesDigest: entriesDigest(values),
1059
+ ...(graph === 'framework' ? { relations: { revision: FRAMEWORK_RELATIONS_REVISION, signature: fileSignature('relations.json') } } : {}),
1060
+ } } : {}),
1061
+ });
1062
+ };
1063
+ // A successful receipt proves the manifest belongs to this exact metadata
1064
+ // file. Still enumerate/stat every source and verify the complete file set;
1065
+ // unchanged builds need not parse the much larger metadata/dependency data.
1066
+ const receipt = storedManifest.receipt;
1067
+ if (!force && reusable && storedManifest.generated && receipt
1068
+ && receipt.indexVersion === INDEX_VERSION && receipt.extractorVersion === INDEX_EXTRACTOR_VERSION
1069
+ && receipt.buildConfigHash === buildConfigHash && receipt.sourceRoot === path.resolve(cwd)
1070
+ && receipt.metadataSignature && receipt.metadataSignature === fileSignature('metadata.json')
1071
+ && relationsReceiptMatches()
1072
+ && receipt.entriesDigest === entriesDigest(storedManifest.entries)
1073
+ && files.length === Object.keys(storedManifest.entries).length
1074
+ && requiredFiles.every(filename => fs.existsSync(path.join(indexOutputDir, filename)))) {
1075
+ const stats = loadIndexFromDir(indexOutputDir, 'stats.json');
1076
+ if (stats?.version === INDEX_VERSION && stats.extractorVersion === INDEX_EXTRACTOR_VERSION
1077
+ && stats.totalArtifacts === files.length && files.every(file => {
1078
+ try {
1079
+ return statMatches(fileStats?.get(file) ?? fs.statSync(file), storedManifest.entries[indexPathFor(cwd, file, graph)]);
1080
+ }
1081
+ catch {
1082
+ return false;
1083
+ }
1084
+ })) {
1085
+ unchangedCount = files.length;
1086
+ manifestStats.checked = files.length;
1087
+ manifestStats.statSkipped = files.length;
1088
+ const buildTimeMs = Date.now() - startTime;
1089
+ writeIndexFile(effectiveOutputCwd, 'stats.json', { ...stats, builtAt: new Date().toISOString(), buildTimeMs }, indexOutputDir);
1090
+ reportBuild(buildTimeMs, files.length);
1091
+ return;
1092
+ }
1093
+ }
1094
+ const existingIndex = force ? null : outputDir
1095
+ ? loadIndexFromDir(indexOutputDir, 'metadata.json')
1096
+ : loadGraphIndexFile(effectiveOutputCwd, 'metadata.json', graph);
1097
+ const canReuseExisting = existingIndex?.version === INDEX_VERSION
1098
+ && existingIndex.extractorVersion === INDEX_EXTRACTOR_VERSION && existingIndex.buildConfigHash === buildConfigHash;
1099
+ const existingEntries = canReuseExisting ? existingIndex.entries : {};
1100
+ const manifest = canReuseExisting ? storedManifest : { version: 1, generated: '', entries: {} };
1101
+ // Establish the complete changed-file batch before any extraction/reduction.
1102
+ // Warm stat hits never create threads or read source contents.
1103
+ const preparationStats = new Map();
1104
+ const jobs = [];
1105
+ if (!useFilenameMetadata)
1106
+ for (const fullPath of files) {
1107
+ let stat;
1108
+ try {
1109
+ stat = fileStats?.get(fullPath) ?? fs.statSync(fullPath);
1110
+ }
1111
+ catch (error) {
1112
+ if (error.code === 'ENOENT')
1113
+ continue;
1114
+ throw error;
1115
+ }
1116
+ preparationStats.set(fullPath, stat);
1117
+ const relativePath = indexPathFor(cwd, fullPath, graph), previous = manifest.entries[relativePath];
1118
+ if (!force && statMatches(stat, previous) && existingEntries[relativePath]?.checksum === previous?.checksum)
1119
+ continue;
1120
+ jobs.push({ fullPath, relativePath, mtimeMs: stat.mtimeMs, birthtimeMs: stat.birthtimeMs, sizeBytes: stat.size,
1121
+ ...(!force && existingEntries[relativePath] ? { existingChecksum: existingEntries[relativePath].checksum } : {}) });
1122
+ }
1123
+ const preparation = await runIndexPreparation(jobs, prepareIndexArtifact, import.meta.url, workers, options.onDiagnostic ?? (message => console.warn(`Index preparation: ${message}`)));
1124
+ const preparedArtifacts = new Map(jobs.map((job, index) => [job.fullPath, preparation.results[index]]));
930
1125
  for (const fullPath of files) {
931
1126
  const relativePath = indexPathFor(cwd, fullPath, graph);
932
1127
  let entry;
@@ -944,7 +1139,7 @@ export async function buildIndex(cwd, options = {}) {
944
1139
  entries[relativePath] = existingEntries[relativePath];
945
1140
  unchangedCount++;
946
1141
  if (verbose)
947
- console.log(` unchanged: ${relativePath}`);
1142
+ log(` unchanged: ${relativePath}`);
948
1143
  continue;
949
1144
  }
950
1145
  entry = buildFilenameMetadataEntry(relativePath, fullPath, graphConfig?.filenamePattern);
@@ -956,13 +1151,13 @@ export async function buildIndex(cwd, options = {}) {
956
1151
  // This is the fast path: unchanged files don't touch the filesystem beyond fs.statSync.
957
1152
  let stat;
958
1153
  try {
959
- stat = fs.statSync(fullPath);
1154
+ stat = preparationStats.get(fullPath) ?? fs.statSync(fullPath);
960
1155
  }
961
1156
  catch (err) {
962
1157
  if (err.code === 'ENOENT') {
963
1158
  manifestStats.pruned++;
964
1159
  if (verbose)
965
- console.log(` skipped missing: ${relativePath}`);
1160
+ log(` skipped missing: ${relativePath}`);
966
1161
  continue;
967
1162
  }
968
1163
  throw err;
@@ -976,98 +1171,70 @@ export async function buildIndex(cwd, options = {}) {
976
1171
  unchangedCount++;
977
1172
  manifestStats.statSkipped++;
978
1173
  if (verbose)
979
- console.log(` unchanged (stat): ${relativePath}`);
1174
+ log(` unchanged (stat): ${relativePath}`);
980
1175
  continue;
981
1176
  }
982
- // Phase 2: content read + checksum verification for files that looked changed.
983
- const content = fs.readFileSync(fullPath, 'utf-8');
984
- const checksum = computeChecksum(content);
985
- // Skip unchanged files in incremental mode (checksum matched despite stat drift)
1177
+ const prepared = preparedArtifacts.get(fullPath);
1178
+ const { checksum } = prepared;
986
1179
  if (!force && existingEntries[relativePath]?.checksum === checksum) {
987
1180
  entries[relativePath] = existingEntries[relativePath];
988
- // Update manifest with current stat so Phase 1 succeeds next time.
989
1181
  nextManifestEntries[relativePath] = makeEntry(checksum, stat);
990
1182
  unchangedCount++;
991
1183
  manifestStats.checksumSkipped++;
992
1184
  if (verbose)
993
- console.log(` unchanged (checksum): ${relativePath}`);
1185
+ log(` unchanged (checksum): ${relativePath}`);
994
1186
  continue;
995
1187
  }
996
1188
  manifestStats.reindexed++;
997
1189
  nextManifestEntries[relativePath] = makeEntry(checksum, stat);
998
- const { data, body } = parseFrontmatter(content);
999
- // YAML Flow documents (workflow.aiwg.io/v1) are pure YAML, not
1000
- // markdown+frontmatter — detect them up front so they classify and
1001
- // become discoverable (#1540).
1002
- const flow = parseFlowDoc(content, relativePath);
1003
- const decision = flow ? null : parseDecisionDoc(content, relativePath);
1004
- const gatePack = flow || decision ? null : parseGatePackDoc(content, relativePath);
1005
- const inferredType = inferType(data, relativePath);
1006
- const physicalType = inferType({ ...data, type: undefined }, relativePath);
1007
- const runbook = flow || decision || gatePack ? null : parseRunbookDoc(data, body, relativePath);
1008
- const schemaDoc = inferredType === 'schema' ? parseSchemaDoc(content, relativePath) : null;
1009
- const title = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.title ?? extractTitle(data, body);
1010
- const phase = typeof data.phase === 'string' ? data.phase : inferPhase(relativePath);
1011
- const type = flow?.type ?? decision?.type ?? gatePack?.type ?? (runbook ? 'runbook' : inferredType);
1012
- const tags = flow?.tags ?? decision?.tags ?? gatePack?.tags ?? (Array.isArray(data.tags) ? data.tags.map(String) : []);
1013
- const summary = flow?.description ?? decision?.description ?? gatePack?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
1014
- const dependencies = extractMentions(content);
1015
- const markdownLinks = extractMarkdownLinks(content);
1016
- // Discovery metadata (#1214, #1540, #1792) — meaningful for operational
1017
- // AIWG artifact kinds. Kept undefined on document types so the index file
1018
- // stays small for the common case.
1019
- const isDiscoverable = OPERATIONAL_DISCOVERY_TYPES.includes(type);
1020
- // Declarative processes have no trigger phrases — they rely on their
1021
- // capability and structure-aware search terms.
1022
- const triggers = isDiscoverable && !flow ? extractTriggers(body, data) : undefined;
1023
- const capability = flow?.capability ?? decision?.capability ?? gatePack?.capability ?? schemaDoc?.capability ?? runbook?.capability ?? (isDiscoverable ? extractCapability(data, body) : undefined);
1024
- const kind = flow?.kind ?? decision?.kind ?? gatePack?.kind ?? runbook?.kind;
1025
- const sourceType = runbook && physicalType !== 'runbook' ? physicalType : undefined;
1026
- const searchTerms = flow?.searchTerms ?? decision?.searchTerms ?? gatePack?.searchTerms ?? schemaDoc?.searchTerms ?? runbook?.searchTerms;
1027
- const kernel = data.kernel === true || data.kernel === 'true' ? true : undefined;
1028
- // Script entrypoint metadata is meaningful for skills only (#1227).
1029
- const script = type === 'skill' ? extractSkillScript(data) : undefined;
1030
- const operationalState = normalizeOperationalState(data.operational_state);
1031
- const stateTransfer = normalizeStateTransferProjection(data.state_transfer);
1032
- // Canonical short name (#1233) — used by the scorer to floor exact-name
1033
- // queries to 1.0 so hyphenated kernel-skill names like `aiwg-doctor`
1034
- // remain searchable even when the rendered title strips the hyphen.
1035
- const name = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.name ?? (isDiscoverable ? extractCanonicalName(data, relativePath) : undefined);
1036
- entry = {
1037
- path: relativePath,
1038
- type,
1039
- ...(kind ? { kind } : {}),
1040
- ...(sourceType ? { sourceType } : {}),
1041
- phase,
1042
- title,
1043
- tags,
1044
- created: typeof data.created === 'string' ? data.created : stat.birthtime.toISOString(),
1045
- updated: stat.mtime.toISOString(),
1046
- checksum,
1047
- summary,
1048
- dependencies,
1049
- ...(markdownLinks.length > 0 ? { markdownLinks } : {}),
1050
- dependents: [], // Computed after all entries are processed
1051
- ...(name ? { name } : {}),
1052
- ...(triggers && triggers.length > 0 ? { triggers } : {}),
1053
- ...(capability ? { capability } : {}),
1054
- ...(searchTerms && searchTerms.length > 0 ? { searchTerms } : {}),
1055
- ...(kernel ? { kernel } : {}),
1056
- ...(script ? { script } : {}),
1057
- ...(operationalState ? { operationalState } : {}),
1058
- ...(stateTransfer ? { stateTransfer } : {}),
1059
- };
1190
+ entry = prepared.entry;
1060
1191
  }
1061
1192
  entries[relativePath] = entry;
1062
1193
  if (existingEntries[relativePath]) {
1063
1194
  updatedCount++;
1064
1195
  if (verbose)
1065
- console.log(` updated: ${relativePath}`);
1196
+ log(` updated: ${relativePath}`);
1066
1197
  }
1067
1198
  else {
1068
1199
  newCount++;
1069
1200
  if (verbose)
1070
- console.log(` new: ${relativePath}`);
1201
+ log(` new: ${relativePath}`);
1202
+ }
1203
+ }
1204
+ // Reuse derived indexes only when their complete input set is unchanged.
1205
+ // Supplements can live outside the scanned files, so they must still be read.
1206
+ // Configured edge extractors must run to discard stale cached edges even
1207
+ // when source checksums match.
1208
+ const total = Object.keys(entries).length;
1209
+ if (canReuseExisting && !useFilenameMetadata && !graphConfig?.metadataSupplements?.length
1210
+ && !graphConfig?.edgeExtraction
1211
+ && newCount === 0 && updatedCount === 0
1212
+ && total === Object.keys(existingEntries).length
1213
+ && manifest.generated && total === Object.keys(manifest.entries).length
1214
+ && manifestStats.pruned === 0) {
1215
+ const stats = loadIndexFromDir(indexOutputDir, 'stats.json');
1216
+ if (stats?.version === INDEX_VERSION && stats.extractorVersion === INDEX_EXTRACTOR_VERSION
1217
+ && stats.totalArtifacts === total
1218
+ && requiredFiles.every(filename => fs.existsSync(path.join(indexOutputDir, filename)))) {
1219
+ // A timestamp-only source change needs a manifest refresh, but does not
1220
+ // invalidate metadata, tags, dependencies, or the persistent backend.
1221
+ const projectionNeedsRepair = graph === 'framework' && !relationsReceiptMatches();
1222
+ if (graph === 'framework') {
1223
+ const { writeRelations } = await import('./relations-builder.js');
1224
+ await writeRelations(entries, effectiveOutputCwd, indexOutputDir, force || !relationsReceiptMatches());
1225
+ }
1226
+ const buildTimeMs = Date.now() - startTime;
1227
+ writeIndexFile(effectiveOutputCwd, 'stats.json', {
1228
+ ...stats,
1229
+ builtAt: new Date().toISOString(),
1230
+ buildTimeMs,
1231
+ }, indexOutputDir);
1232
+ // Preserve no-op manifest bytes after output-only timestamp drift.
1233
+ // Renew receipts only for changed source stats or repaired projections.
1234
+ if (manifestStats.checksumSkipped > 0 || projectionNeedsRepair)
1235
+ writeBuildManifest(nextManifestEntries);
1236
+ reportBuild(buildTimeMs, total);
1237
+ return;
1071
1238
  }
1072
1239
  }
1073
1240
  // Apply metadata supplements if configured (enriches filename-metadata nodes from sidecars)
@@ -1084,13 +1251,18 @@ export async function buildIndex(cwd, options = {}) {
1084
1251
  }
1085
1252
  // Build dependency graph and compute dependents
1086
1253
  let markdownLinkEdgeCount = 0;
1254
+ const entryPaths = Object.keys(entries);
1255
+ const dependencyPaths = new Map();
1087
1256
  for (const entry of Object.values(entries)) {
1088
1257
  if (!depGraph[entry.path]) {
1089
1258
  depGraph[entry.path] = { upstream: [], downstream: [] };
1090
1259
  }
1091
1260
  for (const dep of entry.dependencies) {
1092
1261
  // Normalize: check if referenced path exists in the index
1093
- const normalizedDep = Object.keys(entries).find(p => p === dep || p.endsWith(dep));
1262
+ if (!dependencyPaths.has(dep)) {
1263
+ dependencyPaths.set(dep, entryPaths.find(p => p === dep || p.endsWith(dep)));
1264
+ }
1265
+ const normalizedDep = dependencyPaths.get(dep);
1094
1266
  if (normalizedDep && normalizedDep !== entry.path) {
1095
1267
  addDependencyEdge(depGraph, entries, entry.path, normalizedDep, 'depends-on');
1096
1268
  }
@@ -1173,18 +1345,23 @@ export async function buildIndex(cwd, options = {}) {
1173
1345
  .filter(edge => !canonicalOutgoing.has(edge)).length,
1174
1346
  };
1175
1347
  if (verbose && citationEdgeCount > 0) {
1176
- console.log(` citation edges: ${citationEdgeCount}`);
1348
+ log(` citation edges: ${citationEdgeCount}`);
1177
1349
  }
1178
1350
  }
1179
1351
  // Deduplicate dependents
1180
1352
  for (const entry of Object.values(entries)) {
1181
1353
  entry.dependents = [...new Set(entry.dependents)];
1182
1354
  }
1355
+ if (graph === 'framework') {
1356
+ const { writeRelations } = await import('./relations-builder.js');
1357
+ await writeRelations(entries, effectiveOutputCwd, indexOutputDir, force || !relationsReceiptMatches());
1358
+ }
1183
1359
  const buildTimeMs = Date.now() - startTime;
1184
1360
  // Write index files
1185
1361
  const index = {
1186
1362
  version: INDEX_VERSION,
1187
1363
  extractorVersion: INDEX_EXTRACTOR_VERSION,
1364
+ buildConfigHash,
1188
1365
  builtAt: new Date().toISOString(),
1189
1366
  buildTimeMs,
1190
1367
  entries,
@@ -1195,8 +1372,6 @@ export async function buildIndex(cwd, options = {}) {
1195
1372
  // Materialize the configured backend and always retain dependencies.json as
1196
1373
  // the stable compatibility/export contract.
1197
1374
  const { createGraphBackend } = await import('./graph-backend.js');
1198
- const { resolveGraphBackendType } = await import('./types.js');
1199
- const backendType = resolveGraphBackendType(graph);
1200
1375
  const persistentPath = backendType === 'sqlite' ? path.join(indexOutputDir, 'graph.db') : undefined;
1201
1376
  let selectedBackend;
1202
1377
  try {
@@ -1210,15 +1385,9 @@ export async function buildIndex(cwd, options = {}) {
1210
1385
  // The next manifest contains entries for every file we processed this build.
1211
1386
  // Files that disappeared from disk are pruned; the resulting manifest is
1212
1387
  // written atomically.
1213
- const nextManifest = {
1214
- version: 1,
1215
- generated: new Date().toISOString(),
1216
- entries: nextManifestEntries,
1217
- };
1218
1388
  manifestStats.pruned = Object.keys(manifest.entries).length - Object.keys(nextManifestEntries).length;
1219
1389
  if (manifestStats.pruned < 0)
1220
1390
  manifestStats.pruned = 0;
1221
- writeManifest(indexOutputDir, nextManifest);
1222
1391
  // Write stats
1223
1392
  const downstreamEdges = Object.values(depGraph).reduce((sum, node) => sum + node.downstream.length, 0);
1224
1393
  const adjacencyEntries = Object.values(depGraph).reduce((sum, node) => sum + node.upstream.length + node.downstream.length, 0);
@@ -1262,22 +1431,8 @@ export async function buildIndex(cwd, options = {}) {
1262
1431
  mostReferenced,
1263
1432
  },
1264
1433
  }, indexOutputDir);
1434
+ writeBuildManifest(nextManifestEntries);
1265
1435
  // Report
1266
- const total = Object.keys(entries).length;
1267
- console.log(`Artifact index built in ${buildTimeMs}ms`);
1268
- console.log(` Indexed ${newCount} new, updated ${updatedCount}, unchanged ${unchangedCount}`);
1269
- console.log(` Total: ${total} artifacts`);
1270
- // Report manifest-driven change detection stats (only meaningful when !force
1271
- // and we actually went through the content-read path for at least one file)
1272
- if (!force && manifestStats.checked > 0) {
1273
- const fastPath = manifestStats.statSkipped;
1274
- const slowPath = manifestStats.checked - manifestStats.statSkipped;
1275
- console.log(` Change detection: ${manifestStats.checked} checked, ${fastPath} skipped via stat, ${slowPath} content-read, ${manifestStats.reindexed} re-indexed`);
1276
- if (manifestStats.pruned > 0) {
1277
- console.log(` Pruned ${manifestStats.pruned} stale manifest entries (files no longer on disk)`);
1278
- }
1279
- }
1280
- const displayDir = graph ? indexOutputDir : `${INDEX_DIR}/`;
1281
- console.log(` Output: ${displayDir}`);
1436
+ reportBuild(buildTimeMs, total);
1282
1437
  }
1283
1438
  //# sourceMappingURL=index-builder.js.map