@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.
- package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +72 -9
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +146 -16
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +91 -26
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +130 -56
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/index-worker.mjs +9 -7
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +40 -6
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +33 -19
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/batch-contracts.js +1 -1
- package/dist/src/sessions/batch-import.js +202 -2
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +73 -54
- package/dist/src/sessions/coverage-preflight.js +26 -1
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/policy.js +18 -7
- package/dist/src/sessions/refresh.js +2 -3
- package/dist/src/sessions/repository.js +4 -2
- package/dist/src/sessions/workspace-discovery.js +92 -26
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- 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
|
-
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
},
|
|
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,
|
|
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,
|
|
1369
|
-
writeIndexFile(effectiveOutputCwd, 'tags.json', tagIndex,
|
|
1370
|
-
writeIndexFile(effectiveOutputCwd, 'dependencies.json', depGraph,
|
|
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(
|
|
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
|
-
},
|
|
1562
|
+
}, publicationDir);
|
|
1433
1563
|
writeBuildManifest(nextManifestEntries);
|
|
1434
1564
|
// Report
|
|
1435
1565
|
reportBuild(buildTimeMs, total);
|