@aiwg/cli 2026.10.1 → 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/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3183 -24
- package/bin/aiwg.mjs +135 -47
- package/dist/src/a2a/codecs.js +9 -0
- 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 +107 -15
- 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/repair.js +9 -4
- 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 +78 -12
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- 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 +218 -5
- package/dist/src/cli/handlers/diagnose.js +13 -5
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/feedback.js +14 -5
- 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 +248 -30
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +169 -10
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +28 -0
- package/dist/src/cli/handlers/regenerate.js +14 -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 +25 -11
- package/dist/src/cli/handlers/script-runner.js +19 -8
- package/dist/src/cli/handlers/session.js +406 -62
- package/dist/src/cli/handlers/sessions.js +243 -76
- package/dist/src/cli/handlers/setup-manifest.js +474 -79
- package/dist/src/cli/handlers/setup.js +180 -39
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +60 -3
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +307 -55
- package/dist/src/cli/handlers/utilities.js +125 -26
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -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 +349 -35
- package/dist/src/config/cli.js +256 -23
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -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-health.mjs +14 -3
- 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 +38 -11
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +14 -7
- 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 +25 -6
- 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 +15 -14
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +942 -0
- 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/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -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/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +64 -18
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +15 -3
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +95 -5
- 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 +366 -56
- 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 +620 -85
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +17 -27
- package/dist/src/mcp/helpers.mjs +109 -4
- 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/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +248 -131
- package/dist/src/mcp/registry.mjs +169 -100
- 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 +127 -82
- 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-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +22 -3
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +50 -2
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +647 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +76 -14
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3183 -24
- package/dist/src/models/model-discovery.js +931 -67
- package/dist/src/models/provider-policy.js +719 -11
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +261 -0
- package/dist/src/models/worker-direct-readiness.js +550 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +144 -0
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +986 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +430 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -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 +53 -23
- 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/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- 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/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +47 -12
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +558 -0
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +49 -0
- 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 +43 -9
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +434 -24
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
- package/dist/src/sessions/adapters/muse.js +200 -104
- 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 +30 -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 +25 -5
- package/dist/src/sessions/batch-import.js +72 -51
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +75 -2
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/grok-build-cli.js +67 -5
- 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 +84 -46
- package/dist/src/sessions/index.js +2 -0
- 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 +199 -63
- package/dist/src/sessions/workspace-discovery.js +315 -88
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- 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 +195 -14
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- 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 +85 -31
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- 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/dist/src/writing/fidelity.js +18 -1
- package/package.json +32 -2
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
- package/tools/_resolve-impl.mjs +39 -2
- package/tools/agents/deploy-agents.mjs +64 -23
- package/tools/agents/providers/base.mjs +153 -166
- 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/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +62 -49
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- 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 +360 -0
- package/tools/providers/omp-transport.mjs +194 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
- 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 +439 -0
- 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 +2916 -0
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as fs from 'fs';
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { spawnSync } from 'child_process';
|
|
24
|
+
import { boundedSpawnSync, positiveTimeoutMs } from '../../process/bounded-child-process.mjs';
|
|
24
25
|
/** Strict-transcription prompt. `title` parameterizes the corpus-specific intro. */
|
|
25
26
|
export function buildPrompt(page, title) {
|
|
26
27
|
const subject = title ? `from ${title}` : 'from a scanned document';
|
|
@@ -53,8 +54,13 @@ const codexAdapter = {
|
|
|
53
54
|
try {
|
|
54
55
|
const r = spawnSync(process.env.AIWG_CODEX_BIN || 'codex', args, {
|
|
55
56
|
input: prompt,
|
|
56
|
-
|
|
57
|
+
// ctx.timeoutSec is caller-controlled (a CLI flag); clamp it so a
|
|
58
|
+
// configured 0 (or anything else non-positive) can't disable the
|
|
59
|
+
// bound entirely (#3062 B/C finding 8). 240s matches this
|
|
60
|
+
// function's own documented default (see extractPages() below).
|
|
61
|
+
timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000),
|
|
57
62
|
encoding: 'utf8',
|
|
63
|
+
killSignal: 'SIGKILL',
|
|
58
64
|
});
|
|
59
65
|
if (r.status === 0 && fs.existsSync(tmp)) {
|
|
60
66
|
const text = fs.readFileSync(tmp, 'utf8');
|
|
@@ -70,15 +76,93 @@ const codexAdapter = {
|
|
|
70
76
|
}
|
|
71
77
|
},
|
|
72
78
|
};
|
|
79
|
+
/** Paths enter the shell as positional arguments, never as shell source. */
|
|
80
|
+
const MAX_VISION_TEMPLATE_LENGTH = 4096;
|
|
81
|
+
function visionCommand(template) {
|
|
82
|
+
const guidance = 'Use "$1"/"$2"/"$3" as data operands or move complex logic to a wrapper script';
|
|
83
|
+
const reject = (construct) => {
|
|
84
|
+
throw new Error(`Vision command rejects ${construct}. ${guidance}`);
|
|
85
|
+
};
|
|
86
|
+
// Bound the checks below; a vision command template is a short command line.
|
|
87
|
+
if (template.length > MAX_VISION_TEMPLATE_LENGTH)
|
|
88
|
+
reject(`templates longer than ${MAX_VISION_TEMPLATE_LENGTH} characters`);
|
|
89
|
+
// A vision template is one command line; a backslash-newline continuation can
|
|
90
|
+
// join an allowed name with following text into a different variable at runtime.
|
|
91
|
+
if (/\\\r?\n|\\\r/.test(template))
|
|
92
|
+
reject('line continuations (backslash-newline)');
|
|
93
|
+
// Recognize literal command names even when quoted or backslash-escaped.
|
|
94
|
+
const syntax = template.replace(/\\([^\n])/g, '$1').replace(/["']/g, '');
|
|
95
|
+
if (syntax.includes('$('))
|
|
96
|
+
reject('command substitution ($(...))');
|
|
97
|
+
if (template.includes('`'))
|
|
98
|
+
reject('backticks');
|
|
99
|
+
if (/\beval\b/.test(syntax))
|
|
100
|
+
reject('eval');
|
|
101
|
+
if (syntax.includes('<<'))
|
|
102
|
+
reject('here-documents and here-strings (<<)');
|
|
103
|
+
// Conservatively reject a shell word together with any short -c option
|
|
104
|
+
// cluster, regardless of order, wrappers, redirections or command boundaries.
|
|
105
|
+
const words = syntax.split(/[\s;&|(){}<>]+/);
|
|
106
|
+
const shellWords = words.map(word => word.slice(word.lastIndexOf('/') + 1)
|
|
107
|
+
.match(/^(?:sh|bash|zsh|dash|ksh|ash|mksh|yash|fish|csh|tcsh|rbash|busybox|toybox)(?:$|(-[A-Za-z]*c))/));
|
|
108
|
+
const hasShell = shellWords.some(Boolean);
|
|
109
|
+
if (shellWords.some(word => word?.[1]) || (hasShell && words.some(word => /^-[A-Za-z]*c/.test(word))))
|
|
110
|
+
reject('nested shell invocation (-c)');
|
|
111
|
+
// Only contract-provided parameters may expand; match at each dollar without
|
|
112
|
+
// rescanning the remaining template. Braces must contain only the exact name.
|
|
113
|
+
const parameter = /\$(?:[123](?![0-9])|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT)(?![A-Za-z0-9_])|\{(?:[123]|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT))\})/y;
|
|
114
|
+
// Track quotes to reject quoted/escaped placeholders, expansions and comments.
|
|
115
|
+
let quote = '';
|
|
116
|
+
return template.replace(/\\?\{(image|prompt_file|out)\}|\\[\s\S]|\$'|[$"'#]/g, (token, name, offset) => {
|
|
117
|
+
if ((token === '#' || token === '\\#') && quote !== "'")
|
|
118
|
+
reject('comments (# outside single quotes)');
|
|
119
|
+
if (name) {
|
|
120
|
+
if (quote)
|
|
121
|
+
throw new Error(`Vision command placeholders must be outside shell quotes. ${guidance}`);
|
|
122
|
+
if (token.startsWith('\\'))
|
|
123
|
+
throw new Error(`Vision command placeholders must not be escaped. ${guidance}`);
|
|
124
|
+
return `"$${name === 'image' ? 1 : name === 'prompt_file' ? 2 : 3}"`;
|
|
125
|
+
}
|
|
126
|
+
if (token === '#')
|
|
127
|
+
return token;
|
|
128
|
+
// Backslashes are literal inside POSIX single quotes, but escape the next
|
|
129
|
+
// character in double quotes and ANSI-C ($'...') quotes.
|
|
130
|
+
if (token.startsWith('\\')) {
|
|
131
|
+
if (quote === "'" && token[1] === "'")
|
|
132
|
+
quote = '';
|
|
133
|
+
return token;
|
|
134
|
+
}
|
|
135
|
+
if (token.startsWith('$')) {
|
|
136
|
+
if (quote !== "'") {
|
|
137
|
+
parameter.lastIndex = offset;
|
|
138
|
+
if (!parameter.test(template))
|
|
139
|
+
reject('parameter expansions other than the provided vision variables');
|
|
140
|
+
}
|
|
141
|
+
if (token === '$')
|
|
142
|
+
return token;
|
|
143
|
+
}
|
|
144
|
+
if (!quote)
|
|
145
|
+
quote = token;
|
|
146
|
+
else if (quote === token || (quote === "$'" && token === "'") || (quote === "'" && token === "$'"))
|
|
147
|
+
quote = '';
|
|
148
|
+
return token;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
73
151
|
/**
|
|
74
152
|
* Generic command-template adapter (provider-neutral). Runs
|
|
75
153
|
* `AIWG_VISION_COMMAND` (or the `--command` flag) with placeholders:
|
|
76
|
-
* {image} absolute image path
|
|
77
|
-
* {prompt_file} path to a file containing the prompt
|
|
78
|
-
* {out} path the command must write the transcription to
|
|
79
|
-
*
|
|
154
|
+
* {image} "$1": absolute image path
|
|
155
|
+
* {prompt_file} "$2": path to a file containing the prompt
|
|
156
|
+
* {out} "$3": path the command must write the transcription to
|
|
157
|
+
* Placeholders must be unquoted and unescaped. AIWG_VISION_IMAGE /
|
|
158
|
+
* AIWG_VISION_PROMPT_FILE / AIWG_VISION_OUT are also exported.
|
|
159
|
+
* Only $1/$2/$3 and the exported vision variables (also braced) may expand.
|
|
160
|
+
* Other $ expansions, command substitution, backticks, eval, nested shells with -c, <<, and #
|
|
161
|
+
* outside single quotes are rejected; use a wrapper script for complex logic.
|
|
162
|
+
* e.g. vision-cli --prompt-file {prompt_file} --image {image} > {out}
|
|
80
163
|
*/
|
|
81
164
|
function commandAdapter(template) {
|
|
165
|
+
const cmd = visionCommand(template);
|
|
82
166
|
return {
|
|
83
167
|
name: 'command',
|
|
84
168
|
transcribe(imagePath, prompt, ctx) {
|
|
@@ -86,12 +170,13 @@ function commandAdapter(template) {
|
|
|
86
170
|
const promptFile = path.join(dir, `.vision-prompt-${ctx.page}.txt`);
|
|
87
171
|
const out = path.join(dir, `.vision-out-${ctx.page}.txt`);
|
|
88
172
|
fs.writeFileSync(promptFile, prompt);
|
|
89
|
-
const cmd = template
|
|
90
|
-
.replaceAll('{image}', JSON.stringify(imagePath))
|
|
91
|
-
.replaceAll('{prompt_file}', JSON.stringify(promptFile))
|
|
92
|
-
.replaceAll('{out}', JSON.stringify(out));
|
|
93
173
|
try {
|
|
94
|
-
|
|
174
|
+
// Same caller-controlled-timeout clamp as the codex adapter above.
|
|
175
|
+
const r = spawnSync('sh', ['-c', cmd, 'aiwg-vision', imagePath, promptFile, out], {
|
|
176
|
+
timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000), encoding: 'utf8',
|
|
177
|
+
env: { ...process.env, AIWG_VISION_IMAGE: imagePath, AIWG_VISION_PROMPT_FILE: promptFile, AIWG_VISION_OUT: out },
|
|
178
|
+
killSignal: 'SIGKILL',
|
|
179
|
+
});
|
|
95
180
|
let text;
|
|
96
181
|
if (fs.existsSync(out))
|
|
97
182
|
text = fs.readFileSync(out, 'utf8');
|
|
@@ -115,7 +200,7 @@ export function resolveAdapter(opts = {}) {
|
|
|
115
200
|
const provider = opts.provider ?? process.env.AIWG_VISION_PROVIDER ?? (command ? 'command' : 'codex');
|
|
116
201
|
if (provider === 'command') {
|
|
117
202
|
if (!command)
|
|
118
|
-
throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with {image}/{prompt_file}/{out})");
|
|
203
|
+
throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with unquoted, unescaped {image}/{prompt_file}/{out} placeholders)");
|
|
119
204
|
return commandAdapter(command);
|
|
120
205
|
}
|
|
121
206
|
if (provider === 'codex')
|
|
@@ -125,7 +210,8 @@ export function resolveAdapter(opts = {}) {
|
|
|
125
210
|
// ── PDF → PNG rasterization (poppler pdftoppm) ───────────────────────────────
|
|
126
211
|
export function pdftoppmAvailable() {
|
|
127
212
|
try {
|
|
128
|
-
return
|
|
213
|
+
return boundedSpawnSync('pdftoppm', ['-v'], 'quick', { encoding: 'utf8' }).status === 0
|
|
214
|
+
|| boundedSpawnSync('pdftoppm', ['-h'], 'quick', { encoding: 'utf8' }).status !== null;
|
|
129
215
|
}
|
|
130
216
|
catch {
|
|
131
217
|
return false;
|
|
@@ -144,9 +230,15 @@ export function rasterizePdf(pdfPath, outDir, dpi = 200) {
|
|
|
144
230
|
fs.mkdirSync(outDir, { recursive: true });
|
|
145
231
|
// pdftoppm writes <prefix>-<n>.png with its own zero-padding based on page count.
|
|
146
232
|
const prefix = path.join(outDir, 'page');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
233
|
+
// Rasterizing a multi-page PDF can take a while; long tier (#3062).
|
|
234
|
+
const r = boundedSpawnSync('pdftoppm', ['-png', '-r', String(dpi), pdfPath, prefix], 'long', { encoding: 'utf8' });
|
|
235
|
+
if (r.status !== 0) {
|
|
236
|
+
// #3062 finding 14: r.error.message (when present) carries the
|
|
237
|
+
// enriched timeout explanation (tier/bound/override) that stderr/
|
|
238
|
+
// status alone do not — put it first, captured stderr after.
|
|
239
|
+
const detail = r.error ? r.error.message : `pdftoppm failed: ${r.stderr || r.status}`;
|
|
240
|
+
throw new Error(r.error && r.stderr ? `${detail} (stderr: ${r.stderr})` : detail);
|
|
241
|
+
}
|
|
150
242
|
// Normalize names to page-NNN.png (3-digit zero-pad).
|
|
151
243
|
let count = 0;
|
|
152
244
|
for (const f of fs.readdirSync(outDir)) {
|
|
@@ -326,11 +326,13 @@ export function loadCorpus(root) {
|
|
|
326
326
|
const venue = classifyFirst(`${venueRaw ?? ''} ${text.slice(0, 3500)}`.toLowerCase(), VENUE_PATTERNS) ||
|
|
327
327
|
(venueRaw ? String(venueRaw).trim() : null);
|
|
328
328
|
const { tier, paramsM } = extractParams(text);
|
|
329
|
+
const radar = extractRadar(root, refId);
|
|
329
330
|
records.push({
|
|
330
331
|
refId,
|
|
331
332
|
sourceType: normalizeSourceType({ type: asStr(fm.type), sourceType: asStr(fm.source_type), venue }, srcTypeReg),
|
|
332
333
|
referencedByProjects: extractReferencedByProjects(text),
|
|
333
334
|
title: extractTitle(text, fm),
|
|
335
|
+
grade: radar?.gradeCurrent ?? asStr(fm.grade),
|
|
334
336
|
path: filePath,
|
|
335
337
|
year: extractYear(text, fm, citation),
|
|
336
338
|
authors,
|
|
@@ -343,7 +345,7 @@ export function loadCorpus(root) {
|
|
|
343
345
|
paramsM,
|
|
344
346
|
incoming,
|
|
345
347
|
outgoing,
|
|
346
|
-
radar
|
|
348
|
+
radar,
|
|
347
349
|
discovery: extractDiscovery(citation),
|
|
348
350
|
funders: extractFunders(citation),
|
|
349
351
|
});
|
|
@@ -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) {
|