contextpro-mcp 0.0.0-stage → 1.4.0
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/CHANGELOG.md +416 -0
- package/INSTALL.md +128 -0
- package/INSTRUCTIONS.md +33 -0
- package/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +164 -2
- package/SECURITY.md +27 -0
- package/agent-guide.md +323 -0
- package/agent-instructions.md +1 -0
- package/build/core/agent-setup.d.ts +5 -0
- package/build/core/agent-setup.d.ts.map +1 -0
- package/build/core/agent-setup.js +94 -0
- package/build/core/agent-setup.js.map +1 -0
- package/build/core/artifacts.d.ts +64 -0
- package/build/core/artifacts.d.ts.map +1 -0
- package/build/core/artifacts.js +370 -0
- package/build/core/artifacts.js.map +1 -0
- package/build/core/bounded-source.d.ts +15 -0
- package/build/core/bounded-source.d.ts.map +1 -0
- package/build/core/bounded-source.js +78 -0
- package/build/core/bounded-source.js.map +1 -0
- package/build/core/changed-since.d.ts +20 -0
- package/build/core/changed-since.d.ts.map +1 -0
- package/build/core/changed-since.js +43 -0
- package/build/core/changed-since.js.map +1 -0
- package/build/core/chat-model.d.ts +8 -0
- package/build/core/chat-model.d.ts.map +1 -0
- package/build/core/chat-model.js +135 -0
- package/build/core/chat-model.js.map +1 -0
- package/build/core/clustering.d.ts +6 -0
- package/build/core/clustering.d.ts.map +1 -0
- package/build/core/clustering.js +201 -0
- package/build/core/clustering.js.map +1 -0
- package/build/core/context-metadata.d.ts +50 -0
- package/build/core/context-metadata.d.ts.map +1 -0
- package/build/core/context-metadata.js +118 -0
- package/build/core/context-metadata.js.map +1 -0
- package/build/core/data-dir.d.ts +5 -0
- package/build/core/data-dir.d.ts.map +1 -0
- package/build/core/data-dir.js +53 -0
- package/build/core/data-dir.js.map +1 -0
- package/build/core/db-native.d.ts +28 -0
- package/build/core/db-native.d.ts.map +1 -0
- package/build/core/db-native.js +120 -0
- package/build/core/db-native.js.map +1 -0
- package/build/core/db.d.ts +294 -0
- package/build/core/db.d.ts.map +1 -0
- package/build/core/db.js +1047 -0
- package/build/core/db.js.map +1 -0
- package/build/core/doctor.d.ts +13 -0
- package/build/core/doctor.d.ts.map +1 -0
- package/build/core/doctor.js +55 -0
- package/build/core/doctor.js.map +1 -0
- package/build/core/efficiency.d.ts +16 -0
- package/build/core/efficiency.d.ts.map +1 -0
- package/build/core/efficiency.js +170 -0
- package/build/core/efficiency.js.map +1 -0
- package/build/core/embedding-tracker.d.ts +25 -0
- package/build/core/embedding-tracker.d.ts.map +1 -0
- package/build/core/embedding-tracker.js +228 -0
- package/build/core/embedding-tracker.js.map +1 -0
- package/build/core/embeddings.d.ts +90 -0
- package/build/core/embeddings.d.ts.map +1 -0
- package/build/core/embeddings.js +693 -0
- package/build/core/embeddings.js.map +1 -0
- package/build/core/evidence-state.d.ts +42 -0
- package/build/core/evidence-state.d.ts.map +1 -0
- package/build/core/evidence-state.js +153 -0
- package/build/core/evidence-state.js.map +1 -0
- package/build/core/extractor-stamp.d.ts +10 -0
- package/build/core/extractor-stamp.d.ts.map +1 -0
- package/build/core/extractor-stamp.js +51 -0
- package/build/core/extractor-stamp.js.map +1 -0
- package/build/core/feature-brief-cache.d.ts +37 -0
- package/build/core/feature-brief-cache.d.ts.map +1 -0
- package/build/core/feature-brief-cache.js +100 -0
- package/build/core/feature-brief-cache.js.map +1 -0
- package/build/core/file-fingerprint.d.ts +15 -0
- package/build/core/file-fingerprint.d.ts.map +1 -0
- package/build/core/file-fingerprint.js +24 -0
- package/build/core/file-fingerprint.js.map +1 -0
- package/build/core/graph-analytics.d.ts +46 -0
- package/build/core/graph-analytics.d.ts.map +1 -0
- package/build/core/graph-analytics.js +371 -0
- package/build/core/graph-analytics.js.map +1 -0
- package/build/core/hub.d.ts +23 -0
- package/build/core/hub.d.ts.map +1 -0
- package/build/core/hub.js +123 -0
- package/build/core/hub.js.map +1 -0
- package/build/core/ignore-dirs.d.ts +7 -0
- package/build/core/ignore-dirs.d.ts.map +1 -0
- package/build/core/ignore-dirs.js +84 -0
- package/build/core/ignore-dirs.js.map +1 -0
- package/build/core/index-freshness.d.ts +15 -0
- package/build/core/index-freshness.d.ts.map +1 -0
- package/build/core/index-freshness.js +96 -0
- package/build/core/index-freshness.js.map +1 -0
- package/build/core/index-reconcile.d.ts +28 -0
- package/build/core/index-reconcile.d.ts.map +1 -0
- package/build/core/index-reconcile.js +129 -0
- package/build/core/index-reconcile.js.map +1 -0
- package/build/core/local-daemon.d.ts +5 -0
- package/build/core/local-daemon.d.ts.map +1 -0
- package/build/core/local-daemon.js +282 -0
- package/build/core/local-daemon.js.map +1 -0
- package/build/core/memory-graph.d.ts +68 -0
- package/build/core/memory-graph.d.ts.map +1 -0
- package/build/core/memory-graph.js +152 -0
- package/build/core/memory-graph.js.map +1 -0
- package/build/core/native-repair.d.ts +12 -0
- package/build/core/native-repair.d.ts.map +1 -0
- package/build/core/native-repair.js +167 -0
- package/build/core/native-repair.js.map +1 -0
- package/build/core/native-runtime.d.ts +12 -0
- package/build/core/native-runtime.d.ts.map +1 -0
- package/build/core/native-runtime.js +38 -0
- package/build/core/native-runtime.js.map +1 -0
- package/build/core/pagerank.d.ts +29 -0
- package/build/core/pagerank.d.ts.map +1 -0
- package/build/core/pagerank.js +90 -0
- package/build/core/pagerank.js.map +1 -0
- package/build/core/parser-process.d.ts +21 -0
- package/build/core/parser-process.d.ts.map +1 -0
- package/build/core/parser-process.js +103 -0
- package/build/core/parser-process.js.map +1 -0
- package/build/core/parser-worker.d.ts +2 -0
- package/build/core/parser-worker.d.ts.map +1 -0
- package/build/core/parser-worker.js +17 -0
- package/build/core/parser-worker.js.map +1 -0
- package/build/core/parser.d.ts +66 -0
- package/build/core/parser.d.ts.map +1 -0
- package/build/core/parser.js +750 -0
- package/build/core/parser.js.map +1 -0
- package/build/core/path-guard.d.ts +6 -0
- package/build/core/path-guard.d.ts.map +1 -0
- package/build/core/path-guard.js +55 -0
- package/build/core/path-guard.js.map +1 -0
- package/build/core/process-lifecycle.d.ts +30 -0
- package/build/core/process-lifecycle.d.ts.map +1 -0
- package/build/core/process-lifecycle.js +129 -0
- package/build/core/process-lifecycle.js.map +1 -0
- package/build/core/project-config.d.ts +83 -0
- package/build/core/project-config.d.ts.map +1 -0
- package/build/core/project-config.js +302 -0
- package/build/core/project-config.js.map +1 -0
- package/build/core/project-graph.d.ts +69 -0
- package/build/core/project-graph.d.ts.map +1 -0
- package/build/core/project-graph.js +874 -0
- package/build/core/project-graph.js.map +1 -0
- package/build/core/provider-config.d.ts +15 -0
- package/build/core/provider-config.d.ts.map +1 -0
- package/build/core/provider-config.js +67 -0
- package/build/core/provider-config.js.map +1 -0
- package/build/core/provider-usage.d.ts +21 -0
- package/build/core/provider-usage.d.ts.map +1 -0
- package/build/core/provider-usage.js +52 -0
- package/build/core/provider-usage.js.map +1 -0
- package/build/core/python-language-server.d.ts +23 -0
- package/build/core/python-language-server.d.ts.map +1 -0
- package/build/core/python-language-server.js +217 -0
- package/build/core/python-language-server.js.map +1 -0
- package/build/core/python-runtime-identity.d.ts +3 -0
- package/build/core/python-runtime-identity.d.ts.map +1 -0
- package/build/core/python-runtime-identity.js +53 -0
- package/build/core/python-runtime-identity.js.map +1 -0
- package/build/core/reconcile-scheduler.d.ts +20 -0
- package/build/core/reconcile-scheduler.d.ts.map +1 -0
- package/build/core/reconcile-scheduler.js +58 -0
- package/build/core/reconcile-scheduler.js.map +1 -0
- package/build/core/request-context.d.ts +7 -0
- package/build/core/request-context.d.ts.map +1 -0
- package/build/core/request-context.js +17 -0
- package/build/core/request-context.js.map +1 -0
- package/build/core/request-root.d.ts +19 -0
- package/build/core/request-root.d.ts.map +1 -0
- package/build/core/request-root.js +26 -0
- package/build/core/request-root.js.map +1 -0
- package/build/core/root-guard.d.ts +8 -0
- package/build/core/root-guard.d.ts.map +1 -0
- package/build/core/root-guard.js +53 -0
- package/build/core/root-guard.js.map +1 -0
- package/build/core/roots-detect.d.ts +17 -0
- package/build/core/roots-detect.d.ts.map +1 -0
- package/build/core/roots-detect.js +63 -0
- package/build/core/roots-detect.js.map +1 -0
- package/build/core/runtime-session.d.ts +5 -0
- package/build/core/runtime-session.d.ts.map +1 -0
- package/build/core/runtime-session.js +10 -0
- package/build/core/runtime-session.js.map +1 -0
- package/build/core/scip-index.d.ts +71 -0
- package/build/core/scip-index.d.ts.map +1 -0
- package/build/core/scip-index.js +343 -0
- package/build/core/scip-index.js.map +1 -0
- package/build/core/semantic-diff.d.ts +32 -0
- package/build/core/semantic-diff.d.ts.map +1 -0
- package/build/core/semantic-diff.js +259 -0
- package/build/core/semantic-diff.js.map +1 -0
- package/build/core/source-context.d.ts +3 -0
- package/build/core/source-context.d.ts.map +1 -0
- package/build/core/source-context.js +86 -0
- package/build/core/source-context.js.map +1 -0
- package/build/core/source-mask.d.ts +8 -0
- package/build/core/source-mask.d.ts.map +1 -0
- package/build/core/source-mask.js +156 -0
- package/build/core/source-mask.js.map +1 -0
- package/build/core/source-mutation.d.ts +16 -0
- package/build/core/source-mutation.d.ts.map +1 -0
- package/build/core/source-mutation.js +193 -0
- package/build/core/source-mutation.js.map +1 -0
- package/build/core/sql-schema.d.ts +59 -0
- package/build/core/sql-schema.d.ts.map +1 -0
- package/build/core/sql-schema.js +218 -0
- package/build/core/sql-schema.js.map +1 -0
- package/build/core/startup-health.d.ts +40 -0
- package/build/core/startup-health.d.ts.map +1 -0
- package/build/core/startup-health.js +132 -0
- package/build/core/startup-health.js.map +1 -0
- package/build/core/team-ledger.d.ts +60 -0
- package/build/core/team-ledger.d.ts.map +1 -0
- package/build/core/team-ledger.js +171 -0
- package/build/core/team-ledger.js.map +1 -0
- package/build/core/test-impact.d.ts +23 -0
- package/build/core/test-impact.d.ts.map +1 -0
- package/build/core/test-impact.js +131 -0
- package/build/core/test-impact.js.map +1 -0
- package/build/core/tool-metrics.d.ts +29 -0
- package/build/core/tool-metrics.d.ts.map +1 -0
- package/build/core/tool-metrics.js +74 -0
- package/build/core/tool-metrics.js.map +1 -0
- package/build/core/tool-profile.d.ts +12 -0
- package/build/core/tool-profile.d.ts.map +1 -0
- package/build/core/tool-profile.js +102 -0
- package/build/core/tool-profile.js.map +1 -0
- package/build/core/tool-trace.d.ts +8 -0
- package/build/core/tool-trace.d.ts.map +1 -0
- package/build/core/tool-trace.js +70 -0
- package/build/core/tool-trace.js.map +1 -0
- package/build/core/tree-sitter-runtime.d.ts +20 -0
- package/build/core/tree-sitter-runtime.d.ts.map +1 -0
- package/build/core/tree-sitter-runtime.js +324 -0
- package/build/core/tree-sitter-runtime.js.map +1 -0
- package/build/core/tree-sitter.d.ts +6 -0
- package/build/core/tree-sitter.d.ts.map +1 -0
- package/build/core/tree-sitter.js +24 -0
- package/build/core/tree-sitter.js.map +1 -0
- package/build/core/typescript-reference-process.d.ts +30 -0
- package/build/core/typescript-reference-process.d.ts.map +1 -0
- package/build/core/typescript-reference-process.js +124 -0
- package/build/core/typescript-reference-process.js.map +1 -0
- package/build/core/typescript-reference-worker.d.ts +2 -0
- package/build/core/typescript-reference-worker.d.ts.map +1 -0
- package/build/core/typescript-reference-worker.js +14 -0
- package/build/core/typescript-reference-worker.js.map +1 -0
- package/build/core/typescript-references.d.ts +71 -0
- package/build/core/typescript-references.d.ts.map +1 -0
- package/build/core/typescript-references.js +392 -0
- package/build/core/typescript-references.js.map +1 -0
- package/build/core/typescript-skeleton.d.ts +5 -0
- package/build/core/typescript-skeleton.d.ts.map +1 -0
- package/build/core/typescript-skeleton.js +49 -0
- package/build/core/typescript-skeleton.js.map +1 -0
- package/build/core/walker.d.ts +16 -0
- package/build/core/walker.d.ts.map +1 -0
- package/build/core/walker.js +152 -0
- package/build/core/walker.js.map +1 -0
- package/build/core/workspace-router.d.ts +28 -0
- package/build/core/workspace-router.d.ts.map +1 -0
- package/build/core/workspace-router.js +93 -0
- package/build/core/workspace-router.js.map +1 -0
- package/build/core/worktree-gc.d.ts +16 -0
- package/build/core/worktree-gc.d.ts.map +1 -0
- package/build/core/worktree-gc.js +69 -0
- package/build/core/worktree-gc.js.map +1 -0
- package/build/core/worktree-overlay.d.ts +22 -0
- package/build/core/worktree-overlay.d.ts.map +1 -0
- package/build/core/worktree-overlay.js +84 -0
- package/build/core/worktree-overlay.js.map +1 -0
- package/build/core/worktree-root-guard.d.ts +8 -0
- package/build/core/worktree-root-guard.d.ts.map +1 -0
- package/build/core/worktree-root-guard.js +82 -0
- package/build/core/worktree-root-guard.js.map +1 -0
- package/build/core/worktree-seed.d.ts +14 -0
- package/build/core/worktree-seed.d.ts.map +1 -0
- package/build/core/worktree-seed.js +98 -0
- package/build/core/worktree-seed.js.map +1 -0
- package/build/git/shadow.d.ts +40 -0
- package/build/git/shadow.d.ts.map +1 -0
- package/build/git/shadow.js +298 -0
- package/build/git/shadow.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +599 -0
- package/build/index.js.map +1 -0
- package/build/tools/agentic-context.d.ts +86 -0
- package/build/tools/agentic-context.d.ts.map +1 -0
- package/build/tools/agentic-context.js +1546 -0
- package/build/tools/agentic-context.js.map +1 -0
- package/build/tools/blast-radius.d.ts +24 -0
- package/build/tools/blast-radius.d.ts.map +1 -0
- package/build/tools/blast-radius.js +231 -0
- package/build/tools/blast-radius.js.map +1 -0
- package/build/tools/changed-since.d.ts +7 -0
- package/build/tools/changed-since.d.ts.map +1 -0
- package/build/tools/changed-since.js +68 -0
- package/build/tools/changed-since.js.map +1 -0
- package/build/tools/compiler-actions.d.ts +26 -0
- package/build/tools/compiler-actions.d.ts.map +1 -0
- package/build/tools/compiler-actions.js +121 -0
- package/build/tools/compiler-actions.js.map +1 -0
- package/build/tools/context-tree.d.ts +9 -0
- package/build/tools/context-tree.d.ts.map +1 -0
- package/build/tools/context-tree.js +131 -0
- package/build/tools/context-tree.js.map +1 -0
- package/build/tools/context.d.ts +37 -0
- package/build/tools/context.d.ts.map +1 -0
- package/build/tools/context.js +800 -0
- package/build/tools/context.js.map +1 -0
- package/build/tools/diff.d.ts +9 -0
- package/build/tools/diff.d.ts.map +1 -0
- package/build/tools/diff.js +176 -0
- package/build/tools/diff.js.map +1 -0
- package/build/tools/directory-prior.d.ts +16 -0
- package/build/tools/directory-prior.d.ts.map +1 -0
- package/build/tools/directory-prior.js +121 -0
- package/build/tools/directory-prior.js.map +1 -0
- package/build/tools/discovery-documents.d.ts +11 -0
- package/build/tools/discovery-documents.d.ts.map +1 -0
- package/build/tools/discovery-documents.js +71 -0
- package/build/tools/discovery-documents.js.map +1 -0
- package/build/tools/discovery-hints.d.ts +25 -0
- package/build/tools/discovery-hints.d.ts.map +1 -0
- package/build/tools/discovery-hints.js +329 -0
- package/build/tools/discovery-hints.js.map +1 -0
- package/build/tools/efficiency-report.d.ts +13 -0
- package/build/tools/efficiency-report.d.ts.map +1 -0
- package/build/tools/efficiency-report.js +78 -0
- package/build/tools/efficiency-report.js.map +1 -0
- package/build/tools/evidence-retrieval.d.ts +33 -0
- package/build/tools/evidence-retrieval.d.ts.map +1 -0
- package/build/tools/evidence-retrieval.js +200 -0
- package/build/tools/evidence-retrieval.js.map +1 -0
- package/build/tools/exact-references.d.ts +10 -0
- package/build/tools/exact-references.d.ts.map +1 -0
- package/build/tools/exact-references.js +349 -0
- package/build/tools/exact-references.js.map +1 -0
- package/build/tools/explain.d.ts +14 -0
- package/build/tools/explain.d.ts.map +1 -0
- package/build/tools/explain.js +370 -0
- package/build/tools/explain.js.map +1 -0
- package/build/tools/feature-brief.d.ts +7 -0
- package/build/tools/feature-brief.d.ts.map +1 -0
- package/build/tools/feature-brief.js +89 -0
- package/build/tools/feature-brief.js.map +1 -0
- package/build/tools/feature-hub.d.ts +15 -0
- package/build/tools/feature-hub.d.ts.map +1 -0
- package/build/tools/feature-hub.js +404 -0
- package/build/tools/feature-hub.js.map +1 -0
- package/build/tools/file-skeleton.d.ts +7 -0
- package/build/tools/file-skeleton.d.ts.map +1 -0
- package/build/tools/file-skeleton.js +190 -0
- package/build/tools/file-skeleton.js.map +1 -0
- package/build/tools/grep.d.ts +25 -0
- package/build/tools/grep.d.ts.map +1 -0
- package/build/tools/grep.js +341 -0
- package/build/tools/grep.js.map +1 -0
- package/build/tools/impact-delta.d.ts +7 -0
- package/build/tools/impact-delta.d.ts.map +1 -0
- package/build/tools/impact-delta.js +102 -0
- package/build/tools/impact-delta.js.map +1 -0
- package/build/tools/memory-tools.d.ts +69 -0
- package/build/tools/memory-tools.d.ts.map +1 -0
- package/build/tools/memory-tools.js +366 -0
- package/build/tools/memory-tools.js.map +1 -0
- package/build/tools/planning-helpers.d.ts +26 -0
- package/build/tools/planning-helpers.d.ts.map +1 -0
- package/build/tools/planning-helpers.js +269 -0
- package/build/tools/planning-helpers.js.map +1 -0
- package/build/tools/project-graph.d.ts +19 -0
- package/build/tools/project-graph.d.ts.map +1 -0
- package/build/tools/project-graph.js +259 -0
- package/build/tools/project-graph.js.map +1 -0
- package/build/tools/propose-commit.d.ts +9 -0
- package/build/tools/propose-commit.d.ts.map +1 -0
- package/build/tools/propose-commit.js +156 -0
- package/build/tools/propose-commit.js.map +1 -0
- package/build/tools/register-tools.d.ts +18 -0
- package/build/tools/register-tools.d.ts.map +1 -0
- package/build/tools/register-tools.js +1131 -0
- package/build/tools/register-tools.js.map +1 -0
- package/build/tools/reindex.d.ts +76 -0
- package/build/tools/reindex.d.ts.map +1 -0
- package/build/tools/reindex.js +871 -0
- package/build/tools/reindex.js.map +1 -0
- package/build/tools/related.d.ts +6 -0
- package/build/tools/related.d.ts.map +1 -0
- package/build/tools/related.js +143 -0
- package/build/tools/related.js.map +1 -0
- package/build/tools/repo-map.d.ts +59 -0
- package/build/tools/repo-map.d.ts.map +1 -0
- package/build/tools/repo-map.js +270 -0
- package/build/tools/repo-map.js.map +1 -0
- package/build/tools/resolve-symbol.d.ts +41 -0
- package/build/tools/resolve-symbol.d.ts.map +1 -0
- package/build/tools/resolve-symbol.js +250 -0
- package/build/tools/resolve-symbol.js.map +1 -0
- package/build/tools/retrieval-helpers.d.ts +35 -0
- package/build/tools/retrieval-helpers.d.ts.map +1 -0
- package/build/tools/retrieval-helpers.js +302 -0
- package/build/tools/retrieval-helpers.js.map +1 -0
- package/build/tools/scip-index.d.ts +12 -0
- package/build/tools/scip-index.d.ts.map +1 -0
- package/build/tools/scip-index.js +66 -0
- package/build/tools/scip-index.js.map +1 -0
- package/build/tools/scoped-discovery.d.ts +7 -0
- package/build/tools/scoped-discovery.d.ts.map +1 -0
- package/build/tools/scoped-discovery.js +89 -0
- package/build/tools/scoped-discovery.js.map +1 -0
- package/build/tools/search.d.ts +62 -0
- package/build/tools/search.d.ts.map +1 -0
- package/build/tools/search.js +419 -0
- package/build/tools/search.js.map +1 -0
- package/build/tools/semantic-diff.d.ts +7 -0
- package/build/tools/semantic-diff.d.ts.map +1 -0
- package/build/tools/semantic-diff.js +45 -0
- package/build/tools/semantic-diff.js.map +1 -0
- package/build/tools/semantic-identifiers.d.ts +51 -0
- package/build/tools/semantic-identifiers.d.ts.map +1 -0
- package/build/tools/semantic-identifiers.js +522 -0
- package/build/tools/semantic-identifiers.js.map +1 -0
- package/build/tools/semantic-navigate.d.ts +18 -0
- package/build/tools/semantic-navigate.d.ts.map +1 -0
- package/build/tools/semantic-navigate.js +413 -0
- package/build/tools/semantic-navigate.js.map +1 -0
- package/build/tools/semantic-search.d.ts +30 -0
- package/build/tools/semantic-search.d.ts.map +1 -0
- package/build/tools/semantic-search.js +466 -0
- package/build/tools/semantic-search.js.map +1 -0
- package/build/tools/sql-schema.d.ts +23 -0
- package/build/tools/sql-schema.d.ts.map +1 -0
- package/build/tools/sql-schema.js +155 -0
- package/build/tools/sql-schema.js.map +1 -0
- package/build/tools/static-analysis.d.ts +7 -0
- package/build/tools/static-analysis.d.ts.map +1 -0
- package/build/tools/static-analysis.js +331 -0
- package/build/tools/static-analysis.js.map +1 -0
- package/build/tools/status.d.ts +74 -0
- package/build/tools/status.d.ts.map +1 -0
- package/build/tools/status.js +227 -0
- package/build/tools/status.js.map +1 -0
- package/build/tools/symbolic-edit.d.ts +57 -0
- package/build/tools/symbolic-edit.d.ts.map +1 -0
- package/build/tools/symbolic-edit.js +500 -0
- package/build/tools/symbolic-edit.js.map +1 -0
- package/build/tools/test-impact.d.ts +7 -0
- package/build/tools/test-impact.d.ts.map +1 -0
- package/build/tools/test-impact.js +34 -0
- package/build/tools/test-impact.js.map +1 -0
- package/build/tools/test-map.d.ts +8 -0
- package/build/tools/test-map.d.ts.map +1 -0
- package/build/tools/test-map.js +75 -0
- package/build/tools/test-map.js.map +1 -0
- package/docs/README.md +26 -0
- package/docs/architecture.md +35 -0
- package/docs/configuration.md +178 -0
- package/docs/maintenance.md +77 -0
- package/package.json +80 -4
|
@@ -0,0 +1,1546 @@
|
|
|
1
|
+
// Task-oriented retrieval and memory tools for multi-project agent workflows
|
|
2
|
+
// FEATURE: High-leverage agent context assembly over project intelligence data
|
|
3
|
+
import { readFile, stat } from "fs/promises";
|
|
4
|
+
import { resolveProjectPath } from "../core/path-guard.js";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { bindEvidence, createValidationSnapshot, validationState, memoryEvidenceStatus, evidenceConfigurationFiles } from "../core/evidence-state.js";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { embedMemoryText, embeddingCacheIdentity, withQueryEmbeddingCache, currentEmbeddingModel } from "../core/embeddings.js";
|
|
10
|
+
import { getProvider, getEmbedModelName } from "../core/provider-config.js";
|
|
11
|
+
import { getDB } from "../core/db.js";
|
|
12
|
+
import { createPromotedPattern, listPromotedPatterns, listRegisteredWorkspaces, loadProjectConfig } from "../core/project-config.js";
|
|
13
|
+
import { unifiedSearchData } from "./search.js";
|
|
14
|
+
import { join, resolve } from "path";
|
|
15
|
+
import { persistArtifact, lookupFreshArtifact, readArtifact, buildArtifactId } from "../core/artifacts.js";
|
|
16
|
+
import { buildFreshnessMetadata, buildTrustMetadata, getIndexState } from "../core/context-metadata.js";
|
|
17
|
+
import { resolveSymbolData } from "./resolve-symbol.js";
|
|
18
|
+
import { unique, parseJsonArray, findTestsForFiles, getRecentDiffSummary, getProjectMemoryHits, resolveFilesFromSymbols, scanExactMentions, fileRisk, hubMatchesForFiles, isTestFile, } from "./retrieval-helpers.js";
|
|
19
|
+
import { buildExactIdentifierPathSet, rerankPlanningFiles, maybeChatRerankPlanningFiles, upsertRanked, isSourcePath, isTestPath, } from "./planning-helpers.js";
|
|
20
|
+
import { findDirectoryPriorMatches } from "./directory-prior.js";
|
|
21
|
+
import { connectedRetrievalEnabled, retrieveEvidence } from "./evidence-retrieval.js";
|
|
22
|
+
import { collectSourceEvidence } from "./context.js";
|
|
23
|
+
import { chatModelIdentity } from "../core/chat-model.js";
|
|
24
|
+
// The cache identity for a task pack: same task + same option flags → same artifact. The repo
|
|
25
|
+
// fingerprint and index generation gate freshness separately (handled by lookupFreshArtifact).
|
|
26
|
+
async function taskPackArtifactKey(task, maxFiles, opts) {
|
|
27
|
+
const connected = connectedRetrievalEnabled();
|
|
28
|
+
const graphHops = opts.graphHops ?? (connected ? 1 : 0);
|
|
29
|
+
return createHash("sha256").update(JSON.stringify({
|
|
30
|
+
task,
|
|
31
|
+
connected,
|
|
32
|
+
retrievalVersion: 53,
|
|
33
|
+
adaptive: process.env.CONTEXTPRO_ADAPTIVE_CONTEXT === "true",
|
|
34
|
+
rerank: process.env.CONTEXTPRO_CHAT_RERANK === "true",
|
|
35
|
+
chatModel: process.env.CONTEXTPRO_CHAT_RERANK === "true" ? await chatModelIdentity(opts.rootDir) : null,
|
|
36
|
+
embeddingModel: `${getProvider()}:${getEmbedModelName()}`,
|
|
37
|
+
embeddingState: embeddingCacheIdentity(await getDB(opts.rootDir)),
|
|
38
|
+
preset: opts.preset ?? "balanced",
|
|
39
|
+
graphHops,
|
|
40
|
+
graphMaxFiles: graphHops ? (connected ? Math.max(0, Math.min(opts.graphMaxFiles ?? 8, 16)) : Math.max(1, Math.min(opts.graphMaxFiles ?? 4, 10))) : 0,
|
|
41
|
+
// Connected payloads already contain source selected under this budget.
|
|
42
|
+
// A later larger request must recompute rather than reuse the small packet.
|
|
43
|
+
sourceBudget: connected ? Math.max(512, Math.floor((opts.maxTokens ?? 3000) * .65)) : null,
|
|
44
|
+
maxFiles,
|
|
45
|
+
includeTests: opts.includeTests !== false,
|
|
46
|
+
includeRecentDiffs: opts.includeRecentDiffs !== false,
|
|
47
|
+
includeMemory: opts.includeMemory !== false,
|
|
48
|
+
// maxMemory changes how many memory hits the pack carries, so it MUST be part of the cache
|
|
49
|
+
// identity — otherwise two calls differing only in maxMemory would collide on one cached pack.
|
|
50
|
+
maxMemory: opts.includeMemory === false ? 0 : Math.min(opts.maxMemory ?? 3, 10),
|
|
51
|
+
})).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
function budgetTaskPackPayload(payload, maxTokens, render) {
|
|
54
|
+
const budget = Math.max(512, Math.min(maxTokens ?? 3000, 12_000));
|
|
55
|
+
const maxBytes = budget * 4;
|
|
56
|
+
const original = {
|
|
57
|
+
files: payload.files.length,
|
|
58
|
+
relatedFiles: payload.relatedFiles?.length ?? 0,
|
|
59
|
+
tests: payload.tests.length,
|
|
60
|
+
recentDiffs: payload.recentDiffs.length,
|
|
61
|
+
memory: payload.memory.length,
|
|
62
|
+
symbols: payload.files.reduce((sum, file) => sum + file.symbols.length, 0),
|
|
63
|
+
risks: payload.risks.length,
|
|
64
|
+
sourceRanges: payload.source?.length ?? 0,
|
|
65
|
+
};
|
|
66
|
+
const result = {
|
|
67
|
+
...structuredClone(payload),
|
|
68
|
+
files: payload.files.map((file) => ({ ...file, symbols: file.symbols.slice() })),
|
|
69
|
+
tests: payload.tests.slice(),
|
|
70
|
+
recentDiffs: payload.recentDiffs.slice(),
|
|
71
|
+
memory: payload.memory.slice(),
|
|
72
|
+
risks: payload.risks.slice(),
|
|
73
|
+
};
|
|
74
|
+
if (payload.trust && typeof payload.trust === "object" && !Array.isArray(payload.trust)) {
|
|
75
|
+
const trust = payload.trust;
|
|
76
|
+
result.trust = {
|
|
77
|
+
...trust,
|
|
78
|
+
evidence: Array.isArray(trust.evidence) ? trust.evidence.slice() : trust.evidence,
|
|
79
|
+
ambiguities: Array.isArray(trust.ambiguities) ? trust.ambiguities.slice() : trust.ambiguities,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let droppedMetadataFields = 0;
|
|
83
|
+
const pruneNext = () => {
|
|
84
|
+
if (result.recentDiffs.length > 0)
|
|
85
|
+
result.recentDiffs.pop();
|
|
86
|
+
else if ((result.relatedFiles?.length ?? 0) > 0)
|
|
87
|
+
result.relatedFiles.pop();
|
|
88
|
+
else if (result.memory.length > 0)
|
|
89
|
+
result.memory.pop();
|
|
90
|
+
else if (result.tests.length > 0)
|
|
91
|
+
result.tests.pop();
|
|
92
|
+
else {
|
|
93
|
+
const verboseFile = [...result.files].reverse().find((file) => file.symbols.length > 0);
|
|
94
|
+
if (verboseFile)
|
|
95
|
+
verboseFile.symbols.pop();
|
|
96
|
+
// Preserve the two strongest files: task packs commonly need both the entry point
|
|
97
|
+
// and the implementation file, and dropping the latter makes a tiny pack useless.
|
|
98
|
+
else if (result.files.length > 2)
|
|
99
|
+
result.files.pop();
|
|
100
|
+
else if ((result.source?.length ?? 0) > 1)
|
|
101
|
+
result.source.pop();
|
|
102
|
+
else if (result.risks.length > 1)
|
|
103
|
+
result.risks.pop();
|
|
104
|
+
else {
|
|
105
|
+
const trust = result.trust;
|
|
106
|
+
// Freshness is already present at the payload's top level, so this is the first
|
|
107
|
+
// metadata field to remove under an extremely tight budget.
|
|
108
|
+
if (trust && "freshness" in trust) {
|
|
109
|
+
delete trust.freshness;
|
|
110
|
+
droppedMetadataFields += 1;
|
|
111
|
+
}
|
|
112
|
+
else if (trust && Array.isArray(trust.evidence) && trust.evidence.length > 1) {
|
|
113
|
+
trust.evidence.pop();
|
|
114
|
+
droppedMetadataFields += 1;
|
|
115
|
+
}
|
|
116
|
+
else if (result.files.some((file) => file.reason.length > 0)) {
|
|
117
|
+
result.files = result.files.map((file) => ({ ...file, reason: "" }));
|
|
118
|
+
droppedMetadataFields += 1;
|
|
119
|
+
}
|
|
120
|
+
else if (result.summary.length > 0) {
|
|
121
|
+
result.summary = "";
|
|
122
|
+
droppedMetadataFields += 1;
|
|
123
|
+
}
|
|
124
|
+
else if (result.trust) {
|
|
125
|
+
delete result.trust;
|
|
126
|
+
droppedMetadataFields += 1;
|
|
127
|
+
}
|
|
128
|
+
else if (result.freshness) {
|
|
129
|
+
delete result.freshness;
|
|
130
|
+
droppedMetadataFields += 1;
|
|
131
|
+
}
|
|
132
|
+
else if (result.repo) {
|
|
133
|
+
delete result.repo;
|
|
134
|
+
droppedMetadataFields += 1;
|
|
135
|
+
}
|
|
136
|
+
else if (result.risks.length) {
|
|
137
|
+
result.risks.pop();
|
|
138
|
+
}
|
|
139
|
+
else if (result.source?.length) {
|
|
140
|
+
result.source.pop();
|
|
141
|
+
}
|
|
142
|
+
else if (result.files.length) {
|
|
143
|
+
result.files.pop();
|
|
144
|
+
}
|
|
145
|
+
else if (result.delta) {
|
|
146
|
+
// An incomplete delta is unsafe to apply. Explicitly require reading the full snapshot.
|
|
147
|
+
delete result.delta;
|
|
148
|
+
result.context = { ...result.context, mode: "full_required", reason: "delta_exceeds_budget" };
|
|
149
|
+
droppedMetadataFields += 1;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
};
|
|
158
|
+
const buildReport = (estimatedTokens) => {
|
|
159
|
+
const retainedSymbols = result.files.reduce((sum, file) => sum + file.symbols.length, 0);
|
|
160
|
+
return {
|
|
161
|
+
maxTokens: budget,
|
|
162
|
+
estimator: "utf8_bytes_div_4",
|
|
163
|
+
estimatedTokens,
|
|
164
|
+
truncated: original.files !== result.files.length
|
|
165
|
+
|| original.relatedFiles !== (result.relatedFiles?.length ?? 0)
|
|
166
|
+
|| original.tests !== result.tests.length
|
|
167
|
+
|| original.recentDiffs !== result.recentDiffs.length
|
|
168
|
+
|| original.memory !== result.memory.length
|
|
169
|
+
|| original.symbols !== retainedSymbols
|
|
170
|
+
|| original.risks !== result.risks.length
|
|
171
|
+
|| original.sourceRanges !== (result.source?.length ?? 0)
|
|
172
|
+
|| droppedMetadataFields > 0,
|
|
173
|
+
dropped: {
|
|
174
|
+
files: original.files - result.files.length,
|
|
175
|
+
relatedFiles: original.relatedFiles - (result.relatedFiles?.length ?? 0),
|
|
176
|
+
tests: original.tests - result.tests.length,
|
|
177
|
+
recentDiffs: original.recentDiffs - result.recentDiffs.length,
|
|
178
|
+
memory: original.memory - result.memory.length,
|
|
179
|
+
symbols: original.symbols - retainedSymbols,
|
|
180
|
+
risks: original.risks - result.risks.length,
|
|
181
|
+
metadataFields: droppedMetadataFields,
|
|
182
|
+
...(payload.source ? { sourceRanges: original.sourceRanges - (result.source?.length ?? 0) } : {}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
while (true) {
|
|
187
|
+
let estimatedTokens = 0;
|
|
188
|
+
let candidate = { ...result, budget: buildReport(estimatedTokens) };
|
|
189
|
+
// The estimate changes its own digit count at most a couple of times.
|
|
190
|
+
for (let iteration = 0; iteration < 3; iteration += 1) {
|
|
191
|
+
estimatedTokens = Math.ceil(Buffer.byteLength(render(candidate), "utf8") / 4);
|
|
192
|
+
candidate = { ...result, budget: buildReport(estimatedTokens) };
|
|
193
|
+
}
|
|
194
|
+
if (Buffer.byteLength(render(candidate), "utf8") <= maxBytes)
|
|
195
|
+
return candidate;
|
|
196
|
+
if (!pruneNext()) {
|
|
197
|
+
throw new Error("Task pack response cannot fit the requested UTF-8 byte budget; use a larger max_tokens.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function normalizeConfidence(score) {
|
|
202
|
+
return Math.max(0.1, Math.min(0.99, Number(score.toFixed(2))));
|
|
203
|
+
}
|
|
204
|
+
function buildRepoMetadata(input) {
|
|
205
|
+
return {
|
|
206
|
+
root: input.rootDir,
|
|
207
|
+
projectId: input.projectId ?? null,
|
|
208
|
+
clientId: input.clientId ?? null,
|
|
209
|
+
fingerprint: input.fingerprint ?? null,
|
|
210
|
+
staleFiles: input.staleFiles,
|
|
211
|
+
indexGeneration: input.indexGeneration,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function persistAgentArtifact(input) {
|
|
215
|
+
return persistArtifact({
|
|
216
|
+
rootDir: input.rootDir,
|
|
217
|
+
kind: input.kind,
|
|
218
|
+
key: input.key,
|
|
219
|
+
repoFingerprint: input.repo.fingerprint,
|
|
220
|
+
projectId: input.repo.projectId,
|
|
221
|
+
clientId: input.repo.clientId,
|
|
222
|
+
indexGeneration: input.repo.indexGeneration,
|
|
223
|
+
metadata: input.metadata,
|
|
224
|
+
payload: input.payload,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function inferProjectSummary(task, filePaths) {
|
|
228
|
+
const focus = filePaths.length > 0 ? filePaths.slice(0, 3).join(", ") : "no files ranked yet";
|
|
229
|
+
return `${task} likely centers on ${focus}.`;
|
|
230
|
+
}
|
|
231
|
+
function formatMaybeJson(format, value, textRenderer) {
|
|
232
|
+
if (format === "json")
|
|
233
|
+
return JSON.stringify(value, null, 2);
|
|
234
|
+
return textRenderer();
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Merge directory-purpose / Protocol-implementation prior matches into a candidate set.
|
|
238
|
+
* For "add a new X for Y" tasks, the directory hosting sibling implementations is surfaced even
|
|
239
|
+
* when those files didn't make the semantic top-K. Prior matches get a high base score so they
|
|
240
|
+
* outrank generic single-token matches like status.py.
|
|
241
|
+
*/
|
|
242
|
+
async function applyDirectoryPrior(rootDir, task, candidates) {
|
|
243
|
+
const priors = await findDirectoryPriorMatches(rootDir, task).catch(() => []);
|
|
244
|
+
if (priors.length === 0)
|
|
245
|
+
return { candidates, priorPaths: [] };
|
|
246
|
+
const byPath = new Map(candidates.map((c) => [c.path, c]));
|
|
247
|
+
for (const prior of priors) {
|
|
248
|
+
const existing = byPath.get(prior.path);
|
|
249
|
+
// Prior boost maps to a strong base score (0.7–0.85) so implementation-directory files
|
|
250
|
+
// clearly outrank generic keyword hits.
|
|
251
|
+
const priorScore = 0.7 + prior.boost * 0.3;
|
|
252
|
+
if (existing) {
|
|
253
|
+
byPath.set(prior.path, {
|
|
254
|
+
...existing,
|
|
255
|
+
score: Math.max(existing.score, priorScore),
|
|
256
|
+
reason: `${existing.reason}; ${prior.reason}`,
|
|
257
|
+
symbols: existing.symbols.length > 0 ? existing.symbols : prior.symbols,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
byPath.set(prior.path, {
|
|
262
|
+
path: prior.path,
|
|
263
|
+
reason: prior.reason,
|
|
264
|
+
score: priorScore,
|
|
265
|
+
symbols: prior.symbols,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { candidates: [...byPath.values()], priorPaths: priors.map((p) => p.path) };
|
|
270
|
+
}
|
|
271
|
+
function weightForFileRole(path) {
|
|
272
|
+
if (isTestFile(path))
|
|
273
|
+
return 0.85;
|
|
274
|
+
if (/\.(json|toml|yaml|yml|ini|cfg)$/i.test(path))
|
|
275
|
+
return 0.65;
|
|
276
|
+
if (/(^|\/)(README|TODO|CHANGELOG|CONTRIBUTING)(\.md)?$/i.test(path))
|
|
277
|
+
return 0.6;
|
|
278
|
+
if (/(^|\/)(src|app|lib|packages)\//.test(path))
|
|
279
|
+
return 0.95;
|
|
280
|
+
return 0.75;
|
|
281
|
+
}
|
|
282
|
+
async function readPackageScripts(rootDir) {
|
|
283
|
+
try {
|
|
284
|
+
const raw = await readFile(join(rootDir, "package.json"), "utf-8");
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
return parsed.scripts ?? {};
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function fileExists(path) {
|
|
293
|
+
try {
|
|
294
|
+
await readFile(path);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function detectPythonProfile(rootDir) {
|
|
302
|
+
const profile = {
|
|
303
|
+
isUvManaged: false,
|
|
304
|
+
isPoetryManaged: false,
|
|
305
|
+
hasPyprojectToml: false,
|
|
306
|
+
hasVenv: false,
|
|
307
|
+
hasPytestConfig: false,
|
|
308
|
+
hasRuffConfig: false,
|
|
309
|
+
hasMypyConfig: false,
|
|
310
|
+
testRoots: [],
|
|
311
|
+
};
|
|
312
|
+
const pyproject = join(rootDir, "pyproject.toml");
|
|
313
|
+
if (await fileExists(pyproject)) {
|
|
314
|
+
profile.hasPyprojectToml = true;
|
|
315
|
+
try {
|
|
316
|
+
const raw = await readFile(pyproject, "utf-8");
|
|
317
|
+
if (/\[tool\.pytest\.ini_options\]/.test(raw))
|
|
318
|
+
profile.hasPytestConfig = true;
|
|
319
|
+
if (/\[tool\.ruff/.test(raw))
|
|
320
|
+
profile.hasRuffConfig = true;
|
|
321
|
+
if (/\[tool\.mypy\]/.test(raw))
|
|
322
|
+
profile.hasMypyConfig = true;
|
|
323
|
+
const testpathsMatch = raw.match(/testpaths\s*=\s*\[([^\]]*)\]/);
|
|
324
|
+
if (testpathsMatch) {
|
|
325
|
+
const items = testpathsMatch[1].match(/"([^"]+)"|'([^']+)'/g) ?? [];
|
|
326
|
+
profile.testRoots = items.map((item) => item.replace(/^['"]|['"]$/g, ""));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// ignore parse errors, profile fields stay at defaults
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (await fileExists(join(rootDir, "uv.lock")))
|
|
334
|
+
profile.isUvManaged = true;
|
|
335
|
+
if (await fileExists(join(rootDir, "poetry.lock")))
|
|
336
|
+
profile.isPoetryManaged = true;
|
|
337
|
+
if (await fileExists(join(rootDir, ".venv", "bin", "python")) || await fileExists(join(rootDir, ".venv", "Scripts", "python.exe"))) {
|
|
338
|
+
profile.hasVenv = true;
|
|
339
|
+
}
|
|
340
|
+
if (await fileExists(join(rootDir, "pytest.ini")) || await fileExists(join(rootDir, "setup.cfg"))) {
|
|
341
|
+
profile.hasPytestConfig = true;
|
|
342
|
+
}
|
|
343
|
+
if (await fileExists(join(rootDir, "ruff.toml")) || await fileExists(join(rootDir, ".ruff.toml"))) {
|
|
344
|
+
profile.hasRuffConfig = true;
|
|
345
|
+
}
|
|
346
|
+
if (await fileExists(join(rootDir, "mypy.ini"))) {
|
|
347
|
+
profile.hasMypyConfig = true;
|
|
348
|
+
}
|
|
349
|
+
return profile;
|
|
350
|
+
}
|
|
351
|
+
function pythonRunCommand(profile, tool, rootDir) {
|
|
352
|
+
if (profile.hasVenv) {
|
|
353
|
+
return `.venv/bin/${tool}`;
|
|
354
|
+
}
|
|
355
|
+
if (profile.isUvManaged)
|
|
356
|
+
return `uv run ${tool}`;
|
|
357
|
+
if (profile.isPoetryManaged)
|
|
358
|
+
return `poetry run ${tool}`;
|
|
359
|
+
return tool;
|
|
360
|
+
}
|
|
361
|
+
async function expandTaskImportGraph(rootDir, seeds, hops, maxFiles) {
|
|
362
|
+
if (hops <= 0)
|
|
363
|
+
return [];
|
|
364
|
+
const db = await getDB(rootDir);
|
|
365
|
+
const seen = new Set(seeds);
|
|
366
|
+
const related = [];
|
|
367
|
+
let frontier = seeds.slice(0, 3);
|
|
368
|
+
for (let depth = 0; depth < Math.min(hops, 2) && frontier.length; depth++) {
|
|
369
|
+
const next = [];
|
|
370
|
+
for (const seed of frontier) {
|
|
371
|
+
const file = db.getFile(seed);
|
|
372
|
+
if (!file)
|
|
373
|
+
continue;
|
|
374
|
+
const edges = [
|
|
375
|
+
...db.getImportsFrom(file.id).map((edge) => ({ path: edge.target_file_id ? db.getFileById(edge.target_file_id)?.path : null, reason: `Imported by ${seed}` })),
|
|
376
|
+
...db.getImportersOf(file.id).map((edge) => ({ path: db.getFileById(edge.source_file_id)?.path, reason: `Imports ${seed}` })),
|
|
377
|
+
].sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
378
|
+
for (const edge of edges) {
|
|
379
|
+
if (!edge.path || seen.has(edge.path))
|
|
380
|
+
continue;
|
|
381
|
+
seen.add(edge.path);
|
|
382
|
+
related.push({ path: edge.path, reason: `${edge.reason}; indexed import edge, parser/resolver heuristic (${depth + 1} hop)`, score: 0.5 / (depth + 1), symbols: [] });
|
|
383
|
+
next.push(edge.path);
|
|
384
|
+
if (related.length >= maxFiles)
|
|
385
|
+
return related;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
frontier = next;
|
|
389
|
+
}
|
|
390
|
+
return related;
|
|
391
|
+
}
|
|
392
|
+
export function buildTaskPack(options) {
|
|
393
|
+
return withQueryEmbeddingCache(() => buildTaskPackUncached(options));
|
|
394
|
+
}
|
|
395
|
+
async function buildTaskPackUncached(options) {
|
|
396
|
+
if (options.previousContextId && !/^task_context-[a-f0-9]{14}$/.test(options.previousContextId))
|
|
397
|
+
throw new Error("Invalid previous_context_id; use the task_context ID from a previous response.");
|
|
398
|
+
const maxFiles = Math.max(1, Math.floor(options.maxFiles ?? 8));
|
|
399
|
+
const taskKey = await taskPackArtifactKey(options.task, maxFiles, options);
|
|
400
|
+
const db = await getDB(options.rootDir);
|
|
401
|
+
let memoryRevision = options.includeMemory === false ? 0 : db.getMemoryRevision();
|
|
402
|
+
const gitHead = options.includeRecentDiffs === false ? "" : await promisify(execFile)("git", ["rev-parse", "HEAD"], { cwd: options.rootDir, timeout: 3000 }).then(({ stdout }) => stdout.trim()).catch(() => "");
|
|
403
|
+
let artifactKey = createHash("sha256").update(`${taskKey}:${memoryRevision}:${gitHead}`).digest("hex");
|
|
404
|
+
// READ-THROUGH CACHE: if a task pack for this exact task+flags was already computed and the index
|
|
405
|
+
// hasn't advanced since, re-render the stored payload instead of recomputing the whole pipeline
|
|
406
|
+
// (search + directory-prior + rerank + tests + diffs + memory). This makes a repeated task_pack
|
|
407
|
+
// call genuinely cheap — the artifact is the source of truth, not just a side-effect record.
|
|
408
|
+
// Best-effort: any failure resolving identity falls through to a normal (re)compute.
|
|
409
|
+
try {
|
|
410
|
+
const config = await loadProjectConfig(options.rootDir).catch(() => null);
|
|
411
|
+
const indexState = await getIndexState(options.rootDir);
|
|
412
|
+
// Only serve from cache when the index is genuinely FRESH. The generation number alone is not
|
|
413
|
+
// enough: adding/removing/editing a source file without reindexing leaves the generation
|
|
414
|
+
// unchanged but flips state to "stale" — serving the pre-change pack would be wrong. A non-fresh
|
|
415
|
+
// index always recomputes (and the recompute reflects the current on-disk reality).
|
|
416
|
+
const cached = indexState.state !== "fresh" ? null : await lookupFreshArtifact(options.rootDir, "task_pack", artifactKey, config?.repoFingerprint ?? null, indexState.generation);
|
|
417
|
+
const memoryUsable = cached?.payload?.memory ? (await Promise.all(cached.payload.memory.map(async (hit) => {
|
|
418
|
+
const node = db.getMemoryNode(hit.id);
|
|
419
|
+
if (!node)
|
|
420
|
+
return false;
|
|
421
|
+
const status = await memoryEvidenceStatus(options.rootDir, node);
|
|
422
|
+
return status !== "stale" && status !== "superseded";
|
|
423
|
+
}))).every(Boolean) : true;
|
|
424
|
+
if (cached?.payload?.files && memoryUsable) {
|
|
425
|
+
const trust = buildTrustMetadata({
|
|
426
|
+
baseConfidence: cached.payload.files.length > 0 ? 0.79 : 0.34,
|
|
427
|
+
freshness: buildFreshnessMetadata(indexState, { staleCount: 0, refreshed: false, warning: null }),
|
|
428
|
+
evidence: [...((cached.payload.trust?.evidence) ?? []), "served from artifact cache (no recompute)"],
|
|
429
|
+
ambiguities: (cached.payload.trust?.ambiguities) ?? [],
|
|
430
|
+
artifact: { id: cached.id, kind: cached.kind, key: cached.key, path: cached.path, createdAt: cached.createdAt, updatedAt: cached.updatedAt, indexGeneration: cached.indexGeneration },
|
|
431
|
+
});
|
|
432
|
+
const cachedPayload = { ...cached.payload, trust };
|
|
433
|
+
return finishTaskPack(options, cachedPayload, taskKey, memoryRevision, cached.repoFingerprint);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Cache lookup failed (e.g. corrupt manifest, DB not ready) — fall through to recompute.
|
|
438
|
+
}
|
|
439
|
+
const connected = connectedRetrievalEnabled() ? await retrieveEvidence({ rootDir: options.rootDir, query: options.task,
|
|
440
|
+
graphHops: options.graphHops, graphMaxFiles: options.graphMaxFiles, includeTests: options.includeTests, preset: options.preset }) : undefined;
|
|
441
|
+
const search = connected?.search ?? await unifiedSearchData({
|
|
442
|
+
rootDir: options.rootDir,
|
|
443
|
+
query: options.task,
|
|
444
|
+
mode: "hybrid",
|
|
445
|
+
topK: maxFiles,
|
|
446
|
+
persistArtifact: false,
|
|
447
|
+
expandCallers: false,
|
|
448
|
+
preset: options.preset,
|
|
449
|
+
});
|
|
450
|
+
const fileResults = search.files?.results ?? [];
|
|
451
|
+
const identifierResults = search.identifiers?.results ?? [];
|
|
452
|
+
const exactIdentifierPaths = buildExactIdentifierPathSet(search, options.task);
|
|
453
|
+
const baseTaskCandidates = connected?.candidates ?? fileResults.slice(0, Math.max(maxFiles, 8)).map((item) => ({
|
|
454
|
+
path: item.path,
|
|
455
|
+
reason: `Top semantic and structural match (${Math.round(item.semanticScore)} semantic / ${Math.round(item.keywordScore)} keyword)`,
|
|
456
|
+
score: item.score / 100,
|
|
457
|
+
symbols: item.matchedSymbols,
|
|
458
|
+
}));
|
|
459
|
+
const { candidates: taskCandidatesWithPrior } = await applyDirectoryPrior(options.rootDir, options.task, baseTaskCandidates);
|
|
460
|
+
const rankedTaskFiles = connected?.candidates ?? await maybeChatRerankPlanningFiles(options.rootDir, options.task, rerankPlanningFiles(taskCandidatesWithPrior, options.task, exactIdentifierPaths));
|
|
461
|
+
const topFiles = rankedTaskFiles.slice(0, maxFiles).map((item) => ({
|
|
462
|
+
path: item.path,
|
|
463
|
+
reason: item.reason,
|
|
464
|
+
score: item.score,
|
|
465
|
+
symbols: item.symbols,
|
|
466
|
+
}));
|
|
467
|
+
const filePaths = topFiles.map((item) => item.path);
|
|
468
|
+
const relatedFiles = connected ? connected.candidates.filter(item => !filePaths.includes(item.path) && item.roles.some(role => role === "caller" || role === "dependency")).slice(0, options.graphMaxFiles ?? 4)
|
|
469
|
+
: await expandTaskImportGraph(options.rootDir, filePaths, options.graphHops ?? 0, Math.max(1, Math.min(options.graphMaxFiles ?? 4, 10)));
|
|
470
|
+
const tests = options.includeTests === false ? [] : connected?.tests ?? await findTestsForFiles(options.rootDir, filePaths);
|
|
471
|
+
const source = connected ? await collectSourceEvidence({ rootDir: options.rootDir, query: options.task, retrieved: connected, connected: true,
|
|
472
|
+
retrievalPreset: options.preset, maxTokens: Math.max(512, Math.floor((options.maxTokens ?? 3000) * .65)) }) : undefined;
|
|
473
|
+
const recentDiffs = options.includeRecentDiffs === false
|
|
474
|
+
? []
|
|
475
|
+
: (await Promise.all(filePaths.slice(0, 5).map((path) => getRecentDiffSummary(options.rootDir, path)))).filter(Boolean);
|
|
476
|
+
// Capture before the asynchronous read: concurrent writes must invalidate, never bless stale memory.
|
|
477
|
+
memoryRevision = options.includeMemory === false ? 0 : db.getMemoryRevision();
|
|
478
|
+
const memory = options.includeMemory === false ? [] : await getProjectMemoryHits(options.rootDir, options.task, Math.min(options.maxMemory ?? 3, 10), options.preset === "keyword_only");
|
|
479
|
+
const risks = [
|
|
480
|
+
...identifierResults.slice(0, 2).filter((item) => item.totalCalls > 3).map((item) => `${item.name} has ${item.totalCalls} ranked call sites`),
|
|
481
|
+
...(tests.length === 0 ? ["No nearby tests found for top-ranked files"] : []),
|
|
482
|
+
...(search.freshness.warning ? [search.freshness.warning] : []),
|
|
483
|
+
];
|
|
484
|
+
const repo = buildRepoMetadata({
|
|
485
|
+
rootDir: search.repo.root,
|
|
486
|
+
projectId: search.repo.projectId,
|
|
487
|
+
clientId: search.repo.clientId,
|
|
488
|
+
fingerprint: search.repo.fingerprint,
|
|
489
|
+
staleFiles: search.freshness.staleCount,
|
|
490
|
+
indexGeneration: search.freshness.indexGeneration,
|
|
491
|
+
});
|
|
492
|
+
const evidence = [
|
|
493
|
+
...search.trust.evidence,
|
|
494
|
+
`${topFiles.length} ranked task files`,
|
|
495
|
+
...(tests.length > 0 ? [`${tests.length} nearby tests`] : []),
|
|
496
|
+
...(recentDiffs.length > 0 ? [`${recentDiffs.length} recent diffs`] : []),
|
|
497
|
+
...(memory.length > 0 ? [`${memory.length} project memory hits`] : []),
|
|
498
|
+
];
|
|
499
|
+
const ambiguities = [
|
|
500
|
+
...search.trust.ambiguities,
|
|
501
|
+
...(topFiles.length === 0 ? ["task pack could not rank any files"] : []),
|
|
502
|
+
...(topFiles.length > 1 && Math.abs((topFiles[0]?.score ?? 0) - (topFiles[1]?.score ?? 0)) < 0.06 ? ["top task files are close in score"] : []),
|
|
503
|
+
];
|
|
504
|
+
const partial = {
|
|
505
|
+
repo,
|
|
506
|
+
freshness: search.freshness,
|
|
507
|
+
trust: buildTrustMetadata({
|
|
508
|
+
baseConfidence: topFiles.length > 0 ? 0.79 : 0.34,
|
|
509
|
+
freshness: search.freshness,
|
|
510
|
+
evidence,
|
|
511
|
+
ambiguities,
|
|
512
|
+
artifact: null,
|
|
513
|
+
}),
|
|
514
|
+
summary: inferProjectSummary(options.task, filePaths),
|
|
515
|
+
files: topFiles,
|
|
516
|
+
...(connected || options.graphHops ? { relatedFiles } : {}),
|
|
517
|
+
...(source ? { source: source.ranges } : {}),
|
|
518
|
+
tests,
|
|
519
|
+
recentDiffs,
|
|
520
|
+
memory,
|
|
521
|
+
risks: [...risks, ...(source?.notices ?? [])],
|
|
522
|
+
};
|
|
523
|
+
// Structural refresh can update memory too; cache under the revision captured for retrieval.
|
|
524
|
+
artifactKey = createHash("sha256").update(`${taskKey}:${memoryRevision}:${gitHead}`).digest("hex");
|
|
525
|
+
const artifact = await persistAgentArtifact({
|
|
526
|
+
rootDir: options.rootDir,
|
|
527
|
+
kind: "task_pack",
|
|
528
|
+
key: artifactKey,
|
|
529
|
+
repo,
|
|
530
|
+
payload: partial,
|
|
531
|
+
metadata: {
|
|
532
|
+
task: options.task.slice(0, 500),
|
|
533
|
+
memoryRevision: String(memoryRevision),
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
const payload = {
|
|
537
|
+
...partial,
|
|
538
|
+
trust: buildTrustMetadata({
|
|
539
|
+
baseConfidence: topFiles.length > 0 ? 0.79 : 0.34,
|
|
540
|
+
freshness: search.freshness,
|
|
541
|
+
evidence,
|
|
542
|
+
ambiguities,
|
|
543
|
+
artifact,
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
return finishTaskPack(options, payload, taskKey, memoryRevision, search.repo.fingerprint);
|
|
547
|
+
}
|
|
548
|
+
// Snapshots include source content hashes, so a changed implementation is visible even when its
|
|
549
|
+
// ranking, path and symbols remain identical. IDs bind repository, task/options and delivery budget.
|
|
550
|
+
async function finishTaskPack(options, payload, taskKey, memoryRevision, fingerprint) {
|
|
551
|
+
const db = await getDB(options.rootDir);
|
|
552
|
+
const entries = {};
|
|
553
|
+
for (const file of payload.files)
|
|
554
|
+
entries[`file:${file.path}`] = { ...file, sourceHash: db.getFile(file.path)?.content_hash ?? null };
|
|
555
|
+
for (const file of payload.relatedFiles ?? [])
|
|
556
|
+
entries[`graph:${file.path}`] = { ...file, sourceHash: db.getFile(file.path)?.content_hash ?? null };
|
|
557
|
+
for (const test of payload.tests)
|
|
558
|
+
entries[`test:${test.path}`] = { ...test, sourceHash: db.getFile(test.path)?.content_hash ?? null };
|
|
559
|
+
for (const part of payload.source ?? [])
|
|
560
|
+
entries[`source:${part.file}:${part.start}-${part.end}`] = part;
|
|
561
|
+
for (const memory of payload.memory)
|
|
562
|
+
entries[`memory:${memory.id}`] = memory;
|
|
563
|
+
for (const diff of payload.recentDiffs)
|
|
564
|
+
if (diff)
|
|
565
|
+
entries[`diff:${diff.path}`] = diff;
|
|
566
|
+
payload.risks.forEach((risk, index) => { entries[`risk:${index}`] = risk; });
|
|
567
|
+
const generation = db.getIndexGeneration();
|
|
568
|
+
const key = createHash("sha256").update(JSON.stringify({ root: options.rootDir, taskKey, entries, generation, memoryRevision, maxTokens: options.maxTokens ?? 3000, format: options.format ?? "text", previousId: options.previousContextId ?? null })).digest("hex");
|
|
569
|
+
const contextId = buildArtifactId("task_context", key, fingerprint);
|
|
570
|
+
const previous = options.previousContextId ? await readArtifact(options.rootDir, options.previousContextId) : null;
|
|
571
|
+
const validBaseline = previous?.kind === "task_context" && previous.repoRoot === resolve(options.rootDir) && previous.repoFingerprint === fingerprint
|
|
572
|
+
&& previous.payload.taskKey === taskKey && previous.payload.complete === true;
|
|
573
|
+
const context = {
|
|
574
|
+
id: contextId,
|
|
575
|
+
mode: "full",
|
|
576
|
+
indexGeneration: generation,
|
|
577
|
+
memoryRevision,
|
|
578
|
+
recovery: "artifact_read({artifact_id: context.id}) returns the full snapshot; omit previous_context_id after compaction",
|
|
579
|
+
};
|
|
580
|
+
let delivery = { ...payload, context };
|
|
581
|
+
if (options.previousContextId && validBaseline) {
|
|
582
|
+
const before = previous.payload.entries;
|
|
583
|
+
const added = Object.entries(entries).filter(([id]) => !(id in before)).map(([id, value]) => ({ id, value }));
|
|
584
|
+
const changed = Object.entries(entries).filter(([id, value]) => id in before && JSON.stringify(before[id]) !== JSON.stringify(value)).map(([id, value]) => ({ id, value }));
|
|
585
|
+
const removed = Object.keys(before).filter((id) => !(id in entries));
|
|
586
|
+
context.mode = "delta";
|
|
587
|
+
context.previousId = options.previousContextId;
|
|
588
|
+
context.unchanged = added.length === 0 && changed.length === 0 && removed.length === 0;
|
|
589
|
+
delivery = { ...delivery, files: [], relatedFiles: [], tests: [], memory: [], recentDiffs: [], risks: [], ...(payload.source ? { source: [] } : {}), delta: { added, changed, removed } };
|
|
590
|
+
}
|
|
591
|
+
else if (options.previousContextId) {
|
|
592
|
+
context.fallbackReason = previous ? "unrelated_or_incomplete_baseline" : "baseline_not_found";
|
|
593
|
+
}
|
|
594
|
+
const budgeted = budgetTaskPackPayload(delivery, options.maxTokens, (value) => renderTaskPack(options.format, value));
|
|
595
|
+
// A pruned full packet cannot serve as a delta baseline: the agent never received every entry.
|
|
596
|
+
// Complete deltas can be chained, since the unchanged entries came from a complete baseline.
|
|
597
|
+
const complete = !budgeted.budget.truncated;
|
|
598
|
+
await persistArtifact({ rootDir: options.rootDir, kind: "task_context", key, repoFingerprint: fingerprint,
|
|
599
|
+
indexGeneration: generation, immutable: true, payload: { taskKey, payload, entries, complete } });
|
|
600
|
+
return renderTaskPack(options.format, budgeted);
|
|
601
|
+
}
|
|
602
|
+
function renderTaskPack(format, payload) {
|
|
603
|
+
const context = payload.context;
|
|
604
|
+
if (context?.unchanged) {
|
|
605
|
+
const freshness = payload.freshness;
|
|
606
|
+
const compact = {
|
|
607
|
+
context: { id: context.id, previousId: context.previousId, mode: "delta", unchanged: true, indexGeneration: context.indexGeneration, memoryRevision: context.memoryRevision },
|
|
608
|
+
freshness: { state: freshness?.indexState, warning: freshness?.warning ?? null },
|
|
609
|
+
delta: payload.delta,
|
|
610
|
+
files: [], relatedFiles: [], tests: [], memory: [], recentDiffs: [], risks: [],
|
|
611
|
+
budget: payload.budget,
|
|
612
|
+
};
|
|
613
|
+
if (format === "json")
|
|
614
|
+
return JSON.stringify(compact);
|
|
615
|
+
return `Context unchanged: ${context.id} (previous: ${context.previousId}; generation: ${context.indexGeneration}). Keep the previous packet.\nFreshness: ${freshness?.indexState ?? "unknown"}${freshness?.warning ? `; ${freshness.warning}` : ""}`;
|
|
616
|
+
}
|
|
617
|
+
if (format === "json")
|
|
618
|
+
return JSON.stringify(payload);
|
|
619
|
+
const lines = ["Task Pack", `Summary: ${payload.summary}`,
|
|
620
|
+
`Budget: ~${payload.budget.estimatedTokens}/${payload.budget.maxTokens} tokens (UTF-8 bytes / 4 estimate)${payload.budget.truncated ? " (truncated; read full context artifact)" : ""}`,
|
|
621
|
+
`Context: ${JSON.stringify(payload.context)}`];
|
|
622
|
+
if (payload.delta)
|
|
623
|
+
lines.push(`Delta: ${JSON.stringify(payload.delta)}`);
|
|
624
|
+
if (payload.files.length)
|
|
625
|
+
lines.push("Files:");
|
|
626
|
+
for (const file of payload.files) {
|
|
627
|
+
lines.push(` ${file.path} (${Math.round(file.score * 100)}%) - ${file.reason}`);
|
|
628
|
+
if (file.symbols.length)
|
|
629
|
+
lines.push(` Symbols: ${file.symbols.join(", ")}`);
|
|
630
|
+
}
|
|
631
|
+
if (payload.relatedFiles?.length)
|
|
632
|
+
lines.push("Related files (indexed import graph):");
|
|
633
|
+
for (const file of payload.relatedFiles ?? [])
|
|
634
|
+
lines.push(` ${file.path} - ${file.reason}`);
|
|
635
|
+
if (payload.source?.length)
|
|
636
|
+
lines.push("Source evidence (only indicated ranges):");
|
|
637
|
+
for (const part of payload.source ?? [])
|
|
638
|
+
lines.push(`${part.file}:${part.start}-${part.end}\n${part.text}`);
|
|
639
|
+
if (payload.tests.length)
|
|
640
|
+
lines.push("Tests:");
|
|
641
|
+
for (const test of payload.tests)
|
|
642
|
+
lines.push(` ${test.path} - ${test.reason}`);
|
|
643
|
+
if (payload.recentDiffs.length)
|
|
644
|
+
lines.push("Recent Diffs:");
|
|
645
|
+
for (const diff of payload.recentDiffs)
|
|
646
|
+
if (diff)
|
|
647
|
+
lines.push(` ${diff.path} @ ${diff.commit} - ${diff.summary}`);
|
|
648
|
+
if (payload.memory.length)
|
|
649
|
+
lines.push("Memory:");
|
|
650
|
+
for (const item of payload.memory)
|
|
651
|
+
lines.push(` ${item.id} [${item.kind}] ${item.summary}`);
|
|
652
|
+
if (payload.risks.length)
|
|
653
|
+
lines.push("Risks:");
|
|
654
|
+
for (const risk of payload.risks)
|
|
655
|
+
lines.push(` - ${risk}`);
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
658
|
+
export async function buildChangeImpact(options) {
|
|
659
|
+
const resolvedSymbols = await Promise.all((options.symbols ?? []).map(async (symbol) => ({
|
|
660
|
+
symbol,
|
|
661
|
+
resolution: await resolveSymbolData({
|
|
662
|
+
rootDir: options.rootDir,
|
|
663
|
+
symbolName: symbol,
|
|
664
|
+
limit: 5,
|
|
665
|
+
}).catch(() => null),
|
|
666
|
+
})));
|
|
667
|
+
const resolvedFiles = resolvedSymbols
|
|
668
|
+
.map((item) => item.resolution?.bestMatch?.path)
|
|
669
|
+
.filter((path) => Boolean(path));
|
|
670
|
+
const symbolsByResolvedFile = new Map();
|
|
671
|
+
for (const item of resolvedSymbols) {
|
|
672
|
+
const match = item.resolution?.bestMatch;
|
|
673
|
+
if (!match)
|
|
674
|
+
continue;
|
|
675
|
+
symbolsByResolvedFile.set(match.path, unique([
|
|
676
|
+
...(symbolsByResolvedFile.get(match.path) ?? []),
|
|
677
|
+
match.name,
|
|
678
|
+
]));
|
|
679
|
+
}
|
|
680
|
+
const fallbackFiles = await resolveFilesFromSymbols(options.rootDir, options.symbols);
|
|
681
|
+
const inputFiles = unique([...(options.files ?? []), ...resolvedFiles, ...fallbackFiles]);
|
|
682
|
+
const db = await getDB(options.rootDir);
|
|
683
|
+
const indexedFiles = db.getAllFiles();
|
|
684
|
+
const indexState = await getIndexState(options.rootDir);
|
|
685
|
+
const freshness = buildFreshnessMetadata(indexState, {
|
|
686
|
+
staleCount: 0,
|
|
687
|
+
refreshed: false,
|
|
688
|
+
warning: inputFiles.length === 0 ? "No files resolved for impact analysis." : null,
|
|
689
|
+
});
|
|
690
|
+
const affected = new Map();
|
|
691
|
+
const reasons = [];
|
|
692
|
+
const targetTests = options.includeTests === false ? [] : await findTestsForFiles(options.rootDir, inputFiles);
|
|
693
|
+
const publicSurface = unique([
|
|
694
|
+
...(options.symbols ?? []),
|
|
695
|
+
...resolvedSymbols.map((item) => item.resolution?.bestMatch?.name).filter((name) => Boolean(name)),
|
|
696
|
+
]);
|
|
697
|
+
// Only narrow to symbols when every analyzed file is covered by a resolved
|
|
698
|
+
// definition. An unknown name or an additional explicitly supplied file must
|
|
699
|
+
// remain conservative and be treated as a whole-file change.
|
|
700
|
+
const symbolScoped = inputFiles.length > 0 && inputFiles.every((file) => symbolsByResolvedFile.has(file));
|
|
701
|
+
const directCallerFiles = new Set();
|
|
702
|
+
const symbolImporterFiles = new Set();
|
|
703
|
+
for (const file of inputFiles) {
|
|
704
|
+
const dbFile = db.getFile(file);
|
|
705
|
+
if (!dbFile)
|
|
706
|
+
continue;
|
|
707
|
+
const importers = db.getImportersOf(dbFile.id);
|
|
708
|
+
const fileSymbols = unique(db.getSymbolsByFile(dbFile.id).map((symbol) => symbol.name)).slice(0, 8);
|
|
709
|
+
// An explicitly named symbol is a narrower request than changing its entire
|
|
710
|
+
// module. Falling back to every top-level symbol made a tiny method edit look
|
|
711
|
+
// like a rewrite of a highly-imported core file.
|
|
712
|
+
const relevantSymbols = symbolScoped
|
|
713
|
+
? symbolsByResolvedFile.get(file) ?? []
|
|
714
|
+
: fileSymbols;
|
|
715
|
+
for (const symbol of relevantSymbols) {
|
|
716
|
+
for (const call of db.getCallersOf(symbol)) {
|
|
717
|
+
const callerFile = indexedFiles.find((candidate) => candidate.id === call.file_id);
|
|
718
|
+
if (!callerFile || callerFile.path === file)
|
|
719
|
+
continue;
|
|
720
|
+
if (!isTestFile(callerFile.path))
|
|
721
|
+
directCallerFiles.add(callerFile.path);
|
|
722
|
+
upsertRanked(affected, {
|
|
723
|
+
path: callerFile.path,
|
|
724
|
+
kind: isTestFile(callerFile.path) ? "test_call_site" : "call_site",
|
|
725
|
+
score: isTestFile(callerFile.path) ? 0.84 : 0.86,
|
|
726
|
+
reason: `Calls ${symbol} at L${call.line}`,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (importers.length > 0)
|
|
731
|
+
reasons.push(`${file} has ${importers.length} importers`);
|
|
732
|
+
for (const importer of importers) {
|
|
733
|
+
const source = indexedFiles.find((candidate) => candidate.id === importer.source_file_id);
|
|
734
|
+
if (!source)
|
|
735
|
+
continue;
|
|
736
|
+
const importedSymbols = parseJsonArray(importer.symbols_imported);
|
|
737
|
+
const symbolOverlap = importedSymbols.filter((symbol) => relevantSymbols.includes(symbol));
|
|
738
|
+
if (!isTestFile(source.path) && symbolOverlap.length > 0)
|
|
739
|
+
symbolImporterFiles.add(source.path);
|
|
740
|
+
upsertRanked(affected, {
|
|
741
|
+
path: source.path,
|
|
742
|
+
kind: isTestFile(source.path)
|
|
743
|
+
? "test_importer"
|
|
744
|
+
: symbolOverlap.length > 0
|
|
745
|
+
? "symbol_importer"
|
|
746
|
+
: "importer",
|
|
747
|
+
score: Math.min(0.96, symbolOverlap.length > 0 ? 0.9 : 0.72 + importers.length * 0.04),
|
|
748
|
+
reason: symbolOverlap.length > 0
|
|
749
|
+
? `Imports ${symbolOverlap.join(", ")} from ${file}`
|
|
750
|
+
: `Imports ${file}`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const candidateMentionFiles = unique([
|
|
754
|
+
...importers.map((importer) => indexedFiles.find((candidate) => candidate.id === importer.source_file_id)?.path ?? "").filter(Boolean),
|
|
755
|
+
...targetTests.map((test) => test.path),
|
|
756
|
+
]);
|
|
757
|
+
const exactMentions = await scanExactMentions(options.rootDir, candidateMentionFiles, relevantSymbols);
|
|
758
|
+
for (const dependent of exactMentions) {
|
|
759
|
+
if (dependent === file)
|
|
760
|
+
continue;
|
|
761
|
+
upsertRanked(affected, {
|
|
762
|
+
path: dependent,
|
|
763
|
+
kind: isTestFile(dependent) ? "symbol_test_match" : "symbol_reference",
|
|
764
|
+
score: isTestFile(dependent) ? 0.78 : 0.58,
|
|
765
|
+
reason: "Exact mention of target symbol in graph-related file",
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const riskDetails = await Promise.all(inputFiles.map((file) => fileRisk(options.rootDir, file)));
|
|
770
|
+
const totalImporters = riskDetails.reduce((sum, detail) => sum + detail.importers, 0);
|
|
771
|
+
const totalChurn = options.includeRecentChurn === false ? 0 : riskDetails.reduce((sum, detail) => sum + detail.churn, 0);
|
|
772
|
+
if (targetTests.length === 0)
|
|
773
|
+
reasons.push("No nearby regression tests found");
|
|
774
|
+
if (totalChurn > 0)
|
|
775
|
+
reasons.push(`Recent churn count: ${totalChurn}`);
|
|
776
|
+
const riskScope = symbolScoped ? "symbol" : "file";
|
|
777
|
+
const fanOut = symbolScoped
|
|
778
|
+
? new Set([...directCallerFiles, ...symbolImporterFiles]).size
|
|
779
|
+
: totalImporters;
|
|
780
|
+
const blastRadius = symbolScoped
|
|
781
|
+
? new Set([...directCallerFiles, ...symbolImporterFiles]).size
|
|
782
|
+
: affected.size;
|
|
783
|
+
// A requested symbol name that resolved ambiguously (e.g. "search" matching a throwaway
|
|
784
|
+
// local variable as readily as the real implementation) means everything computed below —
|
|
785
|
+
// inputFiles, fanOut, blastRadius — may be built on the wrong file entirely. Don't let the
|
|
786
|
+
// score present a confident-looking number in that case; a high riskScore built on a bad
|
|
787
|
+
// resolution is worse than no score.
|
|
788
|
+
const ambiguousResolutions = resolvedSymbols
|
|
789
|
+
.filter((item) => item.resolution?.ambiguous)
|
|
790
|
+
.map((item) => ({
|
|
791
|
+
symbol: item.symbol,
|
|
792
|
+
candidates: (item.resolution?.candidates ?? []).slice(0, 5).map((c) => ({
|
|
793
|
+
path: c.path,
|
|
794
|
+
line: c.line,
|
|
795
|
+
kind: c.kind,
|
|
796
|
+
score: c.score,
|
|
797
|
+
})),
|
|
798
|
+
}));
|
|
799
|
+
const rawRiskScore = Math.min(0.95, (symbolScoped ? 0.12 : 0.24)
|
|
800
|
+
+ Math.min(fanOut, 12) * (symbolScoped ? 0.055 : 0.07)
|
|
801
|
+
+ (targetTests.length === 0 ? 0.12 : 0)
|
|
802
|
+
+ Math.min(totalChurn, 10) * 0.015
|
|
803
|
+
+ Math.min(blastRadius, 12) * 0.01);
|
|
804
|
+
const riskScore = ambiguousResolutions.length > 0 ? Math.min(rawRiskScore, 0.4) : rawRiskScore;
|
|
805
|
+
const config = await loadProjectConfig(options.rootDir);
|
|
806
|
+
const repo = buildRepoMetadata({
|
|
807
|
+
rootDir: options.rootDir,
|
|
808
|
+
projectId: config?.projectId ?? null,
|
|
809
|
+
clientId: config?.clientId ?? null,
|
|
810
|
+
fingerprint: config?.repoFingerprint ?? null,
|
|
811
|
+
staleFiles: 0,
|
|
812
|
+
indexGeneration: freshness.indexGeneration,
|
|
813
|
+
});
|
|
814
|
+
const evidence = [
|
|
815
|
+
...resolvedSymbols
|
|
816
|
+
.filter((item) => item.resolution?.bestMatch)
|
|
817
|
+
.map((item) => `${item.symbol} resolved to ${item.resolution?.bestMatch?.path}`),
|
|
818
|
+
...(fanOut > 0 ? [`${fanOut} ${symbolScoped ? "symbol-level dependents" : "direct importers"}`] : []),
|
|
819
|
+
...(targetTests.length > 0 ? [`${targetTests.length} nearby tests`] : []),
|
|
820
|
+
...(blastRadius > 0 ? [`${blastRadius} structurally related files`] : []),
|
|
821
|
+
];
|
|
822
|
+
const ambiguities = [
|
|
823
|
+
...resolvedSymbols
|
|
824
|
+
.filter((item) => item.resolution?.ambiguous)
|
|
825
|
+
.map((item) => `${item.symbol} resolved ambiguously`),
|
|
826
|
+
...(inputFiles.length === 0 ? ["no files resolved for impact analysis"] : []),
|
|
827
|
+
];
|
|
828
|
+
const ambiguousWarning = ambiguousResolutions.length > 0
|
|
829
|
+
? `⚠ AMBIGUOUS: ${ambiguousResolutions.map((item) => `"${item.symbol}"`).join(", ")} did not resolve to a single definition — this report may be built on the wrong file. Disambiguate with resolve_symbol(file_context: ...) or pass "files" explicitly. `
|
|
830
|
+
: "";
|
|
831
|
+
const partial = {
|
|
832
|
+
repo,
|
|
833
|
+
freshness,
|
|
834
|
+
trust: buildTrustMetadata({
|
|
835
|
+
baseConfidence: inputFiles.length > 0 ? 0.76 : 0.24,
|
|
836
|
+
freshness,
|
|
837
|
+
evidence,
|
|
838
|
+
ambiguities,
|
|
839
|
+
artifact: null,
|
|
840
|
+
}),
|
|
841
|
+
riskScore,
|
|
842
|
+
riskScope,
|
|
843
|
+
riskDrivers: {
|
|
844
|
+
fanOut,
|
|
845
|
+
directCallers: directCallerFiles.size,
|
|
846
|
+
symbolImporters: symbolImporterFiles.size,
|
|
847
|
+
wholeFileImporters: totalImporters,
|
|
848
|
+
churn: totalChurn,
|
|
849
|
+
nearbyTests: targetTests.length,
|
|
850
|
+
},
|
|
851
|
+
ambiguousResolutions,
|
|
852
|
+
impactSummary: ambiguousWarning + (inputFiles.length > 0
|
|
853
|
+
? `Changes may affect ${affected.size} structurally related files around ${inputFiles.join(", ")}.`
|
|
854
|
+
: "No files resolved for impact analysis."),
|
|
855
|
+
affectedFiles: [...affected.values()].sort((a, b) => b.score - a.score),
|
|
856
|
+
affectedTests: targetTests,
|
|
857
|
+
publicSurface,
|
|
858
|
+
reasons: unique(reasons),
|
|
859
|
+
};
|
|
860
|
+
const artifact = await persistAgentArtifact({
|
|
861
|
+
rootDir: options.rootDir,
|
|
862
|
+
kind: "change_impact",
|
|
863
|
+
key: JSON.stringify({
|
|
864
|
+
files: options.files ?? [],
|
|
865
|
+
symbols: options.symbols ?? [],
|
|
866
|
+
includeTests: options.includeTests !== false,
|
|
867
|
+
includeRecentChurn: options.includeRecentChurn !== false,
|
|
868
|
+
}),
|
|
869
|
+
repo,
|
|
870
|
+
payload: partial,
|
|
871
|
+
metadata: {
|
|
872
|
+
files: inputFiles.join(","),
|
|
873
|
+
symbols: publicSurface.join(","),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
const payload = {
|
|
877
|
+
...partial,
|
|
878
|
+
trust: buildTrustMetadata({
|
|
879
|
+
baseConfidence: inputFiles.length > 0 ? 0.76 : 0.24,
|
|
880
|
+
freshness,
|
|
881
|
+
evidence,
|
|
882
|
+
ambiguities,
|
|
883
|
+
artifact,
|
|
884
|
+
}),
|
|
885
|
+
};
|
|
886
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
887
|
+
const lines = [
|
|
888
|
+
`Change Impact`,
|
|
889
|
+
`Risk Score: ${Math.round(payload.riskScore * 100)}%${payload.ambiguousResolutions.length > 0 ? " (capped — see ambiguity warning below)" : ""}`,
|
|
890
|
+
`Risk scope: ${payload.riskScope}${payload.riskScope === "symbol" ? ` (${payload.publicSurface.join(", ") || "resolved symbol"})` : " (whole file)"}`,
|
|
891
|
+
artifact ? `Artifact: ${artifact.id}` : "",
|
|
892
|
+
"",
|
|
893
|
+
].filter(Boolean);
|
|
894
|
+
if (payload.ambiguousResolutions.length > 0) {
|
|
895
|
+
lines.push("⚠ AMBIGUOUS SYMBOL RESOLUTION — this report may be built on the wrong file:");
|
|
896
|
+
for (const item of payload.ambiguousResolutions) {
|
|
897
|
+
lines.push(` "${item.symbol}" matched ${item.candidates.length} candidate(s):`);
|
|
898
|
+
for (const c of item.candidates)
|
|
899
|
+
lines.push(` ${c.path}:L${c.line} [${c.kind}] (score ${c.score})`);
|
|
900
|
+
}
|
|
901
|
+
lines.push(" Disambiguate with resolve_symbol(file_context: ...) or pass \"files\" explicitly.");
|
|
902
|
+
lines.push("");
|
|
903
|
+
}
|
|
904
|
+
lines.push(`Summary: ${payload.impactSummary}`, "", "Affected Files:");
|
|
905
|
+
for (const file of payload.affectedFiles.slice(0, 12)) {
|
|
906
|
+
lines.push(` ${file.path} [${file.kind}] (${Math.round(file.score * 100)}%) - ${file.reason}`);
|
|
907
|
+
}
|
|
908
|
+
if (payload.affectedFiles.length > 12) {
|
|
909
|
+
lines.push(` ... and ${payload.affectedFiles.length - 12} more affected file(s) (full list in artifact ${artifact?.id ?? "n/a"})`);
|
|
910
|
+
}
|
|
911
|
+
if (payload.affectedTests.length > 0) {
|
|
912
|
+
lines.push("");
|
|
913
|
+
lines.push("Affected Tests:");
|
|
914
|
+
for (const test of payload.affectedTests.slice(0, 8)) {
|
|
915
|
+
lines.push(` ${test.path} (${Math.round(test.score * 100)}%) - ${test.reason}`);
|
|
916
|
+
}
|
|
917
|
+
if (payload.affectedTests.length > 8) {
|
|
918
|
+
lines.push(` ... and ${payload.affectedTests.length - 8} more affected test(s)`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (payload.reasons.length > 0) {
|
|
922
|
+
lines.push("");
|
|
923
|
+
lines.push("Reasons:");
|
|
924
|
+
for (const reason of payload.reasons)
|
|
925
|
+
lines.push(` - ${reason}`);
|
|
926
|
+
}
|
|
927
|
+
return lines.join("\n");
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
export async function buildEditScope(options) {
|
|
931
|
+
const maxFiles = Math.max(1, Math.floor(options.maxFiles ?? 5));
|
|
932
|
+
const search = await unifiedSearchData({
|
|
933
|
+
rootDir: options.rootDir,
|
|
934
|
+
query: options.task,
|
|
935
|
+
mode: "hybrid",
|
|
936
|
+
topK: Math.max(maxFiles, 5),
|
|
937
|
+
persistArtifact: false,
|
|
938
|
+
});
|
|
939
|
+
const exactIdentifierPaths = buildExactIdentifierPathSet(search, options.task);
|
|
940
|
+
const identifierTop = search.identifiers?.results.slice(0, 5) ?? [];
|
|
941
|
+
const identifierAnchorPaths = identifierTop
|
|
942
|
+
.filter((item) => item.score >= 50 || item.keywordScore >= 80 || (item.semanticScore >= 60 && item.totalCalls >= 2))
|
|
943
|
+
.map((item) => item.path);
|
|
944
|
+
const baseScopeCandidates = (search.files?.results ?? []).slice(0, Math.max(maxFiles, 6)).map((file) => ({
|
|
945
|
+
path: file.path,
|
|
946
|
+
reason: `Scoped from semantic file matches (${Math.round(file.semanticScore)} semantic / ${Math.round(file.keywordScore)} keyword)`,
|
|
947
|
+
score: file.score / 100,
|
|
948
|
+
symbols: file.matchedSymbols,
|
|
949
|
+
}));
|
|
950
|
+
const { candidates: scopeCandidatesWithPrior } = await applyDirectoryPrior(options.rootDir, options.task, baseScopeCandidates);
|
|
951
|
+
const rankedScopeFiles = await maybeChatRerankPlanningFiles(options.rootDir, options.task, rerankPlanningFiles(scopeCandidatesWithPrior, options.task, exactIdentifierPaths));
|
|
952
|
+
const rankedPathList = rankedScopeFiles.map((file) => file.path);
|
|
953
|
+
const orderedPaths = Array.from(new Set([...identifierAnchorPaths, ...rankedPathList]));
|
|
954
|
+
const rankedByPath = new Map(rankedScopeFiles.map((file) => [file.path, file]));
|
|
955
|
+
const identifierScoreByPath = new Map(identifierTop.map((item) => [item.path, item.score]));
|
|
956
|
+
const limit = options.preferSmallestScope === false ? maxFiles : Math.min(maxFiles, 3);
|
|
957
|
+
let recommendedScope = orderedPaths.slice(0, limit).map((path) => {
|
|
958
|
+
const ranked = rankedByPath.get(path);
|
|
959
|
+
const score = ranked?.score ?? ((identifierScoreByPath.get(path) ?? 60) / 100);
|
|
960
|
+
return {
|
|
961
|
+
path,
|
|
962
|
+
action: "modify",
|
|
963
|
+
confidence: Math.min(0.98, 0.45 + score),
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
const tests = await findTestsForFiles(options.rootDir, recommendedScope.map((item) => item.path));
|
|
967
|
+
const existingPaths = new Set(recommendedScope.map((item) => item.path));
|
|
968
|
+
for (const test of tests.slice(0, 2)) {
|
|
969
|
+
if (existingPaths.has(test.path))
|
|
970
|
+
continue;
|
|
971
|
+
existingPaths.add(test.path);
|
|
972
|
+
recommendedScope.push({
|
|
973
|
+
path: test.path,
|
|
974
|
+
action: "modify",
|
|
975
|
+
confidence: Math.min(0.92, test.score),
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
// Final dedupe pass: keep the highest-confidence row per path. Ranking may produce the same
|
|
979
|
+
// path twice if both the file-search and identifier-search anchors include it.
|
|
980
|
+
const dedupedScope = new Map();
|
|
981
|
+
for (const row of recommendedScope) {
|
|
982
|
+
const existing = dedupedScope.get(row.path);
|
|
983
|
+
if (!existing || row.confidence > existing.confidence)
|
|
984
|
+
dedupedScope.set(row.path, row);
|
|
985
|
+
}
|
|
986
|
+
recommendedScope = [...dedupedScope.values()];
|
|
987
|
+
const optionalScope = search.identifiers?.results.slice(0, 3).map((item) => ({
|
|
988
|
+
path: item.path,
|
|
989
|
+
action: "review_only",
|
|
990
|
+
confidence: Math.min(0.85, 0.35 + item.score / 100),
|
|
991
|
+
})).filter((item) => !recommendedScope.some((recommended) => recommended.path === item.path)) ?? [];
|
|
992
|
+
const repo = buildRepoMetadata({
|
|
993
|
+
rootDir: search.repo.root,
|
|
994
|
+
projectId: search.repo.projectId,
|
|
995
|
+
clientId: search.repo.clientId,
|
|
996
|
+
fingerprint: search.repo.fingerprint,
|
|
997
|
+
staleFiles: search.freshness.staleCount,
|
|
998
|
+
indexGeneration: search.freshness.indexGeneration,
|
|
999
|
+
});
|
|
1000
|
+
const evidence = [
|
|
1001
|
+
...search.trust.evidence,
|
|
1002
|
+
`${recommendedScope.length} recommended files in edit scope`,
|
|
1003
|
+
...(tests.length > 0 ? [`${tests.length} nearby tests considered`] : []),
|
|
1004
|
+
];
|
|
1005
|
+
const ambiguities = [
|
|
1006
|
+
...search.trust.ambiguities,
|
|
1007
|
+
...(recommendedScope.length === 0 ? ["edit scope could not recommend source files"] : []),
|
|
1008
|
+
];
|
|
1009
|
+
const partial = {
|
|
1010
|
+
repo,
|
|
1011
|
+
freshness: search.freshness,
|
|
1012
|
+
trust: buildTrustMetadata({
|
|
1013
|
+
baseConfidence: recommendedScope.length > 0 ? 0.78 : 0.31,
|
|
1014
|
+
freshness: search.freshness,
|
|
1015
|
+
evidence,
|
|
1016
|
+
ambiguities,
|
|
1017
|
+
artifact: null,
|
|
1018
|
+
}),
|
|
1019
|
+
recommendedScope,
|
|
1020
|
+
optionalScope,
|
|
1021
|
+
scopeRationale: [
|
|
1022
|
+
`Recommended scope is ranked from semantic file matches for "${options.task}".`,
|
|
1023
|
+
tests.length > 0 ? "Nearby tests were included because they are the cheapest validation surface." : "No nearby tests were detected, so only source files are recommended.",
|
|
1024
|
+
],
|
|
1025
|
+
};
|
|
1026
|
+
const artifact = await persistAgentArtifact({
|
|
1027
|
+
rootDir: options.rootDir,
|
|
1028
|
+
kind: "edit_scope",
|
|
1029
|
+
key: JSON.stringify({
|
|
1030
|
+
task: options.task,
|
|
1031
|
+
maxFiles,
|
|
1032
|
+
preferSmallestScope: options.preferSmallestScope !== false,
|
|
1033
|
+
}),
|
|
1034
|
+
repo,
|
|
1035
|
+
payload: partial,
|
|
1036
|
+
metadata: {
|
|
1037
|
+
task: options.task,
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
const payload = {
|
|
1041
|
+
...partial,
|
|
1042
|
+
trust: buildTrustMetadata({
|
|
1043
|
+
baseConfidence: recommendedScope.length > 0 ? 0.78 : 0.31,
|
|
1044
|
+
freshness: search.freshness,
|
|
1045
|
+
evidence,
|
|
1046
|
+
ambiguities,
|
|
1047
|
+
artifact,
|
|
1048
|
+
}),
|
|
1049
|
+
};
|
|
1050
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1051
|
+
const lines = [
|
|
1052
|
+
`Edit Scope: ${options.task}`,
|
|
1053
|
+
artifact ? `Artifact: ${artifact.id}` : "",
|
|
1054
|
+
"",
|
|
1055
|
+
"Recommended:",
|
|
1056
|
+
].filter(Boolean);
|
|
1057
|
+
for (const item of recommendedScope)
|
|
1058
|
+
lines.push(` ${item.path} [${item.action}] (${Math.round(item.confidence * 100)}%)`);
|
|
1059
|
+
if (optionalScope.length > 0) {
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
lines.push("Optional:");
|
|
1062
|
+
for (const item of optionalScope)
|
|
1063
|
+
lines.push(` ${item.path} [${item.action}] (${Math.round(item.confidence * 100)}%)`);
|
|
1064
|
+
}
|
|
1065
|
+
lines.push("");
|
|
1066
|
+
lines.push("Rationale:");
|
|
1067
|
+
for (const reason of payload.scopeRationale)
|
|
1068
|
+
lines.push(` - ${reason}`);
|
|
1069
|
+
return lines.join("\n");
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
export async function buildDecisionBrief(options) {
|
|
1073
|
+
const search = await unifiedSearchData({
|
|
1074
|
+
rootDir: options.rootDir,
|
|
1075
|
+
query: options.query,
|
|
1076
|
+
mode: "hybrid",
|
|
1077
|
+
topK: 7,
|
|
1078
|
+
persistArtifact: false,
|
|
1079
|
+
});
|
|
1080
|
+
const exactIdentifierPaths = buildExactIdentifierPathSet(search, options.query);
|
|
1081
|
+
const baseBriefCandidates = (search.files?.results ?? []).slice(0, 10).map((item) => ({
|
|
1082
|
+
path: item.path,
|
|
1083
|
+
reason: `Decision candidate (${Math.round(item.semanticScore)} semantic / ${Math.round(item.keywordScore)} keyword)`,
|
|
1084
|
+
score: item.score / 100,
|
|
1085
|
+
symbols: item.matchedSymbols,
|
|
1086
|
+
}));
|
|
1087
|
+
const { candidates: briefCandidatesWithPrior } = await applyDirectoryPrior(options.rootDir, options.query, baseBriefCandidates);
|
|
1088
|
+
const rankedBriefFiles = await maybeChatRerankPlanningFiles(options.rootDir, options.query, rerankPlanningFiles(briefCandidatesWithPrior, options.query, exactIdentifierPaths));
|
|
1089
|
+
const filesFromFileSearch = rankedBriefFiles.map((item) => item.path);
|
|
1090
|
+
const topIdentifiers = search.identifiers?.results.slice(0, 5) ?? [];
|
|
1091
|
+
const identifierResolvedFiles = topIdentifiers
|
|
1092
|
+
.filter((item) => item.score >= 50 || item.keywordScore >= 80 || (item.semanticScore >= 60 && item.totalCalls >= 2))
|
|
1093
|
+
.map((item) => item.path);
|
|
1094
|
+
const filesToReadFirstRaw = [...identifierResolvedFiles, ...filesFromFileSearch];
|
|
1095
|
+
const filesToReadFirst = Array.from(new Set(filesToReadFirstRaw)).slice(0, 4);
|
|
1096
|
+
const memory = options.includeMemory === false ? [] : await getProjectMemoryHits(options.rootDir, options.query, Math.min(options.maxMemory ?? 3, 10));
|
|
1097
|
+
const tests = options.includeTests === false ? [] : await findTestsForFiles(options.rootDir, filesToReadFirst);
|
|
1098
|
+
const hubs = options.includeHubs === false ? [] : await hubMatchesForFiles(options.rootDir, filesToReadFirst);
|
|
1099
|
+
const hotSymbols = topIdentifiers
|
|
1100
|
+
.filter((item) => item.totalCalls > 2)
|
|
1101
|
+
.map((item) => ({ name: item.name, totalCalls: item.totalCalls, path: item.path }));
|
|
1102
|
+
const noTestCoverage = filesToReadFirst.filter((path) => {
|
|
1103
|
+
if (isTestFile(path))
|
|
1104
|
+
return false;
|
|
1105
|
+
const sourceBasename = path.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
|
|
1106
|
+
if (!sourceBasename)
|
|
1107
|
+
return true;
|
|
1108
|
+
return !tests.some((test) => {
|
|
1109
|
+
const testBasename = test.path.split("/").pop()?.replace(/\.(test|spec|demo)\.\w+$/, "") ?? "";
|
|
1110
|
+
return testBasename === sourceBasename;
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
const openQuestions = [
|
|
1114
|
+
...(tests.length === 0
|
|
1115
|
+
? [`No nearby tests were found for ${filesToReadFirst.join(", ") || "the candidate files"}; which regression path validates this subsystem today?`]
|
|
1116
|
+
: noTestCoverage.length > 0
|
|
1117
|
+
? [`${noTestCoverage.join(", ")} appears to have no direct test coverage — should we add one?`]
|
|
1118
|
+
: []),
|
|
1119
|
+
...hotSymbols.map((item) => `Will the change to ${item.name} (${item.totalCalls} call sites in ${item.path}) preserve backward compatibility for all callers?`),
|
|
1120
|
+
...(hubs.length === 0 && filesToReadFirst.length > 0
|
|
1121
|
+
? [`No feature hub links ${filesToReadFirst[0]} — is this subsystem documented anywhere?`]
|
|
1122
|
+
: []),
|
|
1123
|
+
...(rankedBriefFiles.length >= 2 && Math.abs((rankedBriefFiles[0]?.score ?? 0) - (rankedBriefFiles[1]?.score ?? 0)) < 0.06
|
|
1124
|
+
? [`Top candidates (${rankedBriefFiles[0]?.path} vs ${rankedBriefFiles[1]?.path}) are close in score — confirm the intended target before editing.`]
|
|
1125
|
+
: []),
|
|
1126
|
+
];
|
|
1127
|
+
const repo = buildRepoMetadata({
|
|
1128
|
+
rootDir: search.repo.root,
|
|
1129
|
+
projectId: search.repo.projectId,
|
|
1130
|
+
clientId: search.repo.clientId,
|
|
1131
|
+
fingerprint: search.repo.fingerprint,
|
|
1132
|
+
staleFiles: search.freshness.staleCount,
|
|
1133
|
+
indexGeneration: search.freshness.indexGeneration,
|
|
1134
|
+
});
|
|
1135
|
+
const evidence = [
|
|
1136
|
+
...search.trust.evidence,
|
|
1137
|
+
`${filesToReadFirst.length} files to read first`,
|
|
1138
|
+
...(hubs.length > 0 ? [`${hubs.length} linked hubs`] : []),
|
|
1139
|
+
...(memory.length > 0 ? [`${memory.length} memory hits`] : []),
|
|
1140
|
+
...(hotSymbols.length > 0 ? [`${hotSymbols.length} hot symbols with >2 callers`] : []),
|
|
1141
|
+
];
|
|
1142
|
+
const ambiguities = [
|
|
1143
|
+
...search.trust.ambiguities,
|
|
1144
|
+
...(filesToReadFirst.length === 0 ? ["decision brief did not rank any files"] : []),
|
|
1145
|
+
];
|
|
1146
|
+
const partial = {
|
|
1147
|
+
repo,
|
|
1148
|
+
freshness: search.freshness,
|
|
1149
|
+
trust: buildTrustMetadata({
|
|
1150
|
+
baseConfidence: filesToReadFirst.length > 0 ? 0.8 : 0.29,
|
|
1151
|
+
freshness: search.freshness,
|
|
1152
|
+
evidence,
|
|
1153
|
+
ambiguities,
|
|
1154
|
+
artifact: null,
|
|
1155
|
+
}),
|
|
1156
|
+
summary: inferProjectSummary(options.query, filesToReadFirst),
|
|
1157
|
+
topSymbols: topIdentifiers.map((item) => item.name),
|
|
1158
|
+
hotSymbols,
|
|
1159
|
+
filesToReadFirst,
|
|
1160
|
+
doNotReadYet: filesFromFileSearch.filter((path) => !filesToReadFirst.includes(path)).slice(0, 4),
|
|
1161
|
+
hubs,
|
|
1162
|
+
memory,
|
|
1163
|
+
tests: tests.slice(0, 5),
|
|
1164
|
+
openQuestions,
|
|
1165
|
+
};
|
|
1166
|
+
const artifact = await persistAgentArtifact({
|
|
1167
|
+
rootDir: options.rootDir,
|
|
1168
|
+
kind: "decision_brief",
|
|
1169
|
+
key: JSON.stringify({
|
|
1170
|
+
query: options.query,
|
|
1171
|
+
includeHubs: options.includeHubs !== false,
|
|
1172
|
+
includeMemory: options.includeMemory !== false,
|
|
1173
|
+
includeTests: options.includeTests !== false,
|
|
1174
|
+
}),
|
|
1175
|
+
repo,
|
|
1176
|
+
payload: partial,
|
|
1177
|
+
metadata: {
|
|
1178
|
+
query: options.query,
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
const payload = {
|
|
1182
|
+
...partial,
|
|
1183
|
+
trust: buildTrustMetadata({
|
|
1184
|
+
baseConfidence: filesToReadFirst.length > 0 ? 0.8 : 0.29,
|
|
1185
|
+
freshness: search.freshness,
|
|
1186
|
+
evidence,
|
|
1187
|
+
ambiguities,
|
|
1188
|
+
artifact,
|
|
1189
|
+
}),
|
|
1190
|
+
};
|
|
1191
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1192
|
+
const lines = [
|
|
1193
|
+
`Decision Brief: ${options.query}`,
|
|
1194
|
+
artifact ? `Artifact: ${artifact.id}` : "",
|
|
1195
|
+
`Summary: ${payload.summary}`,
|
|
1196
|
+
"",
|
|
1197
|
+
`Files To Read First: ${payload.filesToReadFirst.join(", ") || "(none)"}`,
|
|
1198
|
+
`Top Symbols: ${payload.topSymbols.join(", ") || "(none)"}`,
|
|
1199
|
+
].filter(Boolean);
|
|
1200
|
+
if (payload.hotSymbols.length > 0) {
|
|
1201
|
+
lines.push("");
|
|
1202
|
+
lines.push("Hot Symbols (high call count):");
|
|
1203
|
+
for (const item of payload.hotSymbols)
|
|
1204
|
+
lines.push(` ${item.name} — ${item.totalCalls} call sites in ${item.path}`);
|
|
1205
|
+
}
|
|
1206
|
+
if (payload.hubs.length > 0)
|
|
1207
|
+
lines.push(`Hubs: ${payload.hubs.join(", ")}`);
|
|
1208
|
+
if (payload.tests.length > 0) {
|
|
1209
|
+
lines.push("");
|
|
1210
|
+
lines.push("Tests:");
|
|
1211
|
+
for (const test of payload.tests)
|
|
1212
|
+
lines.push(` ${test.path} (${Math.round(test.score * 100)}%) - ${test.reason}`);
|
|
1213
|
+
}
|
|
1214
|
+
if (payload.memory.length > 0) {
|
|
1215
|
+
lines.push("");
|
|
1216
|
+
lines.push("Memory:");
|
|
1217
|
+
for (const item of payload.memory)
|
|
1218
|
+
lines.push(` ${item.id} [${item.kind}] ${item.summary}`);
|
|
1219
|
+
}
|
|
1220
|
+
if (payload.openQuestions.length > 0) {
|
|
1221
|
+
lines.push("");
|
|
1222
|
+
lines.push("Open Questions:");
|
|
1223
|
+
for (const question of payload.openQuestions)
|
|
1224
|
+
lines.push(` - ${question}`);
|
|
1225
|
+
}
|
|
1226
|
+
return lines.join("\n");
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
export async function buildValidateScope(options) {
|
|
1230
|
+
const maxFiles = Math.max(1, Math.floor(options.maxFiles ?? 5));
|
|
1231
|
+
const explicitFiles = unique(options.files ?? []);
|
|
1232
|
+
const editScopeRaw = JSON.parse(await buildEditScope({
|
|
1233
|
+
rootDir: options.rootDir,
|
|
1234
|
+
task: options.task,
|
|
1235
|
+
maxFiles,
|
|
1236
|
+
preferSmallestScope: true,
|
|
1237
|
+
format: "json",
|
|
1238
|
+
}));
|
|
1239
|
+
const scripts = await readPackageScripts(options.rootDir);
|
|
1240
|
+
const pyProfile = await detectPythonProfile(options.rootDir);
|
|
1241
|
+
const inferredSources = editScopeRaw.recommendedScope.filter((item) => isSourcePath(item.path) && !isTestPath(item.path)).map((item) => item.path);
|
|
1242
|
+
const explicitSources = explicitFiles.filter((path) => isSourcePath(path) && !isTestPath(path));
|
|
1243
|
+
const sourceFiles = unique(explicitFiles.length ? explicitSources : inferredSources).slice(0, Math.max(maxFiles, explicitSources.length));
|
|
1244
|
+
const inferredTests = await findTestsForFiles(options.rootDir, sourceFiles);
|
|
1245
|
+
const explicitTests = explicitFiles
|
|
1246
|
+
.filter((path) => isTestPath(path))
|
|
1247
|
+
.map((path) => ({ path, reason: "Explicitly changed test file", score: 1 }));
|
|
1248
|
+
const testsByPath = new Map();
|
|
1249
|
+
for (const test of [...explicitTests, ...inferredTests]) {
|
|
1250
|
+
if (!testsByPath.has(test.path))
|
|
1251
|
+
testsByPath.set(test.path, test);
|
|
1252
|
+
}
|
|
1253
|
+
const tests = [...testsByPath.values()].slice(0, Math.max(8, explicitTests.length));
|
|
1254
|
+
const testPaths = tests.map((test) => test.path);
|
|
1255
|
+
const checks = [];
|
|
1256
|
+
const hasPythonSources = sourceFiles.some((path) => path.endsWith(".py")) || testPaths.some((path) => path.endsWith(".py"));
|
|
1257
|
+
if (testPaths.length > 0 && scripts.test) {
|
|
1258
|
+
checks.push({
|
|
1259
|
+
type: "test",
|
|
1260
|
+
command: `npm run test -- ${testPaths.join(" ")}`,
|
|
1261
|
+
reason: "Closest regression tests for the scoped source files.",
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
else if (testPaths.length > 0 && testPaths.every((path) => /\.(test|spec)\.(mjs|js)$/.test(path))) {
|
|
1265
|
+
checks.push({
|
|
1266
|
+
type: "test",
|
|
1267
|
+
command: `node --test ${testPaths.join(" ")}`,
|
|
1268
|
+
reason: "Direct Node test invocation is available for the scoped test files.",
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
else if (hasPythonSources && (pyProfile.hasPytestConfig || pyProfile.hasPyprojectToml)) {
|
|
1272
|
+
const pytestCmd = pythonRunCommand(pyProfile, "pytest", options.rootDir);
|
|
1273
|
+
const targets = testPaths.length > 0 ? ` ${testPaths.join(" ")}` : "";
|
|
1274
|
+
checks.push({
|
|
1275
|
+
type: "test",
|
|
1276
|
+
command: `${pytestCmd} -q${targets}`,
|
|
1277
|
+
reason: pyProfile.isUvManaged
|
|
1278
|
+
? "Detected uv-managed Python project; pytest invoked via uv run."
|
|
1279
|
+
: pyProfile.hasVenv
|
|
1280
|
+
? "Detected .venv; running pytest from the virtualenv binary."
|
|
1281
|
+
: pyProfile.isPoetryManaged
|
|
1282
|
+
? "Detected poetry-managed Python project; pytest invoked via poetry run."
|
|
1283
|
+
: "Detected pyproject.toml with pytest configuration.",
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
if (scripts.lint) {
|
|
1287
|
+
checks.push({
|
|
1288
|
+
type: "lint",
|
|
1289
|
+
command: "npm run lint",
|
|
1290
|
+
reason: "Project lint pass to catch type and style regressions around the edit scope.",
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
else if (hasPythonSources && pyProfile.hasRuffConfig) {
|
|
1294
|
+
checks.push({
|
|
1295
|
+
type: "lint",
|
|
1296
|
+
command: `${pythonRunCommand(pyProfile, "ruff", options.rootDir)} check .`,
|
|
1297
|
+
reason: "Detected ruff configuration in pyproject.toml / ruff.toml.",
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
if (scripts.build) {
|
|
1301
|
+
checks.push({
|
|
1302
|
+
type: "build",
|
|
1303
|
+
command: "npm run build",
|
|
1304
|
+
reason: "Project build verifies the edited modules still compile cleanly.",
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
if (hasPythonSources && pyProfile.hasMypyConfig) {
|
|
1308
|
+
checks.push({
|
|
1309
|
+
type: "typecheck",
|
|
1310
|
+
command: `${pythonRunCommand(pyProfile, "mypy", options.rootDir)} .`,
|
|
1311
|
+
reason: "Detected mypy configuration in pyproject.toml / mypy.ini.",
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
if (checks.length === 0) {
|
|
1315
|
+
checks.push({
|
|
1316
|
+
type: "manual",
|
|
1317
|
+
command: "Inspect nearest tests or run the project-standard validation command manually.",
|
|
1318
|
+
reason: "No safe script-level validation command could be inferred from the repository config.",
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const snapshot = await createValidationSnapshot(options.rootDir);
|
|
1322
|
+
const payload = {
|
|
1323
|
+
snapshot,
|
|
1324
|
+
execution: "recommendations only; run checks in the host and submit validation_record",
|
|
1325
|
+
repo: editScopeRaw.repo,
|
|
1326
|
+
freshness: editScopeRaw.freshness,
|
|
1327
|
+
trust: editScopeRaw.trust,
|
|
1328
|
+
task: options.task,
|
|
1329
|
+
explicitFiles,
|
|
1330
|
+
sourceFiles,
|
|
1331
|
+
tests: tests.slice(0, 8),
|
|
1332
|
+
optionalFiles: editScopeRaw.optionalScope,
|
|
1333
|
+
checks,
|
|
1334
|
+
};
|
|
1335
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1336
|
+
const lines = [
|
|
1337
|
+
`Validate Scope: ${options.task}`,
|
|
1338
|
+
`Validation snapshot: ${snapshot.id} (checks have not been executed)`,
|
|
1339
|
+
"",
|
|
1340
|
+
`Source files: ${sourceFiles.join(", ") || "(none)"}`,
|
|
1341
|
+
`Tests: ${testPaths.join(", ") || "(none)"}`,
|
|
1342
|
+
"",
|
|
1343
|
+
"Checks:",
|
|
1344
|
+
];
|
|
1345
|
+
for (const check of checks) {
|
|
1346
|
+
lines.push(` ${check.command}`);
|
|
1347
|
+
lines.push(` ${check.reason}`);
|
|
1348
|
+
}
|
|
1349
|
+
return lines.join("\n");
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
export async function recordTaskMemory(options) {
|
|
1353
|
+
const db = await getDB(options.rootDir);
|
|
1354
|
+
const config = await loadProjectConfig(options.rootDir);
|
|
1355
|
+
const requestedEvidence = [], unavailableEvidence = [];
|
|
1356
|
+
if (options.sourceEvidence)
|
|
1357
|
+
requestedEvidence.push(...options.sourceEvidence);
|
|
1358
|
+
else
|
|
1359
|
+
for (const file of options.files ?? []) {
|
|
1360
|
+
const path = resolveProjectPath(options.rootDir, file);
|
|
1361
|
+
try {
|
|
1362
|
+
if ((await stat(path)).isFile())
|
|
1363
|
+
requestedEvidence.push({ file });
|
|
1364
|
+
else
|
|
1365
|
+
unavailableEvidence.push(file);
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
if (error.code !== "ENOENT")
|
|
1369
|
+
throw error;
|
|
1370
|
+
unavailableEvidence.push(file);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const configuration = requestedEvidence.length ? (await evidenceConfigurationFiles(options.rootDir)).map(file => ({ file })) : [];
|
|
1374
|
+
const sourceEvidence = await bindEvidence(options.rootDir, [...requestedEvidence, ...configuration.filter(item => !requestedEvidence.some(ref => ref.file === item.file))]);
|
|
1375
|
+
const supersedes = [...new Set(options.supersedes ?? [])];
|
|
1376
|
+
for (const id of supersedes)
|
|
1377
|
+
if (!db.getMemoryNode(id))
|
|
1378
|
+
throw new Error(`Unknown superseded memory: ${id}`);
|
|
1379
|
+
const validation = await Promise.all((options.validationRefs ?? []).map(async (id) => ({ id, state: await validationState(options.rootDir, id) })));
|
|
1380
|
+
const contentParts = [
|
|
1381
|
+
`Task: ${options.task}`,
|
|
1382
|
+
`Summary: ${options.summary}`,
|
|
1383
|
+
options.files && options.files.length > 0 ? `Files: ${options.files.join(", ")}` : "",
|
|
1384
|
+
options.assumptions && options.assumptions.length > 0 ? `Assumptions: ${options.assumptions.join(" | ")}` : "",
|
|
1385
|
+
options.evidence && options.evidence.length > 0 ? `Evidence: ${options.evidence.join(" | ")}` : "",
|
|
1386
|
+
options.openRisks && options.openRisks.length > 0 ? `Open Risks: ${options.openRisks.join(" | ")}` : "",
|
|
1387
|
+
].filter(Boolean);
|
|
1388
|
+
const metadata = {
|
|
1389
|
+
scope: "project",
|
|
1390
|
+
project_id: config?.projectId ?? "",
|
|
1391
|
+
client_id: config?.clientId ?? "",
|
|
1392
|
+
record_kind: "task",
|
|
1393
|
+
task: options.task,
|
|
1394
|
+
source_evidence: JSON.stringify(sourceEvidence),
|
|
1395
|
+
configuration_paths: JSON.stringify(configuration.map(item => item.file)),
|
|
1396
|
+
validation_refs: JSON.stringify(options.validationRefs ?? []),
|
|
1397
|
+
unavailable_evidence: JSON.stringify(unavailableEvidence),
|
|
1398
|
+
open_risks: JSON.stringify(options.openRisks ?? []),
|
|
1399
|
+
};
|
|
1400
|
+
let embedding = [];
|
|
1401
|
+
try {
|
|
1402
|
+
embedding = await embedMemoryText(db, contentParts.join("\n"));
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
// Embedding provider unavailable — store node without vector; keyword search still works.
|
|
1406
|
+
}
|
|
1407
|
+
const node = {
|
|
1408
|
+
type: "note",
|
|
1409
|
+
label: `task:${options.task}:${randomUUID()}`,
|
|
1410
|
+
content: contentParts.join("\n"),
|
|
1411
|
+
metadata,
|
|
1412
|
+
};
|
|
1413
|
+
// Pre-check whether the task node already exists so we can report "Created N nodes" accurately.
|
|
1414
|
+
const taskWasNew = !db.findMemoryNodeByLabel("note", node.label);
|
|
1415
|
+
const taskMemoryId = db.transaction(() => {
|
|
1416
|
+
for (const id of supersedes)
|
|
1417
|
+
if (!db.getMemoryNode(id))
|
|
1418
|
+
throw new Error(`Superseded memory disappeared: ${id}`);
|
|
1419
|
+
const id = db.upsertMemoryNode(node, embedding.length > 0 ? embedding : undefined, currentEmbeddingModel());
|
|
1420
|
+
for (const previous of supersedes)
|
|
1421
|
+
db.upsertMemoryEdge({ source: previous, target: id, relation: "resolved_by", metadata: { relation_kind: "supersedes" } });
|
|
1422
|
+
return id;
|
|
1423
|
+
});
|
|
1424
|
+
const linkedNodes = [];
|
|
1425
|
+
const newlyCreatedNodes = [];
|
|
1426
|
+
for (const file of options.files ?? []) {
|
|
1427
|
+
const existingFileNode = db.findMemoryNodeByLabel("file", file);
|
|
1428
|
+
const fileNodeId = db.upsertMemoryNode({
|
|
1429
|
+
type: "file",
|
|
1430
|
+
label: file,
|
|
1431
|
+
content: file,
|
|
1432
|
+
metadata: {
|
|
1433
|
+
scope: "project",
|
|
1434
|
+
project_id: config?.projectId ?? "",
|
|
1435
|
+
client_id: config?.clientId ?? "",
|
|
1436
|
+
record_kind: "file_ref",
|
|
1437
|
+
},
|
|
1438
|
+
});
|
|
1439
|
+
if (!existingFileNode)
|
|
1440
|
+
newlyCreatedNodes.push(fileNodeId);
|
|
1441
|
+
db.upsertMemoryEdge({
|
|
1442
|
+
source: taskMemoryId,
|
|
1443
|
+
target: fileNodeId,
|
|
1444
|
+
relation: "references",
|
|
1445
|
+
weight: weightForFileRole(file),
|
|
1446
|
+
});
|
|
1447
|
+
linkedNodes.push(fileNodeId);
|
|
1448
|
+
}
|
|
1449
|
+
const handoff = await persistArtifact({ rootDir: options.rootDir, kind: "handoff", key: taskMemoryId, immutable: true,
|
|
1450
|
+
payload: { task: options.task, completed: options.summary, remaining: options.openRisks ?? [], assumptions: options.assumptions ?? [], sourceEvidence, unavailableEvidence, validation, taskMemoryId } });
|
|
1451
|
+
const payload = {
|
|
1452
|
+
taskMemoryId,
|
|
1453
|
+
handoff,
|
|
1454
|
+
evidenceState: await memoryEvidenceStatus(options.rootDir, db.getMemoryNode(taskMemoryId)),
|
|
1455
|
+
linkedNodes,
|
|
1456
|
+
newlyCreatedNodes,
|
|
1457
|
+
createdCount: newlyCreatedNodes.length + (taskWasNew ? 1 : 0),
|
|
1458
|
+
storedSummary: options.summary,
|
|
1459
|
+
};
|
|
1460
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1461
|
+
const lines = [
|
|
1462
|
+
`Task memory recorded: ${taskMemoryId}`,
|
|
1463
|
+
`Handoff: ${handoff.id}`,
|
|
1464
|
+
`Summary: ${options.summary}`,
|
|
1465
|
+
`Created: ${payload.createdCount} new node(s), ${linkedNodes.length} edge(s)`,
|
|
1466
|
+
];
|
|
1467
|
+
if (linkedNodes.length > 0)
|
|
1468
|
+
lines.push(`Linked Nodes: ${linkedNodes.join(", ")}`);
|
|
1469
|
+
return lines.join("\n");
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
export async function getWorkspaceContext(options) {
|
|
1473
|
+
const projects = options.includeProjects === false ? [] : await listRegisteredWorkspaces(options.clientId);
|
|
1474
|
+
const recentSessions = options.includeRecentSessions === false
|
|
1475
|
+
? []
|
|
1476
|
+
: projects.slice(0, 8).map((item) => ({
|
|
1477
|
+
sessionSource: item.sessionSource,
|
|
1478
|
+
projectId: item.projectId,
|
|
1479
|
+
summary: `Last active at ${item.lastActiveAt}`,
|
|
1480
|
+
}));
|
|
1481
|
+
const patterns = await listPromotedPatterns(options.clientId);
|
|
1482
|
+
const payload = {
|
|
1483
|
+
clientId: options.clientId ?? null,
|
|
1484
|
+
projects,
|
|
1485
|
+
recentSessions,
|
|
1486
|
+
patterns: patterns.slice(0, 8),
|
|
1487
|
+
};
|
|
1488
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1489
|
+
const lines = [
|
|
1490
|
+
`Workspace Context${options.clientId ? ` for ${options.clientId}` : ""}`,
|
|
1491
|
+
];
|
|
1492
|
+
if (projects.length > 0) {
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
lines.push("Projects:");
|
|
1495
|
+
for (const project of projects.slice(0, 12)) {
|
|
1496
|
+
lines.push(` ${project.projectId} | ${project.workspaceRoot} | ${project.sessionSource} | ${project.lastActiveAt}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (recentSessions.length > 0) {
|
|
1500
|
+
lines.push("");
|
|
1501
|
+
lines.push("Recent Sessions:");
|
|
1502
|
+
for (const session of recentSessions) {
|
|
1503
|
+
lines.push(` ${session.sessionSource} | ${session.projectId} | ${session.summary}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (patterns.length > 0) {
|
|
1507
|
+
lines.push("");
|
|
1508
|
+
lines.push("Promoted Patterns:");
|
|
1509
|
+
for (const pattern of patterns.slice(0, 8)) {
|
|
1510
|
+
lines.push(` ${pattern.patternId} [${pattern.targetScope}] ${pattern.summary}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return lines.join("\n");
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
export async function promotePattern(options) {
|
|
1517
|
+
const db = await getDB(options.rootDir);
|
|
1518
|
+
const config = await loadProjectConfig(options.rootDir);
|
|
1519
|
+
const source = db.getMemoryNode(options.sourceMemoryId);
|
|
1520
|
+
if (!source) {
|
|
1521
|
+
throw new Error(`Memory node not found: ${options.sourceMemoryId}`);
|
|
1522
|
+
}
|
|
1523
|
+
const pattern = await createPromotedPattern({
|
|
1524
|
+
sourceMemoryId: options.sourceMemoryId,
|
|
1525
|
+
targetScope: options.targetScope,
|
|
1526
|
+
clientId: config?.clientId ?? null,
|
|
1527
|
+
projectId: config?.projectId ?? "unknown",
|
|
1528
|
+
summary: options.summary,
|
|
1529
|
+
sanitize: options.sanitize ?? true,
|
|
1530
|
+
});
|
|
1531
|
+
const payload = {
|
|
1532
|
+
patternId: pattern.patternId,
|
|
1533
|
+
targetScope: pattern.targetScope,
|
|
1534
|
+
sanitized: pattern.sanitized,
|
|
1535
|
+
linkedSource: pattern.linkedSource,
|
|
1536
|
+
};
|
|
1537
|
+
return formatMaybeJson(options.format, payload, () => {
|
|
1538
|
+
return [
|
|
1539
|
+
`Pattern promoted: ${pattern.patternId}`,
|
|
1540
|
+
`Target Scope: ${pattern.targetScope}`,
|
|
1541
|
+
`Sanitized: ${pattern.sanitized ? "yes" : "no"}`,
|
|
1542
|
+
`Source: ${pattern.linkedSource}`,
|
|
1543
|
+
].join("\n");
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
//# sourceMappingURL=agentic-context.js.map
|