@aiwg/cli 2026.10.4 → 2026.10.6

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 (220) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +72 -9
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +146 -16
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-preparation.js +91 -26
  16. package/dist/src/artifacts/index-reader.js +44 -0
  17. package/dist/src/artifacts/index-refresh.js +130 -56
  18. package/dist/src/artifacts/index-status.js +20 -5
  19. package/dist/src/artifacts/index-worker.mjs +9 -7
  20. package/dist/src/artifacts/memory-maintenance.js +503 -0
  21. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  22. package/dist/src/artifacts/query-engine.js +263 -108
  23. package/dist/src/artifacts/relations-builder.js +2 -2
  24. package/dist/src/artifacts/source-graph.js +2 -2
  25. package/dist/src/artifacts/types.js +9 -1
  26. package/dist/src/cli/handlers/decision.js +19 -1
  27. package/dist/src/cli/handlers/guided-setup.js +470 -0
  28. package/dist/src/cli/handlers/help.js +2 -2
  29. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  30. package/dist/src/cli/handlers/index.js +4 -2
  31. package/dist/src/cli/handlers/init.js +41 -6
  32. package/dist/src/cli/handlers/local-executor.js +5 -3
  33. package/dist/src/cli/handlers/mc.js +509 -53
  34. package/dist/src/cli/handlers/models.js +32 -2
  35. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  36. package/dist/src/cli/handlers/rules.js +1 -1
  37. package/dist/src/cli/handlers/script-runner.js +73 -2
  38. package/dist/src/cli/handlers/serve.js +5 -3
  39. package/dist/src/cli/handlers/sessions.js +40 -6
  40. package/dist/src/cli/handlers/setup.js +428 -1
  41. package/dist/src/cli/handlers/skill-lint.js +1 -1
  42. package/dist/src/cli/handlers/subcommands.js +118 -8
  43. package/dist/src/cli/handlers/use.js +279 -93
  44. package/dist/src/cli/handlers/workspace.js +3 -1
  45. package/dist/src/cli/prompt-utils.js +8 -0
  46. package/dist/src/cli/router.js +16 -1
  47. package/dist/src/config/aiwg-config.js +42 -0
  48. package/dist/src/config/cli.js +7 -3
  49. package/dist/src/config/decision-config.js +14 -1
  50. package/dist/src/config/native-sandbox-warnings.js +25 -0
  51. package/dist/src/contributors/validation.js +6 -5
  52. package/dist/src/decision/admission.js +7 -3
  53. package/dist/src/decision/benchmark.js +469 -0
  54. package/dist/src/decision/ledger.js +22 -3
  55. package/dist/src/decision/pricing.js +147 -0
  56. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  57. package/dist/src/decision/qualification/load-staged.js +539 -0
  58. package/dist/src/decision/qualification/load.js +109 -33
  59. package/dist/src/decision/qualification/runner.js +5 -2
  60. package/dist/src/decision/review/service.js +6 -4
  61. package/dist/src/decision/review/validate.js +23 -2
  62. package/dist/src/decision/savings.js +8 -3
  63. package/dist/src/discovery/index.js +492 -0
  64. package/dist/src/discovery/interview.js +233 -0
  65. package/dist/src/discovery/plan-apply.js +571 -0
  66. package/dist/src/discovery/plan-store.js +171 -0
  67. package/dist/src/discovery/plan-types.js +2 -0
  68. package/dist/src/discovery/plan-validation.js +139 -0
  69. package/dist/src/discovery/types.js +2 -0
  70. package/dist/src/extensions/commands/definitions.js +49 -9
  71. package/dist/src/extensions/manifest.js +71 -44
  72. package/dist/src/extensions/project-local-discovery.js +1 -1
  73. package/dist/src/extensions/project-quickref.js +18 -17
  74. package/dist/src/extensions/validation.js +32 -29
  75. package/dist/src/housekeeper/daemon-config.js +148 -0
  76. package/dist/src/housekeeper/ipc.js +84 -0
  77. package/dist/src/housekeeper/lifecycle.js +148 -0
  78. package/dist/src/housekeeper/platform.js +35 -0
  79. package/dist/src/housekeeper/status.js +70 -0
  80. package/dist/src/housekeeper/units.js +114 -0
  81. package/dist/src/jobs/flow.js +22 -21
  82. package/dist/src/lint/rule-activation.js +20 -5
  83. package/dist/src/lint/runner.js +1 -1
  84. package/dist/src/mcp/cli.mjs +5 -6
  85. package/dist/src/mcp/config-file.mjs +6 -2
  86. package/dist/src/mcp/config-layers.mjs +23 -7
  87. package/dist/src/mcp/profiles.js +18 -31
  88. package/dist/src/mcp/profiles.mjs +18 -32
  89. package/dist/src/mcp/registry.js +7 -27
  90. package/dist/src/mcp/registry.mjs +7 -28
  91. package/dist/src/mcp/server.mjs +34 -1
  92. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  93. package/dist/src/mcp/tools/command-run.mjs +3 -2
  94. package/dist/src/mcp/tools/decision.mjs +49 -48
  95. package/dist/src/mcp/tools/discovery.mjs +46 -21
  96. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  97. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  98. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  99. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  100. package/dist/src/memory/audit.js +719 -0
  101. package/dist/src/memory/canonical-context.js +392 -13
  102. package/dist/src/memory/canonical-maintenance.js +95 -0
  103. package/dist/src/memory/cli-result.js +22 -0
  104. package/dist/src/memory/cli.js +176 -0
  105. package/dist/src/memory/context-pack.js +57 -6
  106. package/dist/src/memory/cost.js +512 -0
  107. package/dist/src/memory/firewall-classification.d.mts +7 -0
  108. package/dist/src/memory/firewall-classification.mjs +32 -0
  109. package/dist/src/memory/index.js +8 -0
  110. package/dist/src/memory/line-maintenance.js +68 -0
  111. package/dist/src/memory/maintenance-cli.js +286 -0
  112. package/dist/src/memory/maintenance-contract.js +136 -0
  113. package/dist/src/memory/maintenance-index.js +18 -0
  114. package/dist/src/memory/maintenance-owner.js +2 -0
  115. package/dist/src/memory/maintenance-record.js +138 -0
  116. package/dist/src/memory/maintenance.js +254 -0
  117. package/dist/src/memory/provider-memory-adapter.js +154 -0
  118. package/dist/src/memory/recommend-cli.js +83 -0
  119. package/dist/src/memory/recommend.js +269 -0
  120. package/dist/src/memory/scheduled.js +81 -0
  121. package/dist/src/memory/taxonomy.js +44 -0
  122. package/dist/src/mission-protocol/codecs.js +19 -9
  123. package/dist/src/models/config-loader.js +11 -1
  124. package/dist/src/models/model-capabilities.v1.json +74 -50
  125. package/dist/src/models/provider-policy.js +125 -62
  126. package/dist/src/models/resolver.js +45 -8
  127. package/dist/src/models/worker-installation.d.mts +7 -0
  128. package/dist/src/models/worker-installation.mjs +169 -37
  129. package/dist/src/models/wrapper-route.js +1 -1
  130. package/dist/src/output-modes/registry.js +8 -7
  131. package/dist/src/plugin/metadata-validator.js +1 -1
  132. package/dist/src/providers/deploy-version.d.mts +1 -0
  133. package/dist/src/providers/deploy-version.mjs +17 -0
  134. package/dist/src/providers/provider-definitions.js +19 -16
  135. package/dist/src/providers/skill-ownership.d.mts +11 -2
  136. package/dist/src/providers/skill-ownership.mjs +35 -17
  137. package/dist/src/rules/activation.d.mts +11 -2
  138. package/dist/src/rules/activation.mjs +82 -6
  139. package/dist/src/rules/inventory.js +1 -1
  140. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  141. package/dist/src/schema/zod-validation.d.mts +6 -0
  142. package/dist/src/schema/zod-validation.mjs +50 -0
  143. package/dist/src/serve/executor-registry.js +297 -20
  144. package/dist/src/serve/executor-token.d.mts +1 -0
  145. package/dist/src/serve/executor-token.mjs +8 -0
  146. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  147. package/dist/src/serve/worker-invocation.js +114 -15
  148. package/dist/src/sessions/adapters/claude.js +19 -18
  149. package/dist/src/sessions/adapters/codex.js +33 -19
  150. package/dist/src/sessions/adapters/copilot.js +14 -13
  151. package/dist/src/sessions/adapters/cursor.js +19 -18
  152. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  153. package/dist/src/sessions/adapters/factory.js +11 -10
  154. package/dist/src/sessions/adapters/generic.js +32 -23
  155. package/dist/src/sessions/adapters/grokbot.js +35 -35
  156. package/dist/src/sessions/adapters/hermes.js +20 -19
  157. package/dist/src/sessions/adapters/muse.js +50 -49
  158. package/dist/src/sessions/adapters/omp.js +5 -4
  159. package/dist/src/sessions/adapters/openclaw.js +30 -29
  160. package/dist/src/sessions/adapters/opencode.js +26 -26
  161. package/dist/src/sessions/adapters/openhuman.js +24 -23
  162. package/dist/src/sessions/adapters/pi.js +5 -4
  163. package/dist/src/sessions/adapters/windsurf.js +8 -8
  164. package/dist/src/sessions/analytics.js +10 -9
  165. package/dist/src/sessions/batch-contracts.js +1 -1
  166. package/dist/src/sessions/batch-import.js +202 -2
  167. package/dist/src/sessions/candidates.js +7 -6
  168. package/dist/src/sessions/contracts.js +73 -54
  169. package/dist/src/sessions/coverage-preflight.js +26 -1
  170. package/dist/src/sessions/output-registration.js +11 -10
  171. package/dist/src/sessions/policy.js +18 -7
  172. package/dist/src/sessions/refresh.js +2 -3
  173. package/dist/src/sessions/repository.js +4 -2
  174. package/dist/src/sessions/workspace-discovery.js +92 -26
  175. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  176. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  177. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  178. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  179. package/dist/src/update/checker.d.mts +4 -0
  180. package/dist/src/update/checker.mjs +14 -0
  181. package/dist/src/writing/exemplar-selection.js +6 -6
  182. package/dist/src/writing/voice-evaluation.js +24 -23
  183. package/dist/src/writing/writer-migration.js +14 -13
  184. package/dist/src/writing/writer-profile.js +19 -18
  185. package/dist/src/writing/writing-brief.js +18 -17
  186. package/dist/src/writing/writing-receipt.js +47 -46
  187. package/package.json +5 -2
  188. package/schemas/decision/DecisionReview.schema.json +78 -1
  189. package/schemas/mission-v1.schema.json +3 -0
  190. package/tools/agents/deploy-agents.mjs +67 -39
  191. package/tools/agents/providers/antigravity.mjs +6 -1
  192. package/tools/agents/providers/base.mjs +19 -13
  193. package/tools/agents/providers/claude.mjs +1 -1
  194. package/tools/agents/providers/codex.mjs +50 -56
  195. package/tools/agents/providers/copilot.mjs +8 -2
  196. package/tools/agents/providers/cursor.mjs +1 -1
  197. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  198. package/tools/agents/providers/factory.mjs +7 -1
  199. package/tools/agents/providers/grok-build.mjs +18 -2
  200. package/tools/agents/providers/grokbot.mjs +1 -1
  201. package/tools/agents/providers/hermes.mjs +7 -13
  202. package/tools/agents/providers/muse-hooks.mjs +21 -14
  203. package/tools/agents/providers/muse.mjs +7 -2
  204. package/tools/agents/providers/omp.mjs +28 -4
  205. package/tools/agents/providers/opencode.mjs +14 -5
  206. package/tools/agents/providers/openhuman.mjs +1 -1
  207. package/tools/agents/providers/pi.mjs +1 -1
  208. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  209. package/tools/agents/providers/rule-delivery.mjs +263 -71
  210. package/tools/agents/providers/windsurf.mjs +7 -0
  211. package/tools/cli/workspace-status.mjs +5 -0
  212. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  213. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  214. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  215. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  216. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  217. package/tools/ralph-external/process-monitor.mjs +7 -4
  218. package/tools/ralph-external/session-launcher.mjs +280 -32
  219. package/tools/skills/deploy-skills-codex.mjs +11 -7
  220. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -0,0 +1,339 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getUserStateDir } from '../config/user-state.js';
5
+ import { projectAiwgPath, projectControlPath } from '../config/project-artifacts.js';
6
+ import { validateIndexConfig } from '../config/aiwg-config.js';
7
+ import { collectGraphIndexFiles, findArtifactFiles, INDEX_SCRATCH_DIRS } from './index-files.js';
8
+ import { writeIndexFile } from './index-reader.js';
9
+ import { GRAPH_CONFIGS, INDEX_VERSION, INDEX_EXTRACTOR_VERSION, getGraphIndexDir, resolveGraphBackendType, resolveGraphScanDir } from './types.js';
10
+ import { acquireDirectoryLock, DirectoryLockContentionError } from './prebuilt-build-lock.js';
11
+ import { resolveFrameworkIndexRoot } from './framework-index-paths.js';
12
+ import { resolveProjectLocalSearchRoots } from '../extensions/project-local-paths.js';
13
+ import { getFortemiCoreIndexDir } from './fortemi-core-sync.js';
14
+ function json(file) {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return undefined;
21
+ throw error;
22
+ }
23
+ }
24
+ function canonical(root) {
25
+ try {
26
+ return fs.realpathSync(root);
27
+ }
28
+ catch {
29
+ return path.resolve(root);
30
+ }
31
+ }
32
+ function hashFiles(files, context) {
33
+ const hash = createHash('sha256').update(JSON.stringify(context));
34
+ for (const file of [...new Set(files)].sort()) {
35
+ hash.update('\0').update(file).update('\0').update(fs.readFileSync(file));
36
+ }
37
+ return hash.digest('hex');
38
+ }
39
+ /** Same authorized source set as the builder, including links and bundle roots. */
40
+ export async function graphIndexDigest(cwd, graph, observation = {}) {
41
+ const fileStats = observation.fileStats ?? new Map();
42
+ const files = observation.files ? [...observation.files]
43
+ : graph === 'source' ? (await import('./source-graph.js')).collectSourceFiles(cwd, true)
44
+ : await collectGraphIndexFiles(cwd, graph, { selection: observation.selection, fileStats });
45
+ const ignoreRoots = graph && GRAPH_CONFIGS[graph]?.shared && graph !== 'framework' ? [] : [cwd];
46
+ if (graph)
47
+ ignoreRoots.push(...GRAPH_CONFIGS[graph].scanDirs.map(dir => resolveGraphScanDir(cwd, dir)));
48
+ for (const root of ignoreRoots) {
49
+ const ignoreFile = path.join(root, '.gitignore');
50
+ if (fs.existsSync(ignoreFile))
51
+ files.push(ignoreFile);
52
+ }
53
+ if (graph === 'source')
54
+ for (const file of ['tsconfig.json', 'package.json']) {
55
+ if (fs.existsSync(path.join(cwd, file)))
56
+ files.push(path.join(cwd, file));
57
+ }
58
+ // Supplements affect graph entries even though they are outside scanDirs.
59
+ for (const supplement of (graph ? GRAPH_CONFIGS[graph]?.metadataSupplements : undefined) ?? []) {
60
+ files.push(...findArtifactFiles(resolveGraphScanDir(cwd, supplement.scanDir)));
61
+ }
62
+ const inputs = [...new Set(files)].sort().map(file => {
63
+ const stat = fileStats.get(file) ?? fs.statSync(file);
64
+ return [file, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs];
65
+ });
66
+ return createHash('sha256').update(JSON.stringify([graph && GRAPH_CONFIGS[graph]?.shared ? null : canonical(cwd), INDEX_VERSION, INDEX_EXTRACTOR_VERSION,
67
+ graph ? GRAPH_CONFIGS[graph] : null, resolveGraphBackendType(graph), inputs])).digest('hex');
68
+ }
69
+ export function resolveFreshnessPolicy(cwd, adapter) {
70
+ const config = json(projectControlPath(cwd, 'aiwg.config'))?.index;
71
+ const errors = validateIndexConfig(config && { freshness: config.freshness });
72
+ if (errors.length)
73
+ throw new Error(errors.join('; '));
74
+ const defaults = {
75
+ graphs: 3_600_000, sessions: 900_000, caches: 86_400_000, runtime: 21_600_000,
76
+ models: 86_400_000, issues: 300_000, datasets: 86_400_000,
77
+ };
78
+ return { index: adapter.name, targetStaleAfterMs: defaults[adapter.group], maxRetriesPerDay: 3,
79
+ measureConvergence: true, ...config?.freshness?.[adapter.group], ...config?.freshness?.[adapter.name] };
80
+ }
81
+ /** Registry is bounded by graph registrations and existing owner scopes. No host discovery. */
82
+ export function indexAdapterRegistry(cwd) {
83
+ const adapters = [];
84
+ const add = (definition) => {
85
+ const adapter = { ...definition,
86
+ probe: (workspace, nowMs) => probeIndexAdapter(adapter, workspace, nowMs),
87
+ refresh: async (opts) => {
88
+ const { refreshIndexes } = await import('./index-refresh.js');
89
+ return refreshIndexes({ cwd: opts.cwd, only: adapter.name, full: opts.full,
90
+ dryRun: opts.dryRun, signal: opts.signal });
91
+ },
92
+ convergence: last => {
93
+ const jobs = last.jobs.filter(job => definition.group === 'graphs' ? job.kind === 'graph' && job.graph === definition.graph
94
+ : definition.group === 'caches' ? job.kind === 'cache' && job.graph === definition.graph : job.kind === definition.name);
95
+ return { converged: jobs.length > 0 && jobs.every(job => job.freshness?.status === 'fresh'
96
+ && job.freshness.convergence?.converged !== false), wallMs: last.totals.wallMs,
97
+ residualJobs: jobs.filter(job => job.freshness?.status !== 'fresh').length };
98
+ },
99
+ };
100
+ adapters.push(adapter);
101
+ };
102
+ for (const [graph, config] of Object.entries(GRAPH_CONFIGS)) {
103
+ const directory = getGraphIndexDir(cwd, graph);
104
+ const outputs = ['metadata.json', 'dependencies.json', 'tags.json', 'stats.json'];
105
+ if (resolveGraphBackendType(graph) === 'sqlite')
106
+ outputs.push('graph.db');
107
+ if (graph === 'framework')
108
+ outputs.push('relations.json');
109
+ add({ name: graph, graph, group: 'graphs', identity: canonical(directory),
110
+ backend: resolveGraphBackendType(graph), operation: 'buildIndex', supported: true,
111
+ exclusions: [...INDEX_SCRATCH_DIRS, 'symlinks', 'gitignored', 'hidden directories'],
112
+ roots: workspace => [...new Set([
113
+ ...config.scanDirs.map(dir => canonical(resolveGraphScanDir(graph === 'framework' ? resolveFrameworkIndexRoot(workspace) : workspace, dir))),
114
+ ...(graph === 'project' ? [canonical(workspace), ...resolveProjectLocalSearchRoots(workspace).map(root => canonical(root.rootPath))] : []),
115
+ ])],
116
+ outputPaths: outputs.map(file => path.join(directory, file)), statePath: path.join(directory, 'freshness.json') });
117
+ if (graph === 'framework') {
118
+ const owner = adapters[adapters.length - 1];
119
+ add({ ...owner, name: 'relationships', operation: 'buildIndex(framework):relations',
120
+ reason: 'Relationship projection shares the framework snapshot and owner lock' });
121
+ }
122
+ const cacheDirectory = getFortemiCoreIndexDir(cwd, graph);
123
+ if (fs.existsSync(cacheDirectory))
124
+ add({ name: `caches:${graph}`, graph, group: 'caches',
125
+ identity: canonical(cacheDirectory), backend: 'fortemi-core', operation: 'syncFortemiCoreIndex',
126
+ supported: true, exclusions: ['remote mutation'], roots: () => [canonical(directory)],
127
+ outputPaths: ['manifest.json', 'aiwg-fortemi-index-v2.json'].map(file => path.join(cacheDirectory, file)),
128
+ statePath: path.join(cacheDirectory, 'freshness.json') });
129
+ }
130
+ const scopes = [
131
+ { name: 'sessions', directory: path.join(cwd, '.aiwg/sessions'), backend: 'sqlite',
132
+ operation: 'refreshSessionCatalog', outputs: ['catalog.sqlite'] },
133
+ { name: 'runtime', directory: projectAiwgPath(cwd, 'smiths/toolsmith'), backend: 'json',
134
+ operation: 'RuntimeDiscovery', outputs: ['runtime.json', 'runtime-info.md'] },
135
+ { name: 'issues', directory: projectAiwgPath(cwd, 'issues'), backend: 'json',
136
+ operation: 'LocalIssueProviderCore.rebuildIssueIndex', outputs: ['index/issues.index.json'] },
137
+ ];
138
+ for (const scope of scopes) {
139
+ // Absent opt-in scopes are visible as unavailable; planning still respects opt-in.
140
+ add({ name: scope.name, group: scope.name, identity: canonical(scope.directory), backend: scope.backend,
141
+ operation: scope.operation, supported: true, exclusions: ['undeclared provider roots', 'auth and token files'],
142
+ roots: workspace => {
143
+ if (scope.name !== 'sessions')
144
+ return [canonical(scope.directory)];
145
+ let config;
146
+ try {
147
+ config = json(projectControlPath(workspace, 'aiwg.config'));
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ const manifests = [path.join(workspace, '.aiwg/sessions/discovery-manifest.json'),
153
+ ...(config?.sessions?.discovery_manifests ?? []).map(file => path.resolve(workspace, file))];
154
+ return [...new Set(manifests.flatMap(file => {
155
+ try {
156
+ const manifest = json(file);
157
+ return Object.values(manifest?.authorizedRoots ?? {}).flat().map(canonical);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ }))];
163
+ }, outputPaths: scope.outputs.map(file => path.join(scope.directory, file)),
164
+ statePath: path.join(scope.directory, '.index', 'freshness.json') });
165
+ }
166
+ for (const name of ['models', 'datasets'])
167
+ add({ name, group: name,
168
+ identity: `${name}:${canonical(cwd)}`, backend: 'remote', operation: name === 'models'
169
+ ? 'resolveDynamicModelCatalog' : 'dataset owner', supported: false,
170
+ reason: `${name} has no local deterministic freshness probe; use its owning operation`,
171
+ roots: () => [], outputPaths: [], exclusions: ['remote-only owners'],
172
+ statePath: projectAiwgPath(cwd, '.index', `${name}-freshness.json`) });
173
+ return adapters;
174
+ }
175
+ export function selectIndexAdapters(adapters, only = 'all') {
176
+ if (only === 'all')
177
+ return adapters;
178
+ const selected = adapters.filter(adapter => adapter.name === only || adapter.group === only);
179
+ if (!selected.length && !['sessions', 'issues', 'caches'].includes(only))
180
+ throw new Error(`Unknown index: ${only}`);
181
+ return selected;
182
+ }
183
+ async function adapterDigest(adapter, cwd) {
184
+ if (adapter.group === 'graphs')
185
+ return graphIndexDigest(adapter.graph === 'framework' ? resolveFrameworkIndexRoot(cwd) : cwd, adapter.graph);
186
+ if (adapter.group === 'caches')
187
+ return hashFiles(['metadata.json', 'dependencies.json'].map(file => path.join(getGraphIndexDir(cwd, adapter.graph), file)), adapter.identity);
188
+ if (adapter.group === 'issues')
189
+ return hashFiles(['items', 'events', 'comments'].flatMap(dir => findArtifactFiles(projectAiwgPath(cwd, 'issues', dir), ['.md', '.json', '.jsonl'])), adapter.identity);
190
+ if (adapter.group === 'sessions') {
191
+ const { readDiscoveryManifest, discoverWorkspaceHistories } = await import('../sessions/workspace-discovery.js');
192
+ const config = json(projectControlPath(cwd, 'aiwg.config'));
193
+ const manifests = [path.join(cwd, '.aiwg/sessions/discovery-manifest.json'),
194
+ ...(config?.sessions?.discovery_manifests ?? []).map(file => path.resolve(cwd, file))];
195
+ const inputs = [];
196
+ for (const file of [...new Set(manifests)])
197
+ if (fs.existsSync(file)) {
198
+ const previous = await readDiscoveryManifest(file);
199
+ if (previous.workspaceId !== canonical(cwd))
200
+ throw new Error('discovery manifest workspace mismatch');
201
+ const authorizedRoots = previous.authorizedRoots ?? {};
202
+ if (!previous.authorizedRoots)
203
+ for (const source of previous.sources) {
204
+ const roots = authorizedRoots[source.provider] ??= [];
205
+ if (!roots.includes(source.authorizedRoot))
206
+ roots.push(source.authorizedRoot);
207
+ }
208
+ const current = await discoverWorkspaceHistories({ workspace: cwd,
209
+ refreshFrom: { ...previous, authorizedRoots }, knownSources: previous.sources,
210
+ exclude: config?.sessions?.import?.exclude });
211
+ inputs.push(current.manifestId);
212
+ }
213
+ if (!inputs.length)
214
+ throw new Error('no authorized discovery manifest');
215
+ return createHash('sha256').update(JSON.stringify([adapter.identity, inputs])).digest('hex');
216
+ }
217
+ // RuntimeDiscovery owns environmental discovery; its input receipt bounds this probe.
218
+ const runtime = json(adapter.outputPaths[0]);
219
+ if (!runtime?.inputFingerprint)
220
+ throw new Error('runtime input fingerprint unavailable');
221
+ return hashFiles([projectControlPath(cwd, 'aiwg.config')].filter(file => fs.existsSync(file)), runtime.inputFingerprint);
222
+ }
223
+ function outputDigest(adapter) {
224
+ if (!adapter.outputPaths.every(file => fs.existsSync(file)))
225
+ return undefined;
226
+ // The owner may renew stats on an unchanged build. Snapshot identity follows primary output.
227
+ const stat = fs.statSync(adapter.outputPaths[0], { bigint: true });
228
+ // Rename changes ctime during publication; inode, mtime and size survive it.
229
+ return createHash('sha256').update(JSON.stringify([adapter.identity, String(stat.dev), String(stat.ino),
230
+ String(stat.size), String(stat.mtimeNs)])).digest('hex');
231
+ }
232
+ /** Synchronous persisted view for existing reader callers; CLI/daemon also probe inputs. */
233
+ export function indexFreshnessSnapshot(adapter, cwd, nowMs = Date.now(), digest) {
234
+ const fallback = { index: adapter.name, targetStaleAfterMs: 3_600_000,
235
+ maxRetriesPerDay: 3, measureConvergence: true };
236
+ let policy = fallback;
237
+ try {
238
+ policy = resolveFreshnessPolicy(cwd, adapter);
239
+ if (!adapter.supported)
240
+ return { name: adapter.name, status: 'unsupported', policy,
241
+ lastVerifiedDigest: null, lastVerifiedAt: null, dirtySince: null, dirtyAgeMs: null,
242
+ pendingWork: 0, lastError: null, reason: adapter.reason };
243
+ const state = json(adapter.statePath);
244
+ const outputs = outputDigest(adapter);
245
+ const age = state?.lastVerifiedAt ? Math.max(0, nowMs - Date.parse(state.lastVerifiedAt)) : Infinity;
246
+ const changed = !state || !outputs || outputs !== state.outputDigest || (digest !== undefined && state.lastVerifiedDigest !== digest);
247
+ const dirtySince = state?.dirtySince ?? (changed ? new Date(nowMs).toISOString()
248
+ : age > policy.targetStaleAfterMs ? new Date(Date.parse(state.lastVerifiedAt) + policy.targetStaleAfterMs).toISOString() : null);
249
+ return { name: adapter.name, policy,
250
+ status: !outputs ? 'unavailable' : state?.lastError ? 'degraded'
251
+ : changed || state?.pendingWork || age > policy.targetStaleAfterMs ? 'stale' : 'fresh',
252
+ lastVerifiedDigest: state?.lastVerifiedDigest ?? null, lastVerifiedAt: state?.lastVerifiedAt ?? null,
253
+ dirtySince, dirtyAgeMs: dirtySince ? Math.max(0, nowMs - Date.parse(dirtySince)) : null,
254
+ pendingWork: changed ? Math.max(1, state?.pendingWork ?? 0) : state?.pendingWork ?? 0,
255
+ lastError: state?.lastError ?? null, convergence: policy.measureConvergence && state?.convergence
256
+ ? { ...state.convergence, converged: state.convergence.residualJobs === 0
257
+ && state.convergence.wallMs <= policy.targetStaleAfterMs } : undefined };
258
+ }
259
+ catch (error) {
260
+ return { name: adapter.name, status: 'degraded', policy, lastVerifiedDigest: null, lastVerifiedAt: null,
261
+ dirtySince: null, dirtyAgeMs: null, pendingWork: 1, lastError: String(error) };
262
+ }
263
+ }
264
+ export async function probeIndexAdapter(adapter, cwd, nowMs = Date.now()) {
265
+ const persisted = indexFreshnessSnapshot(adapter, cwd, nowMs);
266
+ if (!adapter.supported || persisted.status === 'unavailable')
267
+ return persisted;
268
+ try {
269
+ if (adapter.group === 'sessions') {
270
+ const { sessionRefreshAvailable } = await import('../sessions/refresh.js');
271
+ if (!sessionRefreshAvailable())
272
+ return { ...persisted, status: 'unavailable', pendingWork: 1,
273
+ reason: 'sqlite unavailable' };
274
+ }
275
+ if (adapter.group === 'caches') {
276
+ const { getFortemiCoreSyncStatus } = await import('./fortemi-core-sync.js');
277
+ const cache = getFortemiCoreSyncStatus(cwd, adapter.graph);
278
+ if (cache.stale && !cache.sourceIndexNewer)
279
+ return { ...persisted, status: 'degraded',
280
+ pendingWork: 1, lastError: cache.reason };
281
+ }
282
+ const digest = await adapterDigest(adapter, cwd);
283
+ let snapshot = indexFreshnessSnapshot(adapter, cwd, nowMs, digest);
284
+ // Preserve the first measured dirty observation without racing a publisher.
285
+ // Probe never builds or creates an absent index.
286
+ if (fs.existsSync(adapter.statePath)
287
+ && snapshot.status === 'stale' && snapshot.lastVerifiedDigest !== digest) {
288
+ let release;
289
+ try {
290
+ release = await acquireDirectoryLock(adapter.group === 'graphs'
291
+ ? `${path.dirname(adapter.statePath)}.build.lock` : path.join(getUserStateDir(), 'index-refresh.lock'), { timeoutMs: 0 });
292
+ const state = json(adapter.statePath);
293
+ if (state && !state.dirtySince && state.lastVerifiedDigest !== digest) {
294
+ writeIndexFile(cwd, path.basename(adapter.statePath), { ...state,
295
+ dirtySince: new Date(nowMs).toISOString(), pendingWork: 1 }, path.dirname(adapter.statePath));
296
+ }
297
+ snapshot = indexFreshnessSnapshot(adapter, cwd, nowMs, digest);
298
+ }
299
+ catch (error) {
300
+ if (!(error instanceof DirectoryLockContentionError))
301
+ throw error;
302
+ }
303
+ finally {
304
+ await release?.();
305
+ }
306
+ }
307
+ return snapshot;
308
+ }
309
+ catch (error) {
310
+ return { ...indexFreshnessSnapshot(adapter, cwd, nowMs), status: 'degraded',
311
+ pendingWork: 1, lastError: error instanceof Error ? error.message : String(error) };
312
+ }
313
+ }
314
+ export async function indexAdapterDigest(adapter, cwd) {
315
+ if (!adapter.supported)
316
+ return undefined;
317
+ return adapterDigest(adapter, cwd);
318
+ }
319
+ /** Call under the existing owner lock; failure never advances verification. */
320
+ export function recordIndexVerification(adapter, cwd, opts, publicationDir) {
321
+ const now = opts.nowMs ?? Date.now();
322
+ let prior;
323
+ try {
324
+ prior = json(adapter.statePath);
325
+ }
326
+ catch { /* Replace corrupt receipt, never the snapshot. */ }
327
+ const complete = !opts.error && !!opts.before && opts.before === opts.after && !opts.pendingWork;
328
+ const policy = resolveFreshnessPolicy(cwd, adapter);
329
+ const state = { lastVerifiedDigest: complete ? opts.after : prior?.lastVerifiedDigest ?? null,
330
+ lastVerifiedAt: complete ? new Date(now).toISOString() : prior?.lastVerifiedAt ?? null,
331
+ dirtySince: complete ? null : prior?.dirtySince ?? new Date(now).toISOString(),
332
+ pendingWork: complete ? 0 : Math.max(1, opts.pendingWork ?? 0), lastError: opts.error ?? null,
333
+ outputDigest: complete ? outputDigest(publicationDir ? { ...adapter, outputPaths: adapter.outputPaths.map(file => fs.existsSync(path.join(publicationDir, path.basename(file))) ? path.join(publicationDir, path.basename(file)) : file) } : adapter) : prior?.outputDigest,
334
+ ...(policy.measureConvergence ? { convergence: { converged: complete && opts.wallMs <= policy.targetStaleAfterMs,
335
+ wallMs: opts.wallMs, residualJobs: complete ? 0 : Math.max(1, opts.pendingWork ?? 0) } } : {}) };
336
+ writeIndexFile(cwd, path.basename(adapter.statePath), state, publicationDir ?? path.dirname(adapter.statePath));
337
+ return indexFreshnessSnapshot(adapter, cwd, now, opts.after);
338
+ }
339
+ //# sourceMappingURL=index-adapters.js.map
@@ -18,14 +18,17 @@ import { parseDecisionJson, parseDecisionYaml } from '../decision/entry.js';
18
18
  import { validateGateDocument } from '../gates/schema.js';
19
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
- import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
21
+ import { writeIndexFile, resolveIndexDir, loadGraphIndexFile, publishIndexFiles } from './index-reader.js';
22
22
  import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-manifest.js';
23
23
  import { FRAMEWORK_RELATIONS_REVISION } from './framework-source-digest.js';
24
24
  import { normalizeOperationalState } from './operational-state.js';
25
25
  import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
26
26
  import { normalizeStateTransferProjection } from './state-transfer.js';
27
- import { runIndexPreparation } from './index-preparation.js';
27
+ import { resolveIndexWorkers, runIndexPreparation } from './index-preparation.js';
28
28
  import { collectGraphIndexFiles, findArtifactFiles, indexPathFor } from './index-files.js';
29
+ import { acquireDirectoryLock } from './prebuilt-build-lock.js';
30
+ import { graphIndexDigest, indexAdapterRegistry, recordIndexVerification } from './index-adapters.js';
31
+ import { loadMemoryRecord } from '../memory/maintenance-record.js';
29
32
  function absoluteEntryPath(cwd, entryPath, graph) {
30
33
  if ((!graph || graph === 'project') && entryPath.startsWith(`${DEFAULT_PROJECT_AIWG_DIR}/`)) {
31
34
  return path.join(resolveProjectAiwgDir(cwd), entryPath.slice(DEFAULT_PROJECT_AIWG_DIR.length + 1));
@@ -369,6 +372,9 @@ function inferType(data, filePath) {
369
372
  // Rules/RULES-INDEX.md is a curated index file, not a rule.
370
373
  if (basename === 'rules-index' || basename === 'index')
371
374
  break;
375
+ // <rule>.core.md is a delivery body (#3450), not a separate rule.
376
+ if (/\.core\.md$/i.test(filePath))
377
+ break;
372
378
  if (isMarkdown)
373
379
  return 'rule';
374
380
  break;
@@ -836,27 +842,73 @@ function resolveInstallRootHint() {
836
842
  catch { /* fall through */ }
837
843
  return '<aiwg install root>';
838
844
  }
845
+ /** Current source identities for rule bindings; no search index or graph is written. */
846
+ export async function collectRuleBindingEntries(cwd, scope) {
847
+ const fileStats = new Map();
848
+ const files = await collectGraphIndexFiles(cwd, undefined, { scope, fileStats });
849
+ return files.flatMap(fullPath => {
850
+ const stat = fileStats.get(fullPath) ?? fs.statSync(fullPath);
851
+ const entry = extractIndexArtifact({ fullPath, relativePath: indexPathFor(cwd, fullPath),
852
+ mtimeMs: stat.mtimeMs, birthtimeMs: stat.birthtimeMs, sizeBytes: stat.size }, true).entry;
853
+ return entry && ['skill', 'agent', 'flow'].includes(entry.type) ? [entry] : [];
854
+ });
855
+ }
839
856
  /** Shared serial/worker read, checksum and extraction. No index output or global reduction. */
840
857
  export function prepareIndexArtifact(job) {
858
+ const warnings = [];
859
+ const result = extractIndexArtifact(job, false, warnings);
860
+ return { ...result, ...(warnings.length ? { warnings } : {}) };
861
+ }
862
+ function extractIndexArtifact(job, bindingOnly, warnings = []) {
841
863
  const { fullPath, relativePath } = job;
842
864
  const content = fs.readFileSync(fullPath, 'utf8');
843
865
  const checksum = computeChecksum(content);
844
866
  if (job.existingChecksum === checksum)
845
867
  return { checksum };
846
868
  const { data, body } = parseFrontmatter(content);
869
+ let memoryRecord;
870
+ if (data.memoryRecord !== undefined) {
871
+ try {
872
+ memoryRecord = loadMemoryRecord(data.memoryRecord);
873
+ }
874
+ catch (error) {
875
+ warnings.push(`${relativePath}: invalid memoryRecord metadata quarantined: ${error instanceof Error ? error.message : String(error)}`);
876
+ }
877
+ }
878
+ const inferredType = inferType(data, relativePath);
879
+ const bindingType = ['skill', 'agent', 'flow'].includes(inferredType);
880
+ // Only YAML flows can promote an otherwise unrelated document to a binding target.
881
+ if (bindingOnly && !bindingType && !/\.ya?ml$/i.test(relativePath))
882
+ return { checksum };
847
883
  // YAML Flow documents (workflow.aiwg.io/v1) are pure YAML, not
848
884
  // markdown+frontmatter — detect them up front so they classify and
849
885
  // become discoverable (#1540).
850
886
  const flow = parseFlowDoc(content, relativePath);
887
+ if (bindingOnly && !flow && !bindingType)
888
+ return { checksum };
851
889
  const decision = flow ? null : parseDecisionDoc(content, relativePath);
852
890
  const gatePack = flow || decision ? null : parseGatePackDoc(content, relativePath);
853
- const inferredType = inferType(data, relativePath);
891
+ if (bindingOnly && (decision || gatePack))
892
+ return { checksum };
854
893
  const physicalType = inferType({ ...data, type: undefined }, relativePath);
855
894
  const runbook = flow || decision || gatePack ? null : parseRunbookDoc(data, body, relativePath);
856
895
  const schemaDoc = inferredType === 'schema' ? parseSchemaDoc(content, relativePath) : null;
857
896
  const title = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.title ?? extractTitle(data, body);
858
897
  const phase = typeof data.phase === 'string' ? data.phase : inferPhase(relativePath);
859
898
  const type = flow?.type ?? decision?.type ?? gatePack?.type ?? (runbook ? 'runbook' : inferredType);
899
+ // Binding resolution consumes only identity and title. Keep the same canonical
900
+ // classification (including declarative flows and runbooks), without extracting
901
+ // search terms, mentions, links, or constructing a persistent graph.
902
+ if (bindingOnly)
903
+ return { checksum, entry: {
904
+ path: relativePath, type, title, phase,
905
+ ...(memoryRecord ? { memoryRecord } : {}),
906
+ name: flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.name
907
+ ?? (OPERATIONAL_DISCOVERY_TYPES.includes(type)
908
+ ? extractCanonicalName(data, relativePath) : undefined),
909
+ tags: [], dependencies: [], dependents: [], checksum,
910
+ created: '', updated: '', summary: '',
911
+ } };
860
912
  const tags = flow?.tags ?? decision?.tags ?? gatePack?.tags ?? (Array.isArray(data.tags) ? data.tags.map(String) : []);
861
913
  const summary = flow?.description ?? decision?.description ?? gatePack?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
862
914
  const dependencies = extractMentions(content);
@@ -882,6 +934,8 @@ export function prepareIndexArtifact(job) {
882
934
  // remain searchable even when the rendered title strips the hyphen.
883
935
  const name = flow?.name ?? decision?.name ?? gatePack?.name ?? schemaDoc?.name ?? (isDiscoverable ? extractCanonicalName(data, relativePath) : undefined);
884
936
  const entry = {
937
+ ...(memoryRecord ? { memoryRecord } : {}),
938
+ ...(typeof data.memoryCategory === 'string' ? { memoryCategory: data.memoryCategory } : {}),
885
939
  path: relativePath,
886
940
  type,
887
941
  ...(kind ? { kind } : {}),
@@ -910,7 +964,69 @@ export function prepareIndexArtifact(job) {
910
964
  return { checksum, entry };
911
965
  }
912
966
  export async function buildIndex(cwd, options = {}) {
967
+ loadUserGraphConfigs(cwd);
968
+ loadGlobalGraphConfigs();
969
+ const scanDirs = options.scope ? [path.join(cwd, options.scope)] : options.graph
970
+ ? GRAPH_CONFIGS[options.graph]?.scanDirs.map(dir => resolveGraphScanDir(cwd, dir)) ?? []
971
+ : [resolveProjectAiwgDir(cwd)];
972
+ if (!scanDirs.some(dir => fs.existsSync(dir))) {
973
+ // Keep the owner's missing-root diagnostic/exit path before creating output directories.
974
+ await buildIndexSnapshot(cwd, options, () => '');
975
+ return;
976
+ }
977
+ const directory = options.outputDir
978
+ ? path.join(options.outputDir, options.graph ? `.aiwg/.index/${options.graph}` : INDEX_DIR)
979
+ : resolveIndexDir(cwd, options.graph);
980
+ // Manual and background builds acquire the same per-output owner lock.
981
+ const release = await acquireDirectoryLock(`${directory}.build.lock`, {
982
+ timeoutMs: options.lockWaitMs ?? 180_000, reclaimDeadOwner: true,
983
+ });
984
+ let staging;
985
+ const start = Date.now();
986
+ let owned;
987
+ try {
988
+ const adapter = !options.scope && !options.includeIgnored
989
+ ? indexAdapterRegistry(cwd).find(item => item.name === (options.graph ?? 'project')) : undefined;
990
+ owned = adapter && directory === path.dirname(adapter.statePath) ? adapter : undefined;
991
+ const observation = { selection: {} };
992
+ const preparePublication = () => {
993
+ fs.mkdirSync(directory, { recursive: true });
994
+ return staging ??= fs.mkdtempSync(path.join(directory, '.build-'));
995
+ };
996
+ await buildIndexSnapshot(cwd, options, preparePublication, owned ? observation : undefined);
997
+ // A receipt hit verifies the existing snapshot directly against the scan's
998
+ // stats. There is no extraction interval to recheck. Real builds still probe
999
+ // again after preparation so concurrent source changes remain pending.
1000
+ const after = owned ? observation.unchanged ? observation.before
1001
+ : await graphIndexDigest(cwd, options.graph, { selection: observation.selection }) : undefined;
1002
+ // All extraction and backend work has succeeded before any live file moves.
1003
+ if (owned)
1004
+ recordIndexVerification(owned, cwd, { before: observation.before, after, wallMs: Date.now() - start }, staging);
1005
+ if (staging)
1006
+ publishIndexFiles(staging, directory);
1007
+ }
1008
+ catch (error) {
1009
+ if (owned) {
1010
+ try {
1011
+ recordIndexVerification(owned, cwd, { error: error instanceof Error ? error.message : String(error), wallMs: Date.now() - start });
1012
+ }
1013
+ catch { /* Preserve the original build failure if recording is unavailable. */ }
1014
+ }
1015
+ throw error;
1016
+ }
1017
+ finally {
1018
+ try {
1019
+ if (staging)
1020
+ fs.rmSync(staging, { recursive: true, force: true });
1021
+ }
1022
+ finally {
1023
+ await release();
1024
+ }
1025
+ }
1026
+ }
1027
+ async function buildIndexSnapshot(cwd, options, preparePublication, observation) {
913
1028
  const { force = false, verbose = false, scope, includeIgnored = false, outputDir, graph, explicit = true } = options;
1029
+ const workers = resolveIndexWorkers(options.workers);
914
1030
  const startTime = Date.now();
915
1031
  const log = options.quiet ? (..._args) => { } : console.log;
916
1032
  // Ensure user-defined graphs are loaded
@@ -978,10 +1094,12 @@ export async function buildIndex(cwd, options = {}) {
978
1094
  // effectiveOutputCwd is used for backward-compat loadMetadataIndex calls
979
1095
  const effectiveOutputCwd = outputDir ?? cwd;
980
1096
  if (graph === 'source') {
1097
+ if (observation)
1098
+ observation.before = await graphIndexDigest(cwd, graph, { selection: observation.selection });
981
1099
  const { buildSourceGraphIndex } = await import('./source-graph.js');
982
1100
  await buildSourceGraphIndex({
983
1101
  cwd,
984
- outputDir: indexOutputDir,
1102
+ outputDir: preparePublication(),
985
1103
  effectiveOutputCwd,
986
1104
  verbose,
987
1105
  });
@@ -1000,7 +1118,7 @@ export async function buildIndex(cwd, options = {}) {
1000
1118
  : loadManifest(indexOutputDir);
1001
1119
  const useFilenameMetadata = graphConfig?.nodeStrategy === 'filename-metadata';
1002
1120
  const reusable = !useFilenameMetadata && !graphConfig?.metadataSupplements?.length && !graphConfig?.edgeExtraction;
1003
- const fileStats = !force && reusable ? new Map() : undefined;
1121
+ const fileStats = observation || (!force && reusable) ? new Map() : undefined;
1004
1122
  const nextManifestEntries = {};
1005
1123
  const manifestStats = {
1006
1124
  checked: 0,
@@ -1010,7 +1128,9 @@ export async function buildIndex(cwd, options = {}) {
1010
1128
  pruned: 0,
1011
1129
  };
1012
1130
  // Collect files from all scan directories
1013
- const files = await collectGraphIndexFiles(cwd, graph, { scope, includeIgnored, fileStats });
1131
+ const files = await collectGraphIndexFiles(cwd, graph, { scope, includeIgnored, fileStats, selection: observation?.selection });
1132
+ if (observation)
1133
+ observation.before = await graphIndexDigest(cwd, graph, { files, fileStats });
1014
1134
  const entries = {};
1015
1135
  const tagIndex = {};
1016
1136
  const depGraph = {};
@@ -1051,7 +1171,7 @@ export async function buildIndex(cwd, options = {}) {
1051
1171
  };
1052
1172
  const entriesDigest = (values) => createHash('sha256').update(JSON.stringify(values)).digest('hex');
1053
1173
  const writeBuildManifest = (values) => {
1054
- writeManifest(indexOutputDir, { version: 1, generated: new Date().toISOString(), entries: values,
1174
+ writeManifest(publicationDir, { version: 1, generated: new Date().toISOString(), entries: values,
1055
1175
  ...(reusable ? { receipt: {
1056
1176
  indexVersion: INDEX_VERSION, extractorVersion: INDEX_EXTRACTOR_VERSION, buildConfigHash,
1057
1177
  sourceRoot: path.resolve(cwd), metadataSignature: fileSignature('metadata.json'), entriesDigest: entriesDigest(values),
@@ -1085,11 +1205,16 @@ export async function buildIndex(cwd, options = {}) {
1085
1205
  manifestStats.checked = files.length;
1086
1206
  manifestStats.statSkipped = files.length;
1087
1207
  const buildTimeMs = Date.now() - startTime;
1208
+ // Only stats changes on a receipt hit; its existing atomic writer is
1209
+ // sufficient under the owner lock, without staging or backup copies.
1088
1210
  writeIndexFile(effectiveOutputCwd, 'stats.json', { ...stats, builtAt: new Date().toISOString(), buildTimeMs }, indexOutputDir);
1211
+ if (observation)
1212
+ observation.unchanged = true;
1089
1213
  reportBuild(buildTimeMs, files.length);
1090
1214
  return;
1091
1215
  }
1092
1216
  }
1217
+ const publicationDir = preparePublication();
1093
1218
  const existingIndex = force ? null : outputDir
1094
1219
  ? loadIndexFromDir(indexOutputDir, 'metadata.json')
1095
1220
  : loadGraphIndexFile(effectiveOutputCwd, 'metadata.json', graph);
@@ -1119,7 +1244,11 @@ export async function buildIndex(cwd, options = {}) {
1119
1244
  jobs.push({ fullPath, relativePath, mtimeMs: stat.mtimeMs, birthtimeMs: stat.birthtimeMs, sizeBytes: stat.size,
1120
1245
  ...(!force && existingEntries[relativePath] ? { existingChecksum: existingEntries[relativePath].checksum } : {}) });
1121
1246
  }
1122
- const preparation = await runIndexPreparation(jobs, prepareIndexArtifact, import.meta.url, options.workers);
1247
+ const preparation = await runIndexPreparation(jobs, prepareIndexArtifact, import.meta.url, workers, options.onDiagnostic ?? (message => console.warn(`Index preparation: ${message}`)));
1248
+ for (const result of preparation.results)
1249
+ for (const warning of result.warnings ?? []) {
1250
+ (options.onDiagnostic ?? (message => console.warn(`Index preparation: ${message}`)))(warning);
1251
+ }
1123
1252
  const preparedArtifacts = new Map(jobs.map((job, index) => [job.fullPath, preparation.results[index]]));
1124
1253
  for (const fullPath of files) {
1125
1254
  const relativePath = indexPathFor(cwd, fullPath, graph);
@@ -1220,14 +1349,15 @@ export async function buildIndex(cwd, options = {}) {
1220
1349
  const projectionNeedsRepair = graph === 'framework' && !relationsReceiptMatches();
1221
1350
  if (graph === 'framework') {
1222
1351
  const { writeRelations } = await import('./relations-builder.js');
1223
- await writeRelations(entries, effectiveOutputCwd, indexOutputDir, force || !relationsReceiptMatches());
1352
+ if (force || !relationsReceiptMatches())
1353
+ await writeRelations(entries, effectiveOutputCwd, publicationDir, true);
1224
1354
  }
1225
1355
  const buildTimeMs = Date.now() - startTime;
1226
1356
  writeIndexFile(effectiveOutputCwd, 'stats.json', {
1227
1357
  ...stats,
1228
1358
  builtAt: new Date().toISOString(),
1229
1359
  buildTimeMs,
1230
- }, indexOutputDir);
1360
+ }, publicationDir);
1231
1361
  // Preserve no-op manifest bytes after output-only timestamp drift.
1232
1362
  // Renew receipts only for changed source stats or repaired projections.
1233
1363
  if (manifestStats.checksumSkipped > 0 || projectionNeedsRepair)
@@ -1353,7 +1483,7 @@ export async function buildIndex(cwd, options = {}) {
1353
1483
  }
1354
1484
  if (graph === 'framework') {
1355
1485
  const { writeRelations } = await import('./relations-builder.js');
1356
- await writeRelations(entries, effectiveOutputCwd, indexOutputDir, force || !relationsReceiptMatches());
1486
+ await writeRelations(entries, effectiveOutputCwd, publicationDir, force || !relationsReceiptMatches(), indexOutputDir);
1357
1487
  }
1358
1488
  const buildTimeMs = Date.now() - startTime;
1359
1489
  // Write index files
@@ -1365,13 +1495,13 @@ export async function buildIndex(cwd, options = {}) {
1365
1495
  buildTimeMs,
1366
1496
  entries,
1367
1497
  };
1368
- writeIndexFile(effectiveOutputCwd, 'metadata.json', index, indexOutputDir);
1369
- writeIndexFile(effectiveOutputCwd, 'tags.json', tagIndex, indexOutputDir);
1370
- writeIndexFile(effectiveOutputCwd, 'dependencies.json', depGraph, indexOutputDir);
1498
+ writeIndexFile(effectiveOutputCwd, 'metadata.json', index, publicationDir);
1499
+ writeIndexFile(effectiveOutputCwd, 'tags.json', tagIndex, publicationDir);
1500
+ writeIndexFile(effectiveOutputCwd, 'dependencies.json', depGraph, publicationDir);
1371
1501
  // Materialize the configured backend and always retain dependencies.json as
1372
1502
  // the stable compatibility/export contract.
1373
1503
  const { createGraphBackend } = await import('./graph-backend.js');
1374
- const persistentPath = backendType === 'sqlite' ? path.join(indexOutputDir, 'graph.db') : undefined;
1504
+ const persistentPath = backendType === 'sqlite' ? path.join(publicationDir, 'graph.db') : undefined;
1375
1505
  let selectedBackend;
1376
1506
  try {
1377
1507
  selectedBackend = await createGraphBackend(backendType, persistentPath);
@@ -1429,7 +1559,7 @@ export async function buildIndex(cwd, options = {}) {
1429
1559
  orphanedArtifacts: orphaned,
1430
1560
  mostReferenced,
1431
1561
  },
1432
- }, indexOutputDir);
1562
+ }, publicationDir);
1433
1563
  writeBuildManifest(nextManifestEntries);
1434
1564
  // Report
1435
1565
  reportBuild(buildTimeMs, total);