@aiwg/cli 2026.10.3 → 2026.10.4
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/README.md +2 -2
- package/agentic/code/providers/model-capabilities.v1.json +96 -4
- package/bin/aiwg.mjs +50 -27
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +19 -2
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +19 -12
- package/dist/src/cli/handlers/diagnose.js +4 -3
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +4 -1
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/models.js +104 -8
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/refresh.js +12 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +1 -2
- package/dist/src/cli/handlers/script-runner.js +3 -2
- package/dist/src/cli/handlers/session.js +19 -9
- package/dist/src/cli/handlers/sessions.js +98 -37
- package/dist/src/cli/handlers/setup-manifest.js +39 -44
- package/dist/src/cli/handlers/setup.js +41 -0
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +57 -2
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +294 -55
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +87 -37
- package/dist/src/config/cli.js +111 -5
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +2 -1
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +13 -8
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +2 -15
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +5 -13
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +131 -28
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/gates/providers/loader.js +61 -17
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +14 -2
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +78 -3
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +7 -1
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +36 -11
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +1 -1
- package/dist/src/mcp/helpers.mjs +3 -2
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/registry.js +3 -93
- package/dist/src/mcp/registry.mjs +3 -87
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +37 -375
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tools/decision.mjs +12 -11
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +12 -2
- package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
- package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
- package/dist/src/mission-protocol/codecs.js +8 -0
- package/dist/src/models/config-loader.js +7 -2
- package/dist/src/models/model-capabilities.v1.json +96 -4
- package/dist/src/models/model-discovery.js +76 -11
- package/dist/src/models/provider-policy.js +101 -7
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/resolver.js +12 -10
- package/dist/src/models/worker-direct-readiness.js +65 -44
- package/dist/src/models/worker-dispatch-offer.js +3 -3
- package/dist/src/models/worker-health-readiness.js +16 -3
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +135 -28
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +43 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +12 -4
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/mission-conductor.js +38 -12
- package/dist/src/serve/worker-invocation.js +30 -10
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/sessions/adapter-registry.js +23 -19
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +10 -10
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot.js +24 -13
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
- package/dist/src/sessions/adapters/muse.js +150 -93
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +29 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +16 -8
- package/dist/src/sessions/batch-import.js +51 -11
- package/dist/src/sessions/contracts.js +40 -2
- package/dist/src/sessions/coverage-preflight.js +1 -1
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +60 -38
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +147 -52
- package/dist/src/sessions/workspace-discovery.js +221 -56
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/package.json +15 -2
- package/schemas/models/model-catalog.v1.schema.json +1 -1
- package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
- package/tools/_resolve-impl.mjs +13 -15
- package/tools/agents/deploy-agents.mjs +49 -21
- package/tools/agents/providers/base.mjs +104 -134
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/hermes.mjs +57 -47
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +6 -4
- package/tools/providers/omp-transport.mjs +4 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
- package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
- package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
|
@@ -242,9 +242,9 @@ function renderPipeline(ctx) {
|
|
|
242
242
|
// Cadence → window math lives in corpus-config.ts (the one staleness helper, #1498/#1502).
|
|
243
243
|
/** GRADE display order; unknown grades fall after, 'Ungraded' last. */
|
|
244
244
|
const GRADE_ORDER = ['A', 'A-', 'B', 'B-', 'C', 'C-', 'D'];
|
|
245
|
-
/** Papers grouped by current GRADE
|
|
245
|
+
/** Papers grouped by current GRADE, falling back to reference frontmatter. */
|
|
246
246
|
function renderByGrade(ctx) {
|
|
247
|
-
const groups = groupBy(ctx.records, (r) => [r.
|
|
247
|
+
const groups = groupBy(ctx.records, (r) => [r.grade || 'Ungraded']);
|
|
248
248
|
const lines = header('Index: Papers by GRADE', ctx, ctx.records.length);
|
|
249
249
|
const names = [...groups.keys()].sort((a, b) => {
|
|
250
250
|
if (a === 'Ungraded')
|
|
@@ -284,7 +284,7 @@ function renderStaleQueue(ctx) {
|
|
|
284
284
|
const lines = header('Index: Radar Stale-Refresh Queue', ctx, ctx.records.length);
|
|
285
285
|
lines.push(`Overdue = days since last refresh minus the cadence window. ${staleCount} of ${rows.length} radar-tracked papers are overdue.`, '', '| REF | Title | GRADE | Cadence | Last Refreshed | Overdue (days) | Stale |', '|---|---|---|---|---|---:|---|');
|
|
286
286
|
for (const { r, cadence, last, overdue } of sorted) {
|
|
287
|
-
lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.
|
|
287
|
+
lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.grade ?? '—'} | ${cadence} | ${last} | ${overdue} | ${overdue > 0 ? 'yes' : 'no'} |`);
|
|
288
288
|
}
|
|
289
289
|
return lines.join('\n') + '\n';
|
|
290
290
|
}
|
|
@@ -2,7 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { performance } from 'node:perf_hooks';
|
|
5
|
-
import { OPERATIONAL_DISCOVERY_TYPES } from './types.js';
|
|
5
|
+
import { getGraphIndexDir, OPERATIONAL_DISCOVERY_TYPES } from './types.js';
|
|
6
|
+
import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
|
|
6
7
|
import { loadGraphIndexFile } from './index-reader.js';
|
|
7
8
|
import { loadFortemiCoreExport, loadFortemiCoreMetadataEntries, scoreStaticRecord } from './fortemi-core-query-adapter.js';
|
|
8
9
|
import { discoverCapability } from './query-engine.js';
|
|
@@ -185,8 +186,9 @@ export function calculateDiscoveryMetrics(records, resultSets) {
|
|
|
185
186
|
.some((item) => record.hard_negative_ids.some((id) => matches(item, id)))).length / records.length),
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
|
-
function indexBytes(backend) {
|
|
189
|
-
const root =
|
|
189
|
+
function indexBytes(cwd, backend) {
|
|
190
|
+
const root = backend === 'local' ? getGraphIndexDir(cwd, 'framework')
|
|
191
|
+
: path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', 'framework');
|
|
190
192
|
let total = 0;
|
|
191
193
|
const visit = (target) => {
|
|
192
194
|
if (!fs.existsSync(target))
|
|
@@ -259,7 +261,7 @@ export async function evaluateDiscovery(options) {
|
|
|
259
261
|
configuration: { backend: options.backend, strategy: options.strategy, limit, graph: 'framework' },
|
|
260
262
|
hardware: { platform: os.platform(), arch: os.arch(), cpu_model: cpus[0]?.model ?? 'unknown', logical_cpus: cpus.length, total_memory_bytes: os.totalmem(), node: process.version },
|
|
261
263
|
metrics,
|
|
262
|
-
performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.backend), peak_resident_memory_bytes: peakRss },
|
|
264
|
+
performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.cwd, options.backend), peak_resident_memory_bytes: peakRss },
|
|
263
265
|
...(parity ? { parity } : {}),
|
|
264
266
|
adoption_gate: {
|
|
265
267
|
baseline: 'local:lexical', no_per_type_hit_at_3_regression: null, aggregate_mrr_improvement: null,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { selectPrebuiltRelations } from './relations.js';
|
|
2
|
+
import { resolveFrameworkIndexRoot } from './framework-index-paths.js';
|
|
3
|
+
import { extractRuleBindings } from './index-builder.js';
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import { createHash } from "node:crypto";
|
|
3
6
|
import { bm25Rank } from "./fulltext.js";
|
|
4
|
-
import { getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
|
|
7
|
+
import { consumeFortemiCoreExport, getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
|
|
8
|
+
import { loadGraphIndexFile } from "./index-reader.js";
|
|
5
9
|
import { operationalStateQueryProjection, } from "./operational-state.js";
|
|
6
10
|
function tokenize(text) {
|
|
7
11
|
return text
|
|
@@ -167,6 +171,14 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
|
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
173
|
}
|
|
174
|
+
// A content-validated shipped corpus does not need the source metadata index.
|
|
175
|
+
// This also avoids preferring a much larger locally materialized text cache.
|
|
176
|
+
if (graph === 'framework' && selectPrebuiltRelations(cwd).reader) {
|
|
177
|
+
const prebuilt = getFortemiCorePrebuiltStatus(graph, resolveFrameworkIndexRoot(cwd));
|
|
178
|
+
if (prebuilt.built && !prebuilt.stale) {
|
|
179
|
+
return { exported: JSON.parse(fs.readFileSync(prebuilt.exportPath, 'utf8')) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
170
182
|
let status = getFortemiCoreSyncStatus(cwd, graph);
|
|
171
183
|
// The packaged framework index is a bootstrap fallback for an absent shared
|
|
172
184
|
// cache. Once a shared cache exists, its stale state proves that the local
|
|
@@ -178,6 +190,20 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
|
|
|
178
190
|
status = prebuilt;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
193
|
+
// A packaged bootstrap cache can also predate a source checkout's index.
|
|
194
|
+
// Validate it against the same current data as the shared cache before use.
|
|
195
|
+
if (status.source === "prebuilt") {
|
|
196
|
+
const current = loadGraphIndexFile(cwd, "metadata.json", graph);
|
|
197
|
+
if (current &&
|
|
198
|
+
Date.parse(current.builtAt) > Date.parse(status.sourceIndexBuiltAt ?? status.generatedAt ?? "")) {
|
|
199
|
+
status = {
|
|
200
|
+
...status,
|
|
201
|
+
stale: true,
|
|
202
|
+
sourceIndexNewer: true,
|
|
203
|
+
reason: "source index is newer than the Fortemi Core static cache",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
181
207
|
if (!status.optedIn) {
|
|
182
208
|
return {
|
|
183
209
|
reason: `Fortemi Core static index is not materialized for graph '${graph}'. Run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' first, or pass '--backend local' to use the legacy local index.`,
|
|
@@ -185,12 +211,13 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
|
|
|
185
211
|
}
|
|
186
212
|
if (!status.built || status.stale) {
|
|
187
213
|
return {
|
|
214
|
+
...(status.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
|
|
188
215
|
reason: `${status.reason ??
|
|
189
216
|
`Fortemi Core static index for graph '${graph}' is stale or incomplete`}. Re-run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' or pass '--backend local' to use the legacy local index.`,
|
|
190
217
|
};
|
|
191
218
|
}
|
|
192
219
|
try {
|
|
193
|
-
const exported =
|
|
220
|
+
const exported = consumeFortemiCoreExport(status.exportPath, status.exportChecksum);
|
|
194
221
|
if (exported.schema_version !== "aiwg.fortemi.index.export.v2") {
|
|
195
222
|
return {
|
|
196
223
|
reason: `Fortemi Core static index has schema '${exported.schema_version}', expected 'aiwg.fortemi.index.export.v2'. Re-run 'aiwg index sync'.`,
|
|
@@ -255,13 +282,19 @@ function entryFromRecord(record) {
|
|
|
255
282
|
&& typeof indexedScript.runtime === "string"
|
|
256
283
|
? indexedScript
|
|
257
284
|
: undefined,
|
|
285
|
+
...extractRuleBindings(indexedFrontmatter),
|
|
286
|
+
...(Array.isArray(indexedFrontmatter.aiwg_governed_paths) ? { governedPaths: indexedFrontmatter.aiwg_governed_paths.filter((value) => typeof value === "string") } : {}),
|
|
258
287
|
operationalState: record.operational_state,
|
|
259
288
|
};
|
|
260
289
|
}
|
|
261
290
|
export function loadFortemiCoreMetadataEntries(cwd, graph = "project", sourcePaths) {
|
|
262
291
|
const loaded = loadFortemiCoreExport(cwd, graph, sourcePaths);
|
|
263
292
|
if (!loaded.exported)
|
|
264
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
entries: [],
|
|
295
|
+
reason: loaded.reason,
|
|
296
|
+
...(loaded.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
|
|
297
|
+
};
|
|
265
298
|
return { entries: loaded.exported.items.map(entryFromRecord) };
|
|
266
299
|
}
|
|
267
300
|
export async function queryFortemiCoreAiwgDiscovery(cwd, options) {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parse as parseYaml } from "yaml";
|
|
6
6
|
import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
|
|
7
7
|
import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
|
|
8
8
|
import { loadGraphIndexFile } from "./index-reader.js";
|
|
9
|
+
import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
|
|
9
10
|
function syncDir(cwd, graph) {
|
|
10
11
|
loadGlobalGraphConfigs();
|
|
12
|
+
if (graph === 'framework')
|
|
13
|
+
return path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', graph);
|
|
11
14
|
const config = GRAPH_CONFIGS[graph];
|
|
12
|
-
if (
|
|
15
|
+
if (config?.shared) {
|
|
13
16
|
const xdgData = process.env.XDG_DATA_HOME ?? path.join(process.env.HOME ?? cwd, ".local", "share");
|
|
14
17
|
return path.join(xdgData, "aiwg", "index", "fortemi-core", graph);
|
|
15
18
|
}
|
|
@@ -36,6 +39,47 @@ function prebuiltDir(graph) {
|
|
|
36
39
|
function sha256(text) {
|
|
37
40
|
return createHash("sha256").update(text).digest("hex");
|
|
38
41
|
}
|
|
42
|
+
// Status checks and the query reader often inspect the same large export.
|
|
43
|
+
// Keep at most two private snapshots, and never retain an object exposed to a
|
|
44
|
+
// caller: query results have always been fresh, mutable objects.
|
|
45
|
+
const exportSnapshots = new Map();
|
|
46
|
+
function exportSignature(file) {
|
|
47
|
+
const stat = fs.statSync(file, { bigint: true });
|
|
48
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
|
|
49
|
+
}
|
|
50
|
+
function readExportSnapshot(file, expectedChecksum) {
|
|
51
|
+
const signature = exportSignature(file);
|
|
52
|
+
const cached = exportSnapshots.get(file);
|
|
53
|
+
// The manifest checksum is part of the snapshot identity. Filesystems may
|
|
54
|
+
// preserve every stat field across a same-size rewrite within one timestamp
|
|
55
|
+
// tick, while the manifest already names the replacement's checksum.
|
|
56
|
+
if (cached?.signature === signature &&
|
|
57
|
+
cached.checksum === expectedChecksum)
|
|
58
|
+
return cached;
|
|
59
|
+
exportSnapshots.delete(file);
|
|
60
|
+
const text = fs.readFileSync(file, "utf-8");
|
|
61
|
+
const exported = JSON.parse(text);
|
|
62
|
+
const snapshot = { signature, exported, checksum: sha256(text) };
|
|
63
|
+
// A concurrent replacement cannot publish a snapshot under the wrong stamp.
|
|
64
|
+
if (exportSignature(file) === signature) {
|
|
65
|
+
exportSnapshots.set(file, snapshot);
|
|
66
|
+
while (exportSnapshots.size > 2)
|
|
67
|
+
exportSnapshots.delete(exportSnapshots.keys().next().value);
|
|
68
|
+
}
|
|
69
|
+
return snapshot;
|
|
70
|
+
}
|
|
71
|
+
/** Transfer a checked snapshot to the query reader without sharing mutable results. */
|
|
72
|
+
export function consumeFortemiCoreExport(file, expectedChecksum) {
|
|
73
|
+
try {
|
|
74
|
+
const snapshot = readExportSnapshot(file, expectedChecksum);
|
|
75
|
+
if (snapshot.checksum !== expectedChecksum)
|
|
76
|
+
throw new Error("export checksum does not match manifest");
|
|
77
|
+
return snapshot.exported;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
exportSnapshots.delete(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
39
83
|
function readManifest(manifestPath) {
|
|
40
84
|
return readManifestResult(manifestPath).manifest;
|
|
41
85
|
}
|
|
@@ -59,11 +103,91 @@ function sourceIndexBuiltAt(cwd, graph) {
|
|
|
59
103
|
const index = loadGraphIndexFile(cwd, "metadata.json", graph);
|
|
60
104
|
return typeof index?.builtAt === "string" ? index.builtAt : null;
|
|
61
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Emit the v2 JSON document without stringifying a collection or a whole record.
|
|
108
|
+
* Strings are escaped in bounded slices too: a single large text field must not
|
|
109
|
+
* reintroduce V8's string limit. Preserve pretty-print bytes for stable checksums.
|
|
110
|
+
*/
|
|
111
|
+
function* jsonTokens(value, maxChars, depth = 0, ancestors = new Set()) {
|
|
112
|
+
if (value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function") {
|
|
113
|
+
value = value.toJSON();
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
yield '"';
|
|
117
|
+
const sliceChars = Math.floor(maxChars / 6); // A control character can expand to \uXXXX.
|
|
118
|
+
for (let start = 0; start < value.length;) {
|
|
119
|
+
let end = Math.min(start + sliceChars, value.length);
|
|
120
|
+
// Keep surrogate pairs together, including when UTF-8 encoding each write.
|
|
121
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]) && /[\uDC00-\uDFFF]/.test(value[end]))
|
|
122
|
+
end--;
|
|
123
|
+
yield JSON.stringify(value.slice(start, end)).slice(1, -1);
|
|
124
|
+
start = end;
|
|
125
|
+
}
|
|
126
|
+
yield '"';
|
|
127
|
+
}
|
|
128
|
+
else if (value && typeof value === "object") {
|
|
129
|
+
if (ancestors.has(value))
|
|
130
|
+
throw new TypeError("Converting circular structure to JSON");
|
|
131
|
+
ancestors.add(value);
|
|
132
|
+
const array = Array.isArray(value);
|
|
133
|
+
const entries = Array.isArray(value) ? value.map((item, index) => [String(index), item])
|
|
134
|
+
: Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
|
|
135
|
+
yield array ? "[" : "{";
|
|
136
|
+
for (let i = 0; i < entries.length; i++) {
|
|
137
|
+
yield (i === 0 ? "\n" : ",\n") + " ".repeat(depth + 1);
|
|
138
|
+
const [key, item] = entries[i];
|
|
139
|
+
if (!array) {
|
|
140
|
+
yield* jsonTokens(key, maxChars, depth + 1, ancestors);
|
|
141
|
+
yield ": ";
|
|
142
|
+
}
|
|
143
|
+
yield* jsonTokens(item, maxChars, depth + 1, ancestors);
|
|
144
|
+
}
|
|
145
|
+
if (entries.length)
|
|
146
|
+
yield "\n" + " ".repeat(depth);
|
|
147
|
+
yield array ? "]" : "}";
|
|
148
|
+
ancestors.delete(value);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
yield JSON.stringify(value) ?? "null";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Write and hash the same bounded chunks; publish only complete exports. */
|
|
155
|
+
function writeExport(file, exported, maxChars) {
|
|
156
|
+
const hash = createHash("sha256");
|
|
157
|
+
const fd = fs.openSync(file, "w");
|
|
158
|
+
try {
|
|
159
|
+
const flush = (chunk) => {
|
|
160
|
+
const bytes = Buffer.from(chunk, "utf-8");
|
|
161
|
+
hash.update(bytes);
|
|
162
|
+
for (let offset = 0; offset < bytes.length;)
|
|
163
|
+
offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
164
|
+
};
|
|
165
|
+
let pending = "";
|
|
166
|
+
for (const token of jsonTokens(exported, maxChars)) {
|
|
167
|
+
if (pending.length + token.length > maxChars) {
|
|
168
|
+
flush(pending);
|
|
169
|
+
pending = "";
|
|
170
|
+
}
|
|
171
|
+
pending += token;
|
|
172
|
+
}
|
|
173
|
+
flush(pending);
|
|
174
|
+
flush("\n");
|
|
175
|
+
return hash.digest("hex");
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
fs.closeSync(fd);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
62
181
|
export function syncFortemiCoreIndex(cwd, options = {}) {
|
|
63
182
|
const graph = options.graph ?? "project";
|
|
64
183
|
const dir = syncDir(cwd, graph);
|
|
65
184
|
const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
|
|
66
185
|
const manifestPath = path.join(dir, "manifest.json");
|
|
186
|
+
const maxChars = options.maxChunkChars ?? 64 * 1024;
|
|
187
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 64)
|
|
188
|
+
throw new Error("maxChunkChars must be an integer of at least 64");
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
const temporaryPath = path.join(dir, `.export-${randomUUID()}.tmp`);
|
|
67
191
|
const previous = readManifest(manifestPath);
|
|
68
192
|
const sourceBuiltAt = sourceIndexBuiltAt(cwd, graph);
|
|
69
193
|
const buildExport = (generatedAt) => {
|
|
@@ -74,12 +198,11 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
|
|
|
74
198
|
generatedAt,
|
|
75
199
|
schemaVersion: "v2",
|
|
76
200
|
});
|
|
77
|
-
const
|
|
201
|
+
const checksum = writeExport(temporaryPath, exported, maxChars);
|
|
78
202
|
const recordsWithConcepts = exported.items.filter((item) => (item.skos_concepts?.length ?? 0) > 0).length;
|
|
79
203
|
return {
|
|
80
204
|
generatedAt,
|
|
81
|
-
|
|
82
|
-
checksum: sha256(json),
|
|
205
|
+
checksum,
|
|
83
206
|
itemCount: exported.items.length,
|
|
84
207
|
skosCoverage: {
|
|
85
208
|
records_with_concepts: recordsWithConcepts,
|
|
@@ -91,36 +214,40 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
|
|
|
91
214
|
},
|
|
92
215
|
};
|
|
93
216
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (previous
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
217
|
+
try {
|
|
218
|
+
let built = buildExport(options.generatedAt ?? previous?.generated_at ?? new Date().toISOString());
|
|
219
|
+
let status = "created";
|
|
220
|
+
if (previous) {
|
|
221
|
+
if (previous.export_checksum === built.checksum) {
|
|
222
|
+
status = "unchanged";
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
status = "updated";
|
|
226
|
+
if (options.generatedAt === undefined) {
|
|
227
|
+
built = buildExport(new Date().toISOString());
|
|
228
|
+
}
|
|
104
229
|
}
|
|
105
230
|
}
|
|
231
|
+
fs.renameSync(temporaryPath, exportPath);
|
|
232
|
+
const manifest = {
|
|
233
|
+
schema_version: "aiwg.fortemi.core-sync.v1",
|
|
234
|
+
backend: "fortemi-core",
|
|
235
|
+
graph,
|
|
236
|
+
generated_at: built.generatedAt,
|
|
237
|
+
export_path: path.relative(cwd, exportPath),
|
|
238
|
+
export_schema_version: "aiwg.fortemi.index.export.v2",
|
|
239
|
+
export_checksum: built.checksum,
|
|
240
|
+
item_count: built.itemCount,
|
|
241
|
+
skos_coverage: built.skosCoverage,
|
|
242
|
+
status,
|
|
243
|
+
source_index_built_at: sourceBuiltAt,
|
|
244
|
+
};
|
|
245
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
246
|
+
return manifest;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
106
250
|
}
|
|
107
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
-
fs.writeFileSync(exportPath, built.json, "utf-8");
|
|
109
|
-
const manifest = {
|
|
110
|
-
schema_version: "aiwg.fortemi.core-sync.v1",
|
|
111
|
-
backend: "fortemi-core",
|
|
112
|
-
graph,
|
|
113
|
-
generated_at: built.generatedAt,
|
|
114
|
-
export_path: path.relative(cwd, exportPath),
|
|
115
|
-
export_schema_version: "aiwg.fortemi.index.export.v2",
|
|
116
|
-
export_checksum: built.checksum,
|
|
117
|
-
item_count: built.itemCount,
|
|
118
|
-
skos_coverage: built.skosCoverage,
|
|
119
|
-
status,
|
|
120
|
-
source_index_built_at: sourceBuiltAt,
|
|
121
|
-
};
|
|
122
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
123
|
-
return manifest;
|
|
124
251
|
}
|
|
125
252
|
export function getFortemiCoreSyncStatus(cwd, graph = "project") {
|
|
126
253
|
const dir = syncDir(cwd, graph);
|
|
@@ -149,9 +276,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
|
|
|
149
276
|
let exportChecksumMatches = false;
|
|
150
277
|
if (exportExists) {
|
|
151
278
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
exportChecksumMatches = sha256(exportText) === manifest.export_checksum;
|
|
279
|
+
const { exported, checksum } = readExportSnapshot(exportPath, manifest.export_checksum);
|
|
280
|
+
exportChecksumMatches = checksum === manifest.export_checksum;
|
|
155
281
|
if (exportChecksumMatches) {
|
|
156
282
|
if (exported.schema_version !== manifest.export_schema_version) {
|
|
157
283
|
exportReadReason = `export schema '${exported.schema_version}' does not match manifest '${manifest.export_schema_version}'`;
|
|
@@ -182,6 +308,7 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
|
|
|
182
308
|
exportPath,
|
|
183
309
|
built: exportExists,
|
|
184
310
|
stale,
|
|
311
|
+
...(sourceIsNewer ? { sourceIndexNewer: true } : {}),
|
|
185
312
|
itemCount: manifest.item_count,
|
|
186
313
|
exportChecksum: manifest.export_checksum,
|
|
187
314
|
generatedAt: manifest.generated_at,
|
|
@@ -195,8 +322,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
|
|
|
195
322
|
: null,
|
|
196
323
|
};
|
|
197
324
|
}
|
|
198
|
-
export function getFortemiCorePrebuiltStatus(graph = "framework") {
|
|
199
|
-
const dir = prebuiltDir(graph) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
|
|
325
|
+
export function getFortemiCorePrebuiltStatus(graph = "framework", packageRoot) {
|
|
326
|
+
const dir = (packageRoot ? path.join(packageRoot, "prebuilt", "fortemi-core", graph) : prebuiltDir(graph)) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
|
|
200
327
|
const manifestPath = path.join(dir, "manifest.json");
|
|
201
328
|
const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
|
|
202
329
|
const { manifest, reason: manifestReadReason } = readManifestResult(manifestPath);
|
|
@@ -222,8 +349,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
|
|
|
222
349
|
let reason = null;
|
|
223
350
|
if (exportExists) {
|
|
224
351
|
try {
|
|
225
|
-
const
|
|
226
|
-
const exported = JSON.parse(exportText);
|
|
352
|
+
const { exported, checksum } = readExportSnapshot(exportPath, prebuiltManifest.export_checksum);
|
|
227
353
|
if (prebuiltManifest.schema_version !== "aiwg.fortemi.prebuilt.v1" ||
|
|
228
354
|
prebuiltManifest.backend !== "fortemi-core" ||
|
|
229
355
|
prebuiltManifest.graph !== graph ||
|
|
@@ -231,7 +357,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
|
|
|
231
357
|
prebuiltManifest.export_schema_version !== "aiwg.fortemi.index.export.v2") {
|
|
232
358
|
reason = "prebuilt manifest is incompatible with the requested graph";
|
|
233
359
|
}
|
|
234
|
-
else if (
|
|
360
|
+
else if (checksum !== prebuiltManifest.export_checksum) {
|
|
235
361
|
reason = "prebuilt export checksum does not match manifest";
|
|
236
362
|
}
|
|
237
363
|
else if (exported.schema_version !== prebuiltManifest.export_schema_version) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { getGraphIndexDir } from './types.js';
|
|
5
|
+
import { frameworkSourceDigest } from './framework-source-digest.js';
|
|
6
|
+
import { acquireDirectoryLock, DirectoryLockContentionError } from './prebuilt-build-lock.js';
|
|
7
|
+
import { FRAMEWORK_CORPUS_KINDS as CORPUS_KINDS, resolveFrameworkCorpusRoot } from './framework-index-paths.js';
|
|
8
|
+
export { resolveFrameworkCorpusRoot } from './framework-index-paths.js';
|
|
9
|
+
function readText(file) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(file, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Resolve HEAD without spawning git, including worktrees and packed refs. */
|
|
18
|
+
function gitHead(root) {
|
|
19
|
+
let gitDir = path.join(root, '.git');
|
|
20
|
+
const pointer = readText(gitDir).trim();
|
|
21
|
+
if (pointer.startsWith('gitdir: '))
|
|
22
|
+
gitDir = path.resolve(root, pointer.slice(8));
|
|
23
|
+
const head = readText(path.join(gitDir, 'HEAD')).trim();
|
|
24
|
+
if (!head.startsWith('ref: '))
|
|
25
|
+
return head;
|
|
26
|
+
const ref = head.slice(5);
|
|
27
|
+
const common = readText(path.join(gitDir, 'commondir')).trim();
|
|
28
|
+
const commonDir = common ? path.resolve(gitDir, common) : gitDir;
|
|
29
|
+
return readText(path.join(gitDir, ref)).trim()
|
|
30
|
+
|| readText(path.join(commonDir, ref)).trim()
|
|
31
|
+
|| readText(path.join(commonDir, 'packed-refs')).split('\n').find(line => line.endsWith(` ${ref}`))
|
|
32
|
+
|| head;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Root/version identity plus the portable corpus digest. Its per-root stat
|
|
36
|
+
* receipt avoids rereading unchanged source contents and detects uncommitted
|
|
37
|
+
* edits as well as manifest/package/HEAD changes.
|
|
38
|
+
*/
|
|
39
|
+
function sourceFingerprint(root) {
|
|
40
|
+
const hash = createHash('sha256').update('framework-capabilities-v2');
|
|
41
|
+
hash.update(path.resolve(root));
|
|
42
|
+
try {
|
|
43
|
+
hash.update(JSON.parse(readText(path.join(root, 'package.json'))).version ?? '');
|
|
44
|
+
}
|
|
45
|
+
catch { /* fixture root */ }
|
|
46
|
+
hash.update(gitHead(root));
|
|
47
|
+
hash.update(frameworkSourceDigest(root));
|
|
48
|
+
for (const kind of CORPUS_KINDS) {
|
|
49
|
+
const dir = path.join(root, 'agentic/code', kind);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
hash.update(kind).update(readText(path.join(dir, 'manifest.json')));
|
|
58
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
59
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
60
|
+
continue;
|
|
61
|
+
hash.update(entry.name).update(readText(path.join(dir, entry.name, 'manifest.json')));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return hash.digest('hex');
|
|
65
|
+
}
|
|
66
|
+
/** Stat signatures also notice manual rebuilds, deleted caches, and replacement indexes. */
|
|
67
|
+
function fileSignature(file) {
|
|
68
|
+
try {
|
|
69
|
+
const stat = fs.statSync(file, { bigint: true });
|
|
70
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readReceipt(file) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readText(file));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reconcile only when a cheap source/index receipt changes. Contenders serve an
|
|
86
|
+
* existing index after a short wait; cold starts wait for the first builder.
|
|
87
|
+
* The shared directory lock recovers owners proven dead on this host.
|
|
88
|
+
*/
|
|
89
|
+
export async function refreshFrameworkCapabilities(root, syncFortemi = true, options = {}) {
|
|
90
|
+
const corpusRoot = resolveFrameworkCorpusRoot(root);
|
|
91
|
+
if (!corpusRoot)
|
|
92
|
+
return;
|
|
93
|
+
root = corpusRoot;
|
|
94
|
+
const dir = getGraphIndexDir(root, 'framework');
|
|
95
|
+
const receiptPath = path.join(dir, 'capability-sources.json');
|
|
96
|
+
const metadataPath = path.join(dir, 'metadata.json');
|
|
97
|
+
const fortemiDir = path.join(path.dirname(dir), 'fortemi-core', 'framework');
|
|
98
|
+
const fortemiSignature = () => [
|
|
99
|
+
'manifest.json', 'aiwg-fortemi-index-v2.json',
|
|
100
|
+
].map(file => fileSignature(path.join(fortemiDir, file))).join('|');
|
|
101
|
+
const source = sourceFingerprint(root);
|
|
102
|
+
const current = () => ({
|
|
103
|
+
source, index: fileSignature(metadataPath), relations: fileSignature(path.join(dir, 'relations.json')), ...(syncFortemi ? { fortemi: fortemiSignature() } : {}),
|
|
104
|
+
});
|
|
105
|
+
const matches = (receipt, probe) => Boolean(!options.force && receipt && probe.index && probe.relations && receipt.relations === probe.relations && receipt.source === probe.source
|
|
106
|
+
&& receipt.index === probe.index && (!syncFortemi || receipt.fortemi === probe.fortemi));
|
|
107
|
+
if (matches(readReceipt(receiptPath), current()))
|
|
108
|
+
return;
|
|
109
|
+
let release;
|
|
110
|
+
try {
|
|
111
|
+
release = await acquireDirectoryLock(path.join(dir, 'capability-refresh.lock'), {
|
|
112
|
+
timeoutMs: fs.existsSync(metadataPath) ? 150 : 60_000,
|
|
113
|
+
pollMs: 25,
|
|
114
|
+
reclaimDeadOwner: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof DirectoryLockContentionError && fs.existsSync(metadataPath))
|
|
119
|
+
return;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Another process may have completed reconciliation while we waited.
|
|
124
|
+
const receipt = readReceipt(receiptPath);
|
|
125
|
+
const probe = current();
|
|
126
|
+
if (matches(receipt, probe))
|
|
127
|
+
return;
|
|
128
|
+
if (options.force || !receipt || !probe.index || receipt.source !== source || receipt.index !== probe.index || !probe.relations || receipt.relations !== probe.relations) {
|
|
129
|
+
const { buildIndex } = await import('./index-builder.js');
|
|
130
|
+
await buildIndex(root, { graph: 'framework', explicit: false, quiet: true });
|
|
131
|
+
}
|
|
132
|
+
// A forced source scan can be a no-op. The prior successful receipt still
|
|
133
|
+
// validates the exact index/export files, so avoid hashing the export again.
|
|
134
|
+
const updated = current();
|
|
135
|
+
const cacheUnchanged = receipt && updated.index && receipt.source === source
|
|
136
|
+
&& receipt.index === updated.index && receipt.fortemi === updated.fortemi;
|
|
137
|
+
if (syncFortemi && !cacheUnchanged) {
|
|
138
|
+
const { getFortemiCoreSyncStatus, syncFortemiCoreIndex } = await import('./fortemi-core-sync.js');
|
|
139
|
+
const status = getFortemiCoreSyncStatus(root, 'framework');
|
|
140
|
+
const indexChanged = receipt?.index ? receipt.index !== updated.index : probe.index !== updated.index;
|
|
141
|
+
if (indexChanged || !status.built || status.stale)
|
|
142
|
+
syncFortemiCoreIndex(root, { graph: 'framework' });
|
|
143
|
+
}
|
|
144
|
+
// Publish only after success. A failed rebuild/sync is retried next call.
|
|
145
|
+
const temp = `${receiptPath}.${process.pid}.tmp`;
|
|
146
|
+
fs.writeFileSync(temp, JSON.stringify(current()));
|
|
147
|
+
fs.renameSync(temp, receiptPath);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await release();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Installation is recorded in project config, independently of index coverage. */
|
|
154
|
+
export async function isCorpusBundleInstalled(cwd, root, kind, id) {
|
|
155
|
+
if (!kind || !id)
|
|
156
|
+
return false;
|
|
157
|
+
const { readAiwgConfigCandidate } = await import('../config/aiwg-config.js');
|
|
158
|
+
const config = await readAiwgConfigCandidate(cwd);
|
|
159
|
+
if (!config)
|
|
160
|
+
return false;
|
|
161
|
+
if (config.installed[id] || (kind === 'framework' && config.installed.all))
|
|
162
|
+
return true;
|
|
163
|
+
try {
|
|
164
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'agentic/code', `${kind}s`, id, 'manifest.json'), 'utf8'));
|
|
165
|
+
return (manifest.modeAliases ?? []).some(alias => Boolean(config.installed[alias]));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=framework-capabilities.js.map
|