carthagent 0.0.0-stage → 1.0.7
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/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const ENTRY_KINDS = new Set(['credit_grant', 'reservation', 'charge', 'reservation_release', 'refund', 'adjustment']);
|
|
7
|
+
const FORBIDDEN_METADATA = /prompt|response|source|code|tool|argument|content|message/i;
|
|
8
|
+
|
|
9
|
+
export function nanoUsd(value, name = 'amountNanoUsd') {
|
|
10
|
+
if (!Number.isSafeInteger(value)) throw new TypeError(`${name} must be a safe integer nano-USD amount`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateFinancialMetadata(metadata = {}) {
|
|
15
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) throw new TypeError('metadata must be an object');
|
|
16
|
+
const clean = {};
|
|
17
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
18
|
+
if (FORBIDDEN_METADATA.test(key)) throw new Error(`financial metadata must not contain customer content (${key})`);
|
|
19
|
+
if (!['string', 'number', 'boolean'].includes(typeof value) && value !== null) throw new TypeError(`financial metadata value ${key} must be scalar`);
|
|
20
|
+
clean[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return clean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function openLedger(path = ':memory:') {
|
|
26
|
+
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
27
|
+
const db = new DatabaseSync(path);
|
|
28
|
+
db.exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;');
|
|
29
|
+
db.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
created_at TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS credit_grants (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
37
|
+
original_nano_usd INTEGER NOT NULL CHECK(original_nano_usd > 0),
|
|
38
|
+
remaining_nano_usd INTEGER NOT NULL CHECK(remaining_nano_usd >= 0),
|
|
39
|
+
expires_at TEXT,
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
created_at TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS credit_grants_spendable ON credit_grants(account_id, expires_at, created_at);
|
|
44
|
+
CREATE TABLE IF NOT EXISTS reservations (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
47
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
48
|
+
requested_nano_usd INTEGER NOT NULL CHECK(requested_nano_usd > 0),
|
|
49
|
+
status TEXT NOT NULL CHECK(status IN ('open','settled','released')),
|
|
50
|
+
request_id TEXT UNIQUE,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
settled_at TEXT
|
|
53
|
+
);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS reservation_allocations (
|
|
55
|
+
reservation_id TEXT NOT NULL REFERENCES reservations(id),
|
|
56
|
+
grant_id TEXT NOT NULL REFERENCES credit_grants(id),
|
|
57
|
+
reserved_nano_usd INTEGER NOT NULL CHECK(reserved_nano_usd > 0),
|
|
58
|
+
consumed_nano_usd INTEGER NOT NULL DEFAULT 0 CHECK(consumed_nano_usd >= 0),
|
|
59
|
+
PRIMARY KEY(reservation_id, grant_id)
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS ledger_entries (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
64
|
+
kind TEXT NOT NULL,
|
|
65
|
+
amount_nano_usd INTEGER NOT NULL,
|
|
66
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
67
|
+
reservation_id TEXT REFERENCES reservations(id),
|
|
68
|
+
gateway_request_id TEXT,
|
|
69
|
+
gateway_attempt_id TEXT,
|
|
70
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
71
|
+
created_at TEXT NOT NULL
|
|
72
|
+
);
|
|
73
|
+
`);
|
|
74
|
+
return db;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class CloudLedger {
|
|
78
|
+
constructor({ path = ':memory:', clock = () => new Date(), id = randomUUID } = {}) {
|
|
79
|
+
this.db = openLedger(path);
|
|
80
|
+
this.clock = clock;
|
|
81
|
+
this.id = id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
now() { return this.clock().toISOString(); }
|
|
85
|
+
close() { this.db.close(); }
|
|
86
|
+
|
|
87
|
+
createAccount(accountId = this.id()) {
|
|
88
|
+
this.db.prepare('INSERT OR IGNORE INTO accounts (id, created_at) VALUES (?, ?)').run(accountId, this.now());
|
|
89
|
+
return accountId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
entryByKey(key) {
|
|
93
|
+
return this.db.prepare('SELECT * FROM ledger_entries WHERE idempotency_key = ?').get(key);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
appendEntry({ accountId, kind, amountNanoUsd, idempotencyKey, reservationId = null, gatewayRequestId = null, gatewayAttemptId = null, metadata = {} }) {
|
|
97
|
+
if (!ENTRY_KINDS.has(kind)) throw new Error(`unknown ledger entry kind: ${kind}`);
|
|
98
|
+
nanoUsd(amountNanoUsd);
|
|
99
|
+
if (!idempotencyKey) throw new Error('idempotencyKey is required');
|
|
100
|
+
const cleanMetadata = validateFinancialMetadata(metadata);
|
|
101
|
+
const existing = this.entryByKey(idempotencyKey);
|
|
102
|
+
if (existing) return existing;
|
|
103
|
+
const id = this.id();
|
|
104
|
+
this.db.prepare(`INSERT INTO ledger_entries
|
|
105
|
+
(id, account_id, kind, amount_nano_usd, idempotency_key, reservation_id, gateway_request_id, gateway_attempt_id, metadata_json, created_at)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
107
|
+
id, accountId, kind, amountNanoUsd, idempotencyKey, reservationId, gatewayRequestId, gatewayAttemptId, JSON.stringify(cleanMetadata), this.now()
|
|
108
|
+
);
|
|
109
|
+
return this.db.prepare('SELECT * FROM ledger_entries WHERE id = ?').get(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
grant({ accountId, amountNanoUsd, idempotencyKey, expiresAt = null, source = 'manual', metadata = {} }) {
|
|
113
|
+
nanoUsd(amountNanoUsd);
|
|
114
|
+
if (amountNanoUsd <= 0) throw new Error('grant amount must be positive');
|
|
115
|
+
this.createAccount(accountId);
|
|
116
|
+
const existing = this.entryByKey(idempotencyKey);
|
|
117
|
+
if (existing) return existing;
|
|
118
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
119
|
+
try {
|
|
120
|
+
const grantId = this.id();
|
|
121
|
+
this.db.prepare(`INSERT INTO credit_grants
|
|
122
|
+
(id, account_id, original_nano_usd, remaining_nano_usd, expires_at, source, created_at)
|
|
123
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(grantId, accountId, amountNanoUsd, amountNanoUsd, expiresAt, source, this.now());
|
|
124
|
+
const entry = this.appendEntry({ accountId, kind: 'credit_grant', amountNanoUsd, idempotencyKey, metadata: { ...metadata, grantId } });
|
|
125
|
+
this.db.exec('COMMIT');
|
|
126
|
+
return entry;
|
|
127
|
+
} catch (error) { this.db.exec('ROLLBACK'); throw error; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
available(accountId, at = this.now()) {
|
|
131
|
+
const row = this.db.prepare(`SELECT COALESCE(SUM(remaining_nano_usd), 0) AS balance
|
|
132
|
+
FROM credit_grants WHERE account_id = ? AND remaining_nano_usd > 0 AND (expires_at IS NULL OR expires_at > ?)`).get(accountId, at);
|
|
133
|
+
return Number(row.balance);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
reserve({ accountId, maximumNanoUsd, idempotencyKey }) {
|
|
137
|
+
nanoUsd(maximumNanoUsd, 'maximumNanoUsd');
|
|
138
|
+
if (maximumNanoUsd <= 0) throw new Error('reservation amount must be positive');
|
|
139
|
+
const existing = this.db.prepare('SELECT * FROM reservations WHERE idempotency_key = ?').get(idempotencyKey);
|
|
140
|
+
if (existing) return existing;
|
|
141
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
142
|
+
try {
|
|
143
|
+
const grants = this.db.prepare(`SELECT * FROM credit_grants
|
|
144
|
+
WHERE account_id = ? AND remaining_nano_usd > 0 AND (expires_at IS NULL OR expires_at > ?)
|
|
145
|
+
ORDER BY CASE WHEN expires_at IS NULL THEN 1 ELSE 0 END, expires_at, created_at, id`).all(accountId, this.now());
|
|
146
|
+
if (grants.reduce((sum, grant) => sum + Number(grant.remaining_nano_usd), 0) < maximumNanoUsd) throw new Error('insufficient_credit');
|
|
147
|
+
const reservationId = this.id();
|
|
148
|
+
this.db.prepare(`INSERT INTO reservations
|
|
149
|
+
(id, account_id, idempotency_key, requested_nano_usd, status, created_at)
|
|
150
|
+
VALUES (?, ?, ?, ?, 'open', ?)`).run(reservationId, accountId, idempotencyKey, maximumNanoUsd, this.now());
|
|
151
|
+
let remaining = maximumNanoUsd;
|
|
152
|
+
for (const grant of grants) {
|
|
153
|
+
if (!remaining) break;
|
|
154
|
+
const amount = Math.min(remaining, Number(grant.remaining_nano_usd));
|
|
155
|
+
this.db.prepare('UPDATE credit_grants SET remaining_nano_usd = remaining_nano_usd - ? WHERE id = ?').run(amount, grant.id);
|
|
156
|
+
this.db.prepare('INSERT INTO reservation_allocations (reservation_id, grant_id, reserved_nano_usd) VALUES (?, ?, ?)').run(reservationId, grant.id, amount);
|
|
157
|
+
remaining -= amount;
|
|
158
|
+
}
|
|
159
|
+
this.appendEntry({ accountId, kind: 'reservation', amountNanoUsd: -maximumNanoUsd, idempotencyKey: `reservation:${idempotencyKey}`, reservationId });
|
|
160
|
+
this.db.exec('COMMIT');
|
|
161
|
+
return this.db.prepare('SELECT * FROM reservations WHERE id = ?').get(reservationId);
|
|
162
|
+
} catch (error) { this.db.exec('ROLLBACK'); throw error; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
settle({ reservationId, actualNanoUsd, requestId, attemptId = null, idempotencyKey, metadata = {} }) {
|
|
166
|
+
nanoUsd(actualNanoUsd, 'actualNanoUsd');
|
|
167
|
+
if (actualNanoUsd < 0) throw new Error('actual charge cannot be negative');
|
|
168
|
+
const existing = this.entryByKey(idempotencyKey);
|
|
169
|
+
if (existing) return existing;
|
|
170
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
171
|
+
try {
|
|
172
|
+
const reservation = this.db.prepare('SELECT * FROM reservations WHERE id = ?').get(reservationId);
|
|
173
|
+
if (!reservation) throw new Error('reservation_not_found');
|
|
174
|
+
if (reservation.status !== 'open') throw new Error(`reservation_${reservation.status}`);
|
|
175
|
+
if (actualNanoUsd > reservation.requested_nano_usd) throw new Error('charge_exceeds_reservation');
|
|
176
|
+
const allocations = this.db.prepare('SELECT * FROM reservation_allocations WHERE reservation_id = ? ORDER BY rowid').all(reservationId);
|
|
177
|
+
let chargeRemaining = actualNanoUsd;
|
|
178
|
+
let released = 0;
|
|
179
|
+
for (const allocation of allocations) {
|
|
180
|
+
const consumed = Math.min(chargeRemaining, Number(allocation.reserved_nano_usd));
|
|
181
|
+
const release = Number(allocation.reserved_nano_usd) - consumed;
|
|
182
|
+
this.db.prepare('UPDATE reservation_allocations SET consumed_nano_usd = ? WHERE reservation_id = ? AND grant_id = ?').run(consumed, reservationId, allocation.grant_id);
|
|
183
|
+
if (release) this.db.prepare('UPDATE credit_grants SET remaining_nano_usd = remaining_nano_usd + ? WHERE id = ?').run(release, allocation.grant_id);
|
|
184
|
+
chargeRemaining -= consumed;
|
|
185
|
+
released += release;
|
|
186
|
+
}
|
|
187
|
+
const entry = this.appendEntry({ accountId: reservation.account_id, kind: 'charge', amountNanoUsd: -actualNanoUsd, idempotencyKey, reservationId, gatewayRequestId: requestId, gatewayAttemptId: attemptId, metadata });
|
|
188
|
+
if (released) this.appendEntry({ accountId: reservation.account_id, kind: 'reservation_release', amountNanoUsd: released, idempotencyKey: `release:${idempotencyKey}`, reservationId, gatewayRequestId: requestId });
|
|
189
|
+
this.db.prepare(`UPDATE reservations SET status = 'settled', request_id = ?, settled_at = ? WHERE id = ?`).run(requestId, this.now(), reservationId);
|
|
190
|
+
this.db.exec('COMMIT');
|
|
191
|
+
return entry;
|
|
192
|
+
} catch (error) { this.db.exec('ROLLBACK'); throw error; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
release({ reservationId, idempotencyKey }) {
|
|
196
|
+
const existing = this.entryByKey(idempotencyKey);
|
|
197
|
+
if (existing) return existing;
|
|
198
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
199
|
+
try {
|
|
200
|
+
const reservation = this.db.prepare('SELECT * FROM reservations WHERE id = ?').get(reservationId);
|
|
201
|
+
if (!reservation) throw new Error('reservation_not_found');
|
|
202
|
+
if (reservation.status !== 'open') throw new Error(`reservation_${reservation.status}`);
|
|
203
|
+
const allocations = this.db.prepare('SELECT * FROM reservation_allocations WHERE reservation_id = ?').all(reservationId);
|
|
204
|
+
for (const allocation of allocations) this.db.prepare('UPDATE credit_grants SET remaining_nano_usd = remaining_nano_usd + ? WHERE id = ?').run(allocation.reserved_nano_usd, allocation.grant_id);
|
|
205
|
+
const entry = this.appendEntry({ accountId: reservation.account_id, kind: 'reservation_release', amountNanoUsd: reservation.requested_nano_usd, idempotencyKey, reservationId });
|
|
206
|
+
this.db.prepare(`UPDATE reservations SET status = 'released', settled_at = ? WHERE id = ?`).run(this.now(), reservationId);
|
|
207
|
+
this.db.exec('COMMIT');
|
|
208
|
+
return entry;
|
|
209
|
+
} catch (error) { this.db.exec('ROLLBACK'); throw error; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
entries(accountId) {
|
|
213
|
+
return this.db.prepare('SELECT * FROM ledger_entries WHERE account_id = ? ORDER BY created_at, rowid').all(accountId).map(row => ({ ...row, metadata: JSON.parse(row.metadata_json) }));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isAbsolute, relative } from 'node:path';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
// Bounded presentation of structural candidates, never runtime-resolution proof.
|
|
4
|
+
export function queryCodeIndex(index, { op, name, path, container, limit = 50, offset = 0 }) {
|
|
5
|
+
const stats = index.stats();
|
|
6
|
+
if (op === 'status') return { op, coverage: stats };
|
|
7
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200 || !Number.isInteger(offset) || offset < 0) throw Error('limit must be 1–200 and offset nonnegative');
|
|
8
|
+
name = name?.trim(); path = path?.replaceAll('\\', '/').replace(/^\.\//, '').trim();
|
|
9
|
+
if (path && isAbsolute(path)) {
|
|
10
|
+
if (existsSync(path) && statSync(path).isDirectory()) throw Error('path is a directory; omit path for repository symbol search. outline/imports need a source FILE. Use find/ls to locate files.');
|
|
11
|
+
path = relative(index.root, path).replaceAll('\\', '/');
|
|
12
|
+
if (path.startsWith('../')) throw Error('path must be inside the indexed workspace');
|
|
13
|
+
}
|
|
14
|
+
if (path && existsSync(`${index.root}/${path}`) && statSync(`${index.root}/${path}`).isDirectory()) throw Error('path is a directory; omit path for symbol search, or supply a source file for outline/imports');
|
|
15
|
+
let rows;
|
|
16
|
+
if (['definition', 'symbols', 'callers', 'callees'].includes(op) && !name) throw Error(`${op} requires name`);
|
|
17
|
+
if (['imports', 'importers', 'outline'].includes(op) && !path) throw Error(`${op} requires path`);
|
|
18
|
+
switch (op) {
|
|
19
|
+
case 'definition': rows = index.definition(name); break;
|
|
20
|
+
case 'symbols': rows = index.symbols(name); break;
|
|
21
|
+
case 'callers': rows = index.callers(name); break;
|
|
22
|
+
case 'callees': rows = index.callees(name, path); break;
|
|
23
|
+
case 'imports': rows = index.imports(path); break;
|
|
24
|
+
case 'importers': rows = index.importers(path); break;
|
|
25
|
+
case 'outline': rows = index.outline(path); break;
|
|
26
|
+
default: throw Error(`unknown op: ${op}`);
|
|
27
|
+
}
|
|
28
|
+
if (path && ['definition', 'symbols', 'callers'].includes(op)) rows = rows.filter(row => row.file === path);
|
|
29
|
+
if (container) rows = rows.filter(row => (op === 'callers' ? row.caller?.container : op === 'callees' ? row.callerDef?.container : row.container) === container);
|
|
30
|
+
const total = rows.length;
|
|
31
|
+
const results = rows.slice(offset, offset + limit);
|
|
32
|
+
const nextOffset = offset + results.length < total ? offset + results.length : null;
|
|
33
|
+
return { op, results, returned: results.length, total, offset, truncated: nextOffset !== null,
|
|
34
|
+
next: total === 0 ? 'No indexed matches. Use find/ls to locate a source file, outline that file, or remove path/container filters. grep handles strings and unsupported syntax.' : undefined,
|
|
35
|
+
nextOffset, coverage: { files: stats.files, discovered: stats.discovered, skipped: stats.skipped, unreadable: stats.unreadable, parseErrors: stats.parseErrors, complete: stats.complete },
|
|
36
|
+
semantics: ['callers', 'callees'].includes(op) ? 'Name-based structural candidates, not resolved runtime bindings. path filters call-site files for callers; container filters the enclosing caller.' : 'Indexed syntax only; excluded, unsupported and skipped files are outside coverage.' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FIELD_CAP = 256;
|
|
40
|
+
const clip = value => {
|
|
41
|
+
if (typeof value === 'string') return value.length > FIELD_CAP ? `${value.slice(0, FIELD_CAP)}… [${value.length} chars]` : value;
|
|
42
|
+
if (Array.isArray(value)) return value.map(clip);
|
|
43
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, clip(v)]));
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Shrink the page, then clip oversized fields, so the envelope and nextOffset always stay parseable.
|
|
48
|
+
export function boundedCodeNavResult(index, params, maxBytes = 23000) {
|
|
49
|
+
let result = queryCodeIndex(index, params);
|
|
50
|
+
let serialized = JSON.stringify(result);
|
|
51
|
+
while (Buffer.byteLength(serialized) > maxBytes && result.returned > 1) {
|
|
52
|
+
result = queryCodeIndex(index, { ...params, limit: Math.max(1, Math.floor(result.returned / 2)) });
|
|
53
|
+
serialized = JSON.stringify(result);
|
|
54
|
+
}
|
|
55
|
+
if (Buffer.byteLength(serialized) > maxBytes && result.results) {
|
|
56
|
+
result = { ...result, results: result.results.map(clip), clipped: `Fields over ${FIELD_CAP} chars were shortened; read the source file for full text.` };
|
|
57
|
+
serialized = JSON.stringify(result);
|
|
58
|
+
}
|
|
59
|
+
return serialized;
|
|
60
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { readdirSync, statSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative, extname, resolve, dirname, isAbsolute } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const require_ = createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
const LANG_BY_EXT = {
|
|
8
|
+
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
|
|
9
|
+
'.ts': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
|
|
10
|
+
'.tsx': 'tsx',
|
|
11
|
+
'.py': 'python',
|
|
12
|
+
'.rs': 'rust',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_IGNORES = new Set([
|
|
16
|
+
'node_modules', '.git', '.hg', '.svn', 'dist', 'build', 'out', 'coverage',
|
|
17
|
+
'.next', '.nuxt', 'target', 'vendor', '.venv', 'venv', '__pycache__',
|
|
18
|
+
'.idea', '.vscode', '.turbo', '.cache', '.harness', 'release',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// ---- per-language AST extraction -------------------------------------------
|
|
22
|
+
// Each spec: nameOf(node) → def name | null for def node types, callName(node)
|
|
23
|
+
// → callee name for call node types, importSpec(node) → specifier string.
|
|
24
|
+
|
|
25
|
+
const firstNamedOfType = (node, types) => {
|
|
26
|
+
for (const child of node.namedChildren) if (types.includes(child.type)) return child;
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const fieldName = node => node.childForFieldName('name');
|
|
31
|
+
const lastNamedChild = node => node.namedChildren[node.namedChildren.length - 1] || null;
|
|
32
|
+
|
|
33
|
+
const jsSpec = {
|
|
34
|
+
defTypes: new Set(['function_declaration', 'generator_function_declaration', 'class_declaration',
|
|
35
|
+
'abstract_class_declaration', 'interface_declaration', 'type_alias_declaration',
|
|
36
|
+
'enum_declaration', 'method_definition']),
|
|
37
|
+
nameOf(node) {
|
|
38
|
+
const named = fieldName(node);
|
|
39
|
+
return named?.text || firstNamedOfType(node, ['identifier', 'type_identifier', 'property_identifier'])?.text || null;
|
|
40
|
+
},
|
|
41
|
+
defKind: node => node.type === 'method_definition' ? 'method'
|
|
42
|
+
: node.type.includes('class') ? 'class'
|
|
43
|
+
: node.type === 'interface_declaration' ? 'interface'
|
|
44
|
+
: node.type === 'type_alias_declaration' ? 'type'
|
|
45
|
+
: node.type === 'enum_declaration' ? 'enum' : 'function',
|
|
46
|
+
containerTypes: new Set(['class_declaration', 'abstract_class_declaration']),
|
|
47
|
+
isDef(node, parent) {
|
|
48
|
+
if (this.defTypes.has(node.type)) return true;
|
|
49
|
+
// const/let x = () => ... or = function ...
|
|
50
|
+
if (node.type === 'variable_declarator') {
|
|
51
|
+
const value = node.childForFieldName('value');
|
|
52
|
+
return !!value && ['arrow_function', 'function_expression', 'generator_function'].includes(value.type);
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
defName(node) {
|
|
57
|
+
if (node.type === 'variable_declarator') return node.childForFieldName('name')?.text || null;
|
|
58
|
+
return this.nameOf(node);
|
|
59
|
+
},
|
|
60
|
+
callTypes: new Set(['call_expression']),
|
|
61
|
+
callName(node) {
|
|
62
|
+
const fn = node.childForFieldName('function');
|
|
63
|
+
if (!fn) return null;
|
|
64
|
+
if (fn.type === 'identifier') return fn.text;
|
|
65
|
+
if (fn.type === 'member_expression') return fn.childForFieldName('property')?.text || null;
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
importTypes: new Set(['import_statement']),
|
|
69
|
+
importSpec(node) {
|
|
70
|
+
const src = node.childForFieldName('source') || firstNamedOfType(node, ['string']);
|
|
71
|
+
return src ? src.text.replace(/^['"`]|['"`]$/g, '') : null;
|
|
72
|
+
},
|
|
73
|
+
// require('x') as an import edge
|
|
74
|
+
extraImport(node) {
|
|
75
|
+
if (node.type !== 'call_expression') return null;
|
|
76
|
+
const fn = node.childForFieldName('function');
|
|
77
|
+
if (fn?.type !== 'identifier' || fn.text !== 'require') return null;
|
|
78
|
+
const arg = node.childForFieldName('arguments')?.namedChildren[0];
|
|
79
|
+
return arg?.type === 'string' ? arg.text.replace(/^['"`]|['"`]$/g, '') : null;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const pySpec = {
|
|
84
|
+
defTypes: new Set(['function_definition', 'class_definition']),
|
|
85
|
+
nameOf(node) { return firstNamedOfType(node, ['identifier'])?.text || null; },
|
|
86
|
+
defKind: node => node.type === 'class_definition' ? 'class' : 'function',
|
|
87
|
+
containerTypes: new Set(['class_definition']),
|
|
88
|
+
isDef(node) { return this.defTypes.has(node.type); },
|
|
89
|
+
defName(node) { return this.nameOf(node); },
|
|
90
|
+
callTypes: new Set(['call']),
|
|
91
|
+
callName(node) {
|
|
92
|
+
const fn = node.childForFieldName('function');
|
|
93
|
+
if (!fn) return null;
|
|
94
|
+
if (fn.type === 'identifier') return fn.text;
|
|
95
|
+
if (fn.type === 'attribute') return lastNamedChild(fn)?.text || null;
|
|
96
|
+
return null;
|
|
97
|
+
},
|
|
98
|
+
importTypes: new Set(['import_statement', 'import_from_statement']),
|
|
99
|
+
importSpec(node) {
|
|
100
|
+
const module = node.type === 'import_from_statement'
|
|
101
|
+
? (node.childForFieldName('module_name') || firstNamedOfType(node, ['dotted_name', 'relative_import']))
|
|
102
|
+
: firstNamedOfType(node, ['dotted_name', 'aliased_import']);
|
|
103
|
+
if (!module) return null;
|
|
104
|
+
const aliased = module.type === 'aliased_import' ? firstNamedOfType(module, ['dotted_name']) : module;
|
|
105
|
+
return aliased?.text || null;
|
|
106
|
+
},
|
|
107
|
+
extraImport() { return null; },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const rustSpec = {
|
|
111
|
+
defTypes: new Set(['function_item', 'struct_item', 'enum_item', 'union_item', 'trait_item',
|
|
112
|
+
'type_item', 'const_item', 'static_item', 'mod_item', 'macro_definition']),
|
|
113
|
+
nameOf(node) { return firstNamedOfType(node, ['identifier', 'type_identifier'])?.text || null; },
|
|
114
|
+
defKind: node => node.type === 'function_item' ? 'function'
|
|
115
|
+
: node.type === 'struct_item' ? 'struct'
|
|
116
|
+
: node.type === 'enum_item' ? 'enum'
|
|
117
|
+
: node.type === 'trait_item' ? 'trait'
|
|
118
|
+
: node.type === 'mod_item' ? 'module' : 'item',
|
|
119
|
+
containerTypes: new Set(['impl_item', 'trait_item', 'mod_item']),
|
|
120
|
+
containerOf(node) { return firstNamedOfType(node, ['type_identifier', 'identifier'])?.text || null; },
|
|
121
|
+
isDef(node) { return this.defTypes.has(node.type); },
|
|
122
|
+
defName(node) { return this.nameOf(node); },
|
|
123
|
+
callTypes: new Set(['call_expression', 'macro_invocation']),
|
|
124
|
+
callName(node) {
|
|
125
|
+
if (node.type === 'macro_invocation') {
|
|
126
|
+
const macro = node.childForFieldName('macro') || firstNamedOfType(node, ['identifier', 'scoped_identifier']);
|
|
127
|
+
return macro ? `${macro.text.split('::').pop()}!` : null;
|
|
128
|
+
}
|
|
129
|
+
const fn = node.childForFieldName('function');
|
|
130
|
+
if (!fn) return null;
|
|
131
|
+
if (fn.type === 'identifier') return fn.text;
|
|
132
|
+
if (fn.type === 'field_expression') return fn.childForFieldName('field')?.text || null;
|
|
133
|
+
if (fn.type === 'scoped_identifier' || fn.type === 'generic_function') return lastNamedChild(fn)?.text || fn.text.split('::').pop();
|
|
134
|
+
return null;
|
|
135
|
+
},
|
|
136
|
+
importTypes: new Set(['use_declaration']),
|
|
137
|
+
importSpec(node) {
|
|
138
|
+
const arg = firstNamedOfType(node, ['scoped_identifier', 'identifier', 'use_list', 'scoped_use_list', 'use_as_clause']);
|
|
139
|
+
return arg ? arg.text : node.text.replace(/^use\s+/, '').replace(/;$/, '').trim() || null;
|
|
140
|
+
},
|
|
141
|
+
extraImport() { return null; },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const SPECS = { javascript: jsSpec, typescript: jsSpec, tsx: jsSpec, python: pySpec, rust: rustSpec };
|
|
145
|
+
|
|
146
|
+
// ---- parser bootstrap --------------------------------------------------------
|
|
147
|
+
// Dependency pin constraint: the tree-sitter-wasms grammars ship the pre-`dylink.0`
|
|
148
|
+
// wasm section, which only web-tree-sitter ≤0.22.x accepts. Bump both together.
|
|
149
|
+
|
|
150
|
+
let runtimePromise = null;
|
|
151
|
+
const parsers = new Map(); // lang -> dedicated parser (setLanguage once; concurrent parses never share)
|
|
152
|
+
|
|
153
|
+
async function parserFor(lang) {
|
|
154
|
+
if (!runtimePromise) {
|
|
155
|
+
runtimePromise = (async () => {
|
|
156
|
+
const Parser = require_('web-tree-sitter');
|
|
157
|
+
await Parser.init();
|
|
158
|
+
const wasmDir = require_.resolve('tree-sitter-wasms/package.json').replace(/package\.json$/, 'out');
|
|
159
|
+
const languages = new Map();
|
|
160
|
+
const load = async name => {
|
|
161
|
+
if (!languages.has(name)) languages.set(name, await Parser.Language.load(join(wasmDir, `tree-sitter-${name}.wasm`)));
|
|
162
|
+
return languages.get(name);
|
|
163
|
+
};
|
|
164
|
+
return { Parser, load };
|
|
165
|
+
})();
|
|
166
|
+
}
|
|
167
|
+
const { Parser, load } = await runtimePromise;
|
|
168
|
+
if (!parsers.has(lang)) {
|
|
169
|
+
const parser = new Parser();
|
|
170
|
+
parser.setLanguage(await load(lang));
|
|
171
|
+
parsers.set(lang, parser);
|
|
172
|
+
}
|
|
173
|
+
return parsers.get(lang);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- index -------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a structural index over a workspace. The index is per-process;
|
|
180
|
+
* `refresh()` re-parses only files whose mtime/size changed since last scan.
|
|
181
|
+
*/
|
|
182
|
+
export async function createCodeIndex(root, { maxFiles = 8000, maxBytes = 1_048_576, ignores = DEFAULT_IGNORES } = {}) {
|
|
183
|
+
if (!Number.isSafeInteger(maxFiles) || maxFiles < 1 || !Number.isSafeInteger(maxBytes) || maxBytes < 1) throw Error('Index limits must be positive safe integers');
|
|
184
|
+
const base = resolve(root);
|
|
185
|
+
const toRel = abs => relative(base, abs).replace(/\\/g, '/'); // index keys always POSIX-style
|
|
186
|
+
const files = new Map(); // relPath -> { mtimeMs, size, defs, calls, imports }
|
|
187
|
+
const defIndex = new Map(); // name -> [{file, line, kind, container}]
|
|
188
|
+
const callIndex = new Map(); // calleeName -> [{file, line, caller:{name,file,line}|null}]
|
|
189
|
+
const importEdges = new Map(); // file -> [spec]
|
|
190
|
+
const stats = { files: 0, symbols: 0, calls: 0, parsed: 0, parseErrors: 0, totalParseErrors: 0, parseMs: 0, skipped: 0, unreadable: 0, discovered: 0, complete: true };
|
|
191
|
+
|
|
192
|
+
const dropFile = rel => {
|
|
193
|
+
const prev = files.get(rel);
|
|
194
|
+
if (!prev) return;
|
|
195
|
+
for (const d of prev.defs) {
|
|
196
|
+
for (const key of d.keys) {
|
|
197
|
+
const list = defIndex.get(key);
|
|
198
|
+
if (list) { const i = list.findIndex(x => x.file === rel && x.line === d.line); if (i >= 0) list.splice(i, 1); if (!list.length) defIndex.delete(key); }
|
|
199
|
+
}
|
|
200
|
+
stats.symbols--;
|
|
201
|
+
}
|
|
202
|
+
for (const c of prev.calls) {
|
|
203
|
+
const list = callIndex.get(c.name);
|
|
204
|
+
if (list) { const i = list.findIndex(x => x.file === rel && x.line === c.line); if (i >= 0) list.splice(i, 1); if (!list.length) callIndex.delete(c.name); }
|
|
205
|
+
stats.calls--;
|
|
206
|
+
}
|
|
207
|
+
files.delete(rel);
|
|
208
|
+
importEdges.delete(rel);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const addDef = (rel, { name, kind, line, container }) => {
|
|
212
|
+
const keys = [name];
|
|
213
|
+
if (container) keys.push(`${container}.${name}`);
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
if (!defIndex.has(key)) defIndex.set(key, []);
|
|
216
|
+
defIndex.get(key).push({ file: rel, line, kind, container: container || null, name });
|
|
217
|
+
}
|
|
218
|
+
stats.symbols++;
|
|
219
|
+
return { name, kind, line, container: container || null, keys };
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const extract = (tree, rel, spec) => {
|
|
223
|
+
const entry = { defs: [], calls: [], imports: [] };
|
|
224
|
+
const visit = (node, enclosing, container) => {
|
|
225
|
+
if (spec.isDef(node)) {
|
|
226
|
+
const name = spec.defName(node);
|
|
227
|
+
if (name) {
|
|
228
|
+
entry.defs.push(addDef(rel, { name, kind: spec.defKind(node), line: node.startPosition.row + 1, container }));
|
|
229
|
+
enclosing = { name, line: node.startPosition.row + 1, container: container || null };
|
|
230
|
+
}
|
|
231
|
+
if (spec.containerTypes.has(node.type)) container = spec.containerOf ? spec.containerOf(node) : name;
|
|
232
|
+
} else if (spec.containerTypes.has(node.type)) {
|
|
233
|
+
container = spec.containerOf ? spec.containerOf(node) : (spec.nameOf(node) || container);
|
|
234
|
+
}
|
|
235
|
+
if (spec.callTypes.has(node.type)) {
|
|
236
|
+
const name = spec.callName(node);
|
|
237
|
+
if (name) {
|
|
238
|
+
entry.calls.push({ name, line: node.startPosition.row + 1, caller: enclosing ? { ...enclosing, file: rel } : null });
|
|
239
|
+
if (!callIndex.has(name)) callIndex.set(name, []);
|
|
240
|
+
callIndex.get(name).push({ file: rel, line: node.startPosition.row + 1, caller: enclosing ? { ...enclosing, file: rel } : null });
|
|
241
|
+
stats.calls++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (spec.importTypes.has(node.type)) {
|
|
245
|
+
const specText = spec.importSpec(node);
|
|
246
|
+
if (specText) entry.imports.push(specText);
|
|
247
|
+
}
|
|
248
|
+
const extra = spec.extraImport(node);
|
|
249
|
+
if (extra) entry.imports.push(extra);
|
|
250
|
+
for (const child of node.namedChildren) visit(child, enclosing, container);
|
|
251
|
+
};
|
|
252
|
+
visit(tree.rootNode, null, null);
|
|
253
|
+
return entry;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const parseFile = async (abs, rel, lang, stat) => {
|
|
257
|
+
const source = readFileSync(abs, 'utf8');
|
|
258
|
+
const parser = await parserFor(lang);
|
|
259
|
+
const started = Date.now();
|
|
260
|
+
const tree = parser.parse(source);
|
|
261
|
+
stats.parseMs += Date.now() - started;
|
|
262
|
+
stats.parsed++;
|
|
263
|
+
const entry = extract(tree, rel, SPECS[lang]);
|
|
264
|
+
files.set(rel, { mtimeMs: stat.mtimeMs, size: stat.size, defs: entry.defs, calls: entry.calls, imports: entry.imports });
|
|
265
|
+
importEdges.set(rel, entry.imports);
|
|
266
|
+
tree.delete();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const scan = (dir, out) => {
|
|
270
|
+
let entries;
|
|
271
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { stats.unreadable++; return; }
|
|
272
|
+
for (const ent of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
273
|
+
const abs = join(dir, ent.name);
|
|
274
|
+
if (ent.isDirectory()) {
|
|
275
|
+
if (ent.name.startsWith('.') || ignores.has(ent.name)) continue;
|
|
276
|
+
scan(abs, out);
|
|
277
|
+
} else if (ent.isFile()) {
|
|
278
|
+
const lang = LANG_BY_EXT[extname(ent.name)];
|
|
279
|
+
if (lang) out.push({ abs, lang });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const doRefresh = async () => {
|
|
285
|
+
const found = [];
|
|
286
|
+
stats.unreadable = 0;
|
|
287
|
+
scan(base, found);
|
|
288
|
+
stats.skipped = 0;
|
|
289
|
+
stats.parseErrors = 0;
|
|
290
|
+
stats.discovered = found.length;
|
|
291
|
+
if (found.length > maxFiles) { stats.skipped += found.length - maxFiles; found.length = maxFiles; }
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
for (const { abs, lang } of found) {
|
|
294
|
+
const rel = toRel(abs);
|
|
295
|
+
seen.add(rel);
|
|
296
|
+
let stat;
|
|
297
|
+
try { stat = statSync(abs); } catch { dropFile(rel); stats.unreadable++; continue; }
|
|
298
|
+
if (stat.size > maxBytes) { dropFile(rel); stats.skipped++; continue; }
|
|
299
|
+
const prev = files.get(rel);
|
|
300
|
+
if (prev && prev.mtimeMs === stat.mtimeMs && prev.size === stat.size) continue;
|
|
301
|
+
dropFile(rel);
|
|
302
|
+
try { await parseFile(abs, rel, lang, stat); } catch { stats.parseErrors++; stats.totalParseErrors++; }
|
|
303
|
+
}
|
|
304
|
+
for (const rel of [...files.keys()]) if (!seen.has(rel)) dropFile(rel);
|
|
305
|
+
stats.files = files.size;
|
|
306
|
+
stats.complete = stats.skipped === 0 && stats.parseErrors === 0 && stats.unreadable === 0;
|
|
307
|
+
return stats;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const resolveImport = (fromFile, spec) => {
|
|
311
|
+
// Relative specifiers resolve to a repo file; bare specifiers stay as-is.
|
|
312
|
+
if (!spec.startsWith('.') && !spec.startsWith('/')) return null;
|
|
313
|
+
const stem = resolve(dirname(join(base, fromFile)), spec);
|
|
314
|
+
const rel = toRel(stem);
|
|
315
|
+
const candidates = [rel, ...Object.keys(LANG_BY_EXT).map(e => rel + e), `${rel}/index.ts`, `${rel}/index.tsx`, `${rel}/index.js`, `${rel}/__init__.py`, `${rel}/mod.rs`];
|
|
316
|
+
return candidates.find(c => files.has(c)) || null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Serialize refresh calls: overlapping refreshes would interleave dropFile/parseFile.
|
|
320
|
+
let refreshQueue = Promise.resolve();
|
|
321
|
+
const refresh = () => (refreshQueue = refreshQueue.then(doRefresh, doRefresh));
|
|
322
|
+
|
|
323
|
+
await refresh();
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
root: base,
|
|
327
|
+
refresh,
|
|
328
|
+
stats: () => ({ ...stats }),
|
|
329
|
+
definition(name) {
|
|
330
|
+
return defIndex.get(name) || [];
|
|
331
|
+
},
|
|
332
|
+
symbols(query, limit = Infinity) {
|
|
333
|
+
const q = String(query || '').toLowerCase();
|
|
334
|
+
const out = [];
|
|
335
|
+
for (const [name, defs] of defIndex) {
|
|
336
|
+
if (!name.toLowerCase().includes(q)) continue;
|
|
337
|
+
for (const d of defs) out.push({ ...d, displayName: name });
|
|
338
|
+
if (out.length >= limit) break;
|
|
339
|
+
}
|
|
340
|
+
return out.slice(0, limit);
|
|
341
|
+
},
|
|
342
|
+
callers(name) { return callIndex.get(name) || []; },
|
|
343
|
+
callees(name, file) {
|
|
344
|
+
return (defIndex.get(name) || [])
|
|
345
|
+
.filter(d => !file || d.file === file)
|
|
346
|
+
.flatMap(def => collectCallsOf(def, def.name));
|
|
347
|
+
},
|
|
348
|
+
imports(file) { return files.get(file)?.imports || files.get(normalizeRel(file))?.imports || []; },
|
|
349
|
+
importers(file) {
|
|
350
|
+
const rel = normalizeRel(file);
|
|
351
|
+
const out = [];
|
|
352
|
+
for (const [f, specs] of importEdges) {
|
|
353
|
+
for (const spec of specs) {
|
|
354
|
+
if (resolveImport(f, spec) === rel || (!resolveImport(f, spec) && spec === rel)) { out.push({ file: f, spec }); break; }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
},
|
|
359
|
+
outline(file) {
|
|
360
|
+
const rel = normalizeRel(file);
|
|
361
|
+
return (files.get(rel)?.defs || []).map(({ keys, ...d }) => d);
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
function normalizeRel(file) {
|
|
366
|
+
const s = String(file || '').replace(/^\.\//, '');
|
|
367
|
+
return (isAbsolute(s) ? toRel(s) : s).replace(/\\/g, '/');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function collectCallsOf(def, name) {
|
|
371
|
+
// calls made inside the def at def.file:def.line
|
|
372
|
+
const entry = files.get(def.file);
|
|
373
|
+
if (!entry) return [];
|
|
374
|
+
return entry.calls
|
|
375
|
+
.filter(c => c.caller && c.caller.name === name && c.caller.line === def.line && c.caller.file === def.file)
|
|
376
|
+
.map(c => ({ ...c, callerDef: def }));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { localPath } from './delivery.mjs';
|
|
5
|
+
|
|
6
|
+
// Host storage boundary. Content-addressed originals must exist intact before
|
|
7
|
+
// the outbound projection can substitute a pointer. No mutation of run state.
|
|
8
|
+
export function archiveContext(cwd, directory, value) {
|
|
9
|
+
const text = JSON.stringify(value, null, 2);
|
|
10
|
+
const digest = createHash('sha256').update(text).digest('hex');
|
|
11
|
+
const root = localPath(cwd, join(directory, 'context-archive'));
|
|
12
|
+
mkdirSync(root, { recursive: true });
|
|
13
|
+
const path = localPath(cwd, join(root, `${digest}.json`));
|
|
14
|
+
try { writeFileSync(path, text, { flag: 'wx', mode: 0o600 }); }
|
|
15
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
16
|
+
if (readFileSync(path, 'utf8') !== text) throw Error('Context archive mismatch; original messages retained');
|
|
17
|
+
return { path, digest, bytes: Buffer.byteLength(text) };
|
|
18
|
+
}
|