@moretea-labs/forge 1.6.0 → 1.7.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/AGENTS.md +2 -0
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +2 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +1 -1
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +170 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +124 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +29 -10
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capture-plan.sh +3 -3
- package/scripts/check-agent-tooling.sh +8 -35
- package/scripts/check-architecture-sync.sh +17 -86
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +107 -64
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-workflow.sh +4 -4
- package/scripts/contract-worktree.sh +1 -21
- package/scripts/ensure-task-workflow.sh +22 -55
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +15 -44
- package/scripts/migrate-project-template.sh +23 -1179
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +11 -36
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/ship-worktrees.sh +2 -29
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/verify-contract.sh +1 -111
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +3 -180
- package/scripts/workflow-contract.ts +1 -5
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +1 -1
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +106 -3
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +785 -469
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/github.ts +133 -0
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +0 -217
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +0 -513
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -189
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/repository-tools.ts +247 -107
- package/src/cli/mcp/server.ts +537 -111
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +42 -5
- package/src/cli/mcp/transports/http.ts +101 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +101 -19
- package/src/cli/repositories/command-executor.ts +55 -521
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +121 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +61 -3
- package/src/cli/repository/inspector.ts +553 -63
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation.ts +275 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
- package/src/runtime/control-plane/facade/index.ts +1 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +22 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/routing/route-policy.ts +7 -16
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +53 -0
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +43 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -0
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/workers/executor.ts +0 -48
- package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +184 -82
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
- package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
- package/src/runtime/plugins/browser-adapter.ts +2901 -241
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff.ts +92 -2
- package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
- package/src/runtime/plugins/browser-runtime.ts +183 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
- package/src/runtime/plugins/external-adapter.ts +324 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +55 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +269 -29
- package/src/runtime/plugins/types.ts +35 -2
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +393 -28
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +186 -8
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +430 -22
- package/src/runtime/root/release-manifest.ts +7 -2
- package/src/runtime/root/release-materialize.ts +19 -0
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +2 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +700 -92
- package/src/runtime/standalone-recovery/entry.ts +99 -23
- package/src/runtime/standalone-recovery/installer.ts +19 -3
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +287 -151
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +48 -4
- package/src/runtime/workflow/schedules/store.ts +131 -10
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -1,429 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
gitSnapshot,
|
|
3
|
+
getRepositoryReadSessionCache,
|
|
4
|
+
searchRepositoryMany,
|
|
5
|
+
searchRepositoryManyAsync,
|
|
6
|
+
searchRepositoryManyCacheIdentity,
|
|
7
|
+
} from "../repository/inspector";
|
|
8
|
+
import { globMatches } from "../mcp/paths";
|
|
9
|
+
import { posix } from "path";
|
|
4
10
|
import type { McpPolicy } from "../mcp/types";
|
|
5
|
-
import {
|
|
11
|
+
import { materializeSource } from "./context/source-materializer";
|
|
6
12
|
import { projectBoard } from "./issue-store";
|
|
7
|
-
import { buildControllerTaskLedgerProjection
|
|
13
|
+
import { buildControllerTaskLedgerProjection } from "./task-ledger";
|
|
8
14
|
import { legacyIssueAuthorityRetired } from "./legacy-issue-cutover";
|
|
9
15
|
import {
|
|
10
16
|
queryCodeGraphReadProvider,
|
|
11
|
-
|
|
17
|
+
queryCodeGraphReadProviderAsync,
|
|
12
18
|
type CodeGraphNodeSummary,
|
|
13
19
|
type CodeGraphReadProviderResponse,
|
|
14
20
|
} from "../../runtime/context/codegraph-read-provider";
|
|
21
|
+
import {
|
|
22
|
+
CONTEXT_PACK_SCHEMA_VERSION,
|
|
23
|
+
CONTROLLER_CONTEXT_IMPACT_DOMAINS,
|
|
24
|
+
type ControllerContextImpactDomain,
|
|
25
|
+
type ControllerContextPackDependencies,
|
|
26
|
+
type ControllerContextPackFile,
|
|
27
|
+
type ControllerContextPackOptions,
|
|
28
|
+
type ControllerContextPackProjection,
|
|
29
|
+
type ControllerContextPackSnippet,
|
|
30
|
+
} from './context/types';
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_MAX_CHARS_PER_SNIPPET,
|
|
33
|
+
DEFAULT_MAX_FILES,
|
|
34
|
+
DEFAULT_MAX_SNIPPETS,
|
|
35
|
+
DEFAULT_SEARCH_EXCLUDE_GLOBS,
|
|
36
|
+
IMPACT_DOMAIN_TERMS,
|
|
37
|
+
MAX_TOTAL_SEARCHED_FILES,
|
|
38
|
+
clamp,
|
|
39
|
+
cleanList,
|
|
40
|
+
gitStatusChangedPaths,
|
|
41
|
+
isCodeShapedTerm,
|
|
42
|
+
pathNoisePenalty,
|
|
43
|
+
structuralIntentQuery,
|
|
44
|
+
textTokens,
|
|
45
|
+
} from './context/query-planning';
|
|
46
|
+
import { issueTaskFocus, ledgerTask } from './context/focus';
|
|
47
|
+
import { addReason, coveredByGlob, expandKnownPath, looksLikeGlob, readableFile } from './context/known-paths';
|
|
48
|
+
import { structuralResult } from './context/structural';
|
|
49
|
+
import { buildRepositoryInstructionContext } from './context/instruction-context';
|
|
50
|
+
import { resolveRepositoryGovernance } from './context/rule-resolution';
|
|
51
|
+
import { controllerCheckSelection, listControllerChecks } from './check-runner';
|
|
15
52
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"coverage/**",
|
|
30
|
-
"**/*.bak",
|
|
31
|
-
] as const;
|
|
32
|
-
const MAX_TOTAL_SEARCHED_FILES = 800;
|
|
33
|
-
|
|
34
|
-
const STOPWORDS = new Set([
|
|
35
|
-
"about", "after", "again", "also", "and", "around", "because", "before", "between", "change", "code", "config",
|
|
36
|
-
"context", "current", "does", "file", "from", "have", "into", "issue", "make", "need", "needs", "only", "path",
|
|
37
|
-
"repo", "repository", "runtime", "should", "task", "that", "this", "through", "todo", "update", "when", "with",
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
export type StructuralContextMode = "off" | "auto" | "required";
|
|
41
|
-
export type ControllerContextRetrievalMode = "implementation" | "plan" | "debug" | "review";
|
|
42
|
-
export const CONTROLLER_CONTEXT_IMPACT_DOMAINS = [
|
|
43
|
-
"persistence", "scheduler", "notification", "timeline", "events", "cache", "api", "concurrency",
|
|
44
|
-
] as const;
|
|
45
|
-
export type ControllerContextImpactDomain = (typeof CONTROLLER_CONTEXT_IMPACT_DOMAINS)[number];
|
|
46
|
-
|
|
47
|
-
const IMPACT_DOMAIN_TERMS: Record<ControllerContextImpactDomain, readonly string[]> = {
|
|
48
|
-
persistence: ["persist", "database", "repository"],
|
|
49
|
-
scheduler: ["scheduler", "schedule", "reminder"],
|
|
50
|
-
notification: ["notification", "notify", "push"],
|
|
51
|
-
timeline: ["timeline", "history", "activity"],
|
|
52
|
-
events: ["event", "publish", "subscribe"],
|
|
53
|
-
cache: ["cache", "invalidate", "memo"],
|
|
54
|
-
api: ["api", "dto", "controller"],
|
|
55
|
-
concurrency: ["transaction", "lock", "atomic"],
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export interface ControllerContextPackOptions {
|
|
59
|
-
description?: string;
|
|
60
|
-
issueId?: string;
|
|
61
|
-
taskId?: string;
|
|
62
|
-
knownPaths?: string[];
|
|
63
|
-
includeGlobs?: string[];
|
|
64
|
-
excludeGlobs?: string[];
|
|
65
|
-
searchTerms?: string[];
|
|
66
|
-
maxFiles?: number;
|
|
67
|
-
maxSnippets?: number;
|
|
68
|
-
maxCharsPerSnippet?: number;
|
|
69
|
-
structuralContext?: StructuralContextMode;
|
|
70
|
-
/** Optional repository-level CodeGraph root used as structural baseline when the selected checkout has no local index. */
|
|
71
|
-
structuralIndexRoot?: string;
|
|
72
|
-
retrievalMode?: ControllerContextRetrievalMode;
|
|
73
|
-
/** GPT-selected cross-cutting evidence dimensions. Forge expands mechanically and never treats them as semantic completeness proof. */
|
|
74
|
-
impactDomains?: ControllerContextImpactDomain[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface ControllerContextPackDependencies {
|
|
78
|
-
queryCodeGraph?: typeof queryCodeGraphReadProvider;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ControllerContextPackSnippet {
|
|
82
|
-
path: string;
|
|
83
|
-
startLine: number;
|
|
84
|
-
endLine: number;
|
|
85
|
-
totalLines: number;
|
|
86
|
-
sha256: string;
|
|
87
|
-
content: string;
|
|
88
|
-
truncated: boolean;
|
|
89
|
-
redactions: Array<{ type: string; count: number }>;
|
|
90
|
-
reason: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ControllerContextPackFile {
|
|
94
|
-
path: string;
|
|
95
|
-
reasons: string[];
|
|
96
|
-
hitLines: number[];
|
|
97
|
-
snippetCount: number;
|
|
98
|
-
snippets: ControllerContextPackSnippet[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface ControllerContextPackProjection {
|
|
102
|
-
schemaVersion: typeof CONTEXT_PACK_SCHEMA_VERSION;
|
|
103
|
-
generatedAt: string;
|
|
104
|
-
source: "controller-context-pack";
|
|
105
|
-
focus: {
|
|
106
|
-
issueId?: string;
|
|
107
|
-
issueTitle?: string;
|
|
108
|
-
taskId?: string;
|
|
109
|
-
taskTitle?: string;
|
|
110
|
-
taskStatus?: string;
|
|
111
|
-
};
|
|
112
|
-
goal: string;
|
|
113
|
-
git: {
|
|
114
|
-
branch: string | null;
|
|
115
|
-
status: string;
|
|
116
|
-
diffStat: string;
|
|
117
|
-
dirty: boolean;
|
|
118
|
-
};
|
|
119
|
-
search: {
|
|
120
|
-
terms: string[];
|
|
121
|
-
impactDomains: ControllerContextImpactDomain[];
|
|
122
|
-
impactCoverage: Array<{
|
|
123
|
-
domain: ControllerContextImpactDomain;
|
|
124
|
-
queries: string[];
|
|
125
|
-
matchedFiles: string[];
|
|
126
|
-
selectedFiles: string[];
|
|
127
|
-
omittedFileCount: number;
|
|
128
|
-
status: "selected" | "matched_not_selected" | "no_evidence";
|
|
129
|
-
}>;
|
|
130
|
-
includeGlobs: string[];
|
|
131
|
-
excludeGlobs: string[];
|
|
132
|
-
scannedFiles: number;
|
|
133
|
-
policyDeniedFiles: number;
|
|
134
|
-
skippedLargeFiles: number;
|
|
135
|
-
skippedBinaryFiles: number;
|
|
136
|
-
truncated: boolean;
|
|
137
|
-
};
|
|
138
|
-
structuralContext: {
|
|
139
|
-
provider: "codegraph";
|
|
140
|
-
requestedMode: StructuralContextMode;
|
|
141
|
-
status: "disabled" | "ready" | "stale" | "unavailable" | "degraded";
|
|
142
|
-
requiredSatisfied: boolean;
|
|
143
|
-
indexSource: "current_checkout" | "repository_baseline";
|
|
144
|
-
overlayChangedFiles: string[];
|
|
145
|
-
baselineRevisionMatches: boolean;
|
|
146
|
-
query?: string;
|
|
147
|
-
entryPoints: Array<Pick<CodeGraphNodeSummary, "id" | "kind" | "name" | "filePath" | "startLine" | "endLine">>;
|
|
148
|
-
relatedFiles: string[];
|
|
149
|
-
metadata?: Pick<CodeGraphIndexMetadata, "initialized" | "lastIndexedAt" | "buildVersion" | "extractionVersion" | "staleEngine" | "changedFiles" | "ignoredChangedFileCount">;
|
|
150
|
-
fallbackReason?: string;
|
|
151
|
-
timingsMs?: CodeGraphReadProviderResponse["timingsMs"];
|
|
152
|
-
truncated: boolean;
|
|
153
|
-
};
|
|
154
|
-
impactContext: {
|
|
155
|
-
status: "not_requested" | "ready" | "degraded";
|
|
156
|
-
confidence: "low" | "medium" | "high";
|
|
157
|
-
primaryTargets: string[];
|
|
158
|
-
mustInspect: string[];
|
|
159
|
-
likelyAffected: string[];
|
|
160
|
-
relevantTests: string[];
|
|
161
|
-
relevantChecks: string[];
|
|
162
|
-
architectureContracts: string[];
|
|
163
|
-
coverageGaps: string[];
|
|
164
|
-
relationSources: Array<"codegraph" | "lexical" | "forge_checks">;
|
|
165
|
-
freshness: {
|
|
166
|
-
structuralStatus: "disabled" | "ready" | "stale" | "unavailable" | "degraded";
|
|
167
|
-
lastIndexedAt?: number | null;
|
|
168
|
-
changedFileCount: number;
|
|
169
|
-
indexSource: "current_checkout" | "repository_baseline";
|
|
170
|
-
overlayChangedFileCount: number;
|
|
171
|
-
baselineRevisionMatches: boolean;
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
files: ControllerContextPackFile[];
|
|
175
|
-
deniedPaths: Array<{ path: string; reason: string }>;
|
|
176
|
-
omitted: Array<{ path: string; reason: string }>;
|
|
177
|
-
limits: {
|
|
178
|
-
maxFiles: number;
|
|
179
|
-
maxSnippets: number;
|
|
180
|
-
maxCharsPerSnippet: number;
|
|
181
|
-
};
|
|
182
|
-
validation: {
|
|
183
|
-
policy: "task-targeted" | "minimal";
|
|
184
|
-
checks: string[];
|
|
185
|
-
};
|
|
186
|
-
contextContract: {
|
|
187
|
-
strategy: string;
|
|
188
|
-
retrievalMode: ControllerContextRetrievalMode;
|
|
189
|
-
semanticSufficiencyAuthority: "chatgpt";
|
|
190
|
-
rawCodeRequiredForImplementation: boolean;
|
|
191
|
-
expansionSignals: string[];
|
|
192
|
-
notes: string[];
|
|
193
|
-
};
|
|
194
|
-
next: string[];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function cleanList(value: string[] | undefined): string[] {
|
|
198
|
-
return Array.from(new Set((value ?? []).map((entry) => entry.trim()).filter(Boolean)));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function clamp(value: number | undefined, fallback: number, min: number, max: number): number {
|
|
202
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
203
|
-
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function textTokens(value: string): string[] {
|
|
207
|
-
const split = value
|
|
208
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
209
|
-
.split(/[^\p{L}\p{N}_./:-]+/u)
|
|
210
|
-
.map((entry) => entry.trim())
|
|
211
|
-
.filter(Boolean);
|
|
212
|
-
const tokens = split
|
|
213
|
-
.map((entry) => entry.replace(/^['"`]+|['"`]+$/g, ""))
|
|
214
|
-
.filter((entry) => entry.length >= 3)
|
|
215
|
-
.filter((entry) => !STOPWORDS.has(entry.toLowerCase()))
|
|
216
|
-
.filter((entry) => !/^\d+$/.test(entry));
|
|
217
|
-
return Array.from(new Set(tokens)).slice(0, 12);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
interface ContextPackIssueFocus {
|
|
221
|
-
id?: string;
|
|
222
|
-
title?: string;
|
|
223
|
-
summary?: string;
|
|
224
|
-
tasks: ContextPackTaskFocus[];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
interface ContextPackTaskFocus {
|
|
228
|
-
id?: string;
|
|
229
|
-
title?: string;
|
|
230
|
-
objective?: string;
|
|
231
|
-
status?: string;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function unknownRecord(value: unknown): Record<string, unknown> {
|
|
235
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function unknownString(value: unknown): string | undefined {
|
|
239
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function unknownRecordArray(value: unknown): Array<Record<string, unknown>> {
|
|
243
|
-
if (!Array.isArray(value)) return [];
|
|
244
|
-
return value.map(unknownRecord).filter((entry) => Object.keys(entry).length > 0);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function contextPackTaskFocus(value: unknown): ContextPackTaskFocus {
|
|
248
|
-
const record = unknownRecord(value);
|
|
249
|
-
return {
|
|
250
|
-
id: unknownString(record.id),
|
|
251
|
-
title: unknownString(record.title),
|
|
252
|
-
objective: unknownString(record.objective),
|
|
253
|
-
status: unknownString(record.status),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function contextPackIssueFocus(value: unknown): ContextPackIssueFocus {
|
|
258
|
-
const record = unknownRecord(value);
|
|
259
|
-
return {
|
|
260
|
-
id: unknownString(record.id),
|
|
261
|
-
title: unknownString(record.title),
|
|
262
|
-
summary: unknownString(record.summary),
|
|
263
|
-
tasks: unknownRecordArray(record.tasks).map(contextPackTaskFocus),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function issueTaskFocus(board: ReturnType<typeof projectBoard>, issueId?: string, taskId?: string): { issue?: ContextPackIssueFocus; task?: ContextPackTaskFocus } {
|
|
268
|
-
const issues = board.issues.map(contextPackIssueFocus);
|
|
269
|
-
const resolvedIssue = issueId
|
|
270
|
-
? issues.find((issue) => issue.id === issueId)
|
|
271
|
-
: board.currentIssueId
|
|
272
|
-
? issues.find((issue) => issue.id === board.currentIssueId)
|
|
273
|
-
: undefined;
|
|
274
|
-
const resolvedTask = resolvedIssue?.tasks.find((task) => task.id === taskId) ?? resolvedIssue?.tasks[0];
|
|
275
|
-
return { issue: resolvedIssue, task: resolvedTask };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function ledgerTask(ledger: ReturnType<typeof buildControllerTaskLedgerProjection>, issueId?: string, taskId?: string): TaskLedgerTaskProjection | undefined {
|
|
279
|
-
const tasks = ledger.issues.flatMap((issue) => issue.tasks);
|
|
280
|
-
const findTask = (candidateIssueId?: string, candidateTaskId?: string) => tasks
|
|
281
|
-
.find((task) => (!candidateIssueId || task.issueId === candidateIssueId) && (!candidateTaskId || task.taskId === candidateTaskId));
|
|
282
|
-
|
|
283
|
-
if (!issueId && !taskId) {
|
|
284
|
-
const readyTask = ledger.readyTasks[0];
|
|
285
|
-
return ledger.attention[0]
|
|
286
|
-
?? findTask(readyTask?.issueId, readyTask?.taskId)
|
|
287
|
-
?? tasks.find((task) => task.dispatchable || task.queueable);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return findTask(issueId, taskId);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function looksLikeGlob(path: string): boolean {
|
|
294
|
-
return /[*?{[]/.test(path);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function coveredByGlob(path: string, globs: string[]): boolean {
|
|
298
|
-
return globs.length === 0 || globs.some((glob) => globMatches(glob, path));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function gitStatusChangedPaths(status: string): string[] {
|
|
302
|
-
const paths: string[] = [];
|
|
303
|
-
for (const line of status.split(/\r?\n/)) {
|
|
304
|
-
if (!line.trim() || line.startsWith("##") || line.length < 4) continue;
|
|
305
|
-
let path = line.slice(3).trim();
|
|
306
|
-
if (path.includes(" -> ")) path = path.slice(path.lastIndexOf(" -> ") + 4).trim();
|
|
307
|
-
if (path.startsWith('"') && path.endsWith('"')) path = path.slice(1, -1);
|
|
308
|
-
if (path && !paths.includes(path)) paths.push(path);
|
|
309
|
-
}
|
|
310
|
-
return paths;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function addReason(map: Map<string, { reasons: Set<string>; lines: Set<number> }>, path: string, reason: string, line?: number): void {
|
|
314
|
-
const entry = map.get(path) ?? { reasons: new Set<string>(), lines: new Set<number>() };
|
|
315
|
-
entry.reasons.add(reason);
|
|
316
|
-
if (typeof line === "number" && Number.isFinite(line)) entry.lines.add(Math.max(1, Math.trunc(line)));
|
|
317
|
-
map.set(path, entry);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function readableFile(repoRoot: string, policy: McpPolicy, path: string): { ok: true; path: string } | { ok: false; path: string; reason: string } {
|
|
321
|
-
const decision = resolveMcpPath(repoRoot, path, policy, "read");
|
|
322
|
-
if (!decision.ok || !decision.relativePath || !decision.absolutePath) {
|
|
323
|
-
return { ok: false, path: decision.relativePath ?? path, reason: decision.reason ?? "path denied" };
|
|
324
|
-
}
|
|
325
|
-
if (!existsSync(decision.absolutePath)) return { ok: false, path: decision.relativePath, reason: "path does not exist" };
|
|
326
|
-
if (!statSync(decision.absolutePath).isFile()) return { ok: false, path: decision.relativePath, reason: "path is not a file" };
|
|
327
|
-
return { ok: true, path: decision.relativePath };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
interface ExpandedKnownPath {
|
|
331
|
-
files: string[];
|
|
332
|
-
denied: Array<{ path: string; reason: string }>;
|
|
333
|
-
directory?: string;
|
|
334
|
-
truncated: boolean;
|
|
335
|
-
}
|
|
53
|
+
export { CONTROLLER_CONTEXT_IMPACT_DOMAINS } from './context/types';
|
|
54
|
+
export type {
|
|
55
|
+
ControllerContextImpactDomain,
|
|
56
|
+
ControllerContextPackDependencies,
|
|
57
|
+
ControllerContextPackFile,
|
|
58
|
+
ControllerContextPackOptions,
|
|
59
|
+
ControllerContextPackProjection,
|
|
60
|
+
ControllerContextPackSnippet,
|
|
61
|
+
ControllerContextRetrievalMode,
|
|
62
|
+
ControllerEvidenceReadiness,
|
|
63
|
+
ControllerContextExpansionState,
|
|
64
|
+
StructuralContextMode,
|
|
65
|
+
} from './context/types';
|
|
336
66
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
* is re-checked through resolveMcpPath so directory support never broadens the
|
|
340
|
-
* policy boundary. Enumeration is deterministic and bounded.
|
|
341
|
-
*/
|
|
342
|
-
function expandKnownPath(
|
|
343
|
-
repoRoot: string,
|
|
344
|
-
policy: McpPolicy,
|
|
345
|
-
path: string,
|
|
346
|
-
maxFiles: number,
|
|
347
|
-
): ExpandedKnownPath {
|
|
348
|
-
const decision = resolveMcpPath(repoRoot, path, policy, "read");
|
|
349
|
-
if (!decision.ok || !decision.relativePath || !decision.absolutePath) {
|
|
350
|
-
return { files: [], denied: [{ path: decision.relativePath ?? path, reason: decision.reason ?? "path denied" }], truncated: false };
|
|
351
|
-
}
|
|
352
|
-
if (!existsSync(decision.absolutePath)) {
|
|
353
|
-
return { files: [], denied: [{ path: decision.relativePath, reason: "path does not exist" }], truncated: false };
|
|
354
|
-
}
|
|
355
|
-
const rootStat = lstatSync(decision.absolutePath);
|
|
356
|
-
if (rootStat.isSymbolicLink()) {
|
|
357
|
-
return { files: [], denied: [{ path: decision.relativePath, reason: "symbolic links are not followed" }], truncated: false };
|
|
358
|
-
}
|
|
359
|
-
if (rootStat.isFile()) return { files: [decision.relativePath], denied: [], truncated: false };
|
|
360
|
-
if (!rootStat.isDirectory()) {
|
|
361
|
-
return { files: [], denied: [{ path: decision.relativePath, reason: "path is neither a regular file nor directory" }], truncated: false };
|
|
362
|
-
}
|
|
67
|
+
const MAX_CONTEXT_EVIDENCE_WAVES = 3;
|
|
68
|
+
const SOURCE_REFERENCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json'] as const;
|
|
363
69
|
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let entries;
|
|
376
|
-
try {
|
|
377
|
-
entries = readdirSync(directoryDecision.absolutePath, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
378
|
-
} catch (error) {
|
|
379
|
-
denied.push({ path: directoryDecision.relativePath, reason: error instanceof Error ? error.message : String(error) });
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
for (const entry of entries) {
|
|
383
|
-
if (files.length >= maxFiles) { truncated = true; break; }
|
|
384
|
-
const child = `${directoryDecision.relativePath}/${entry.name}`.replace(/^\.\//, "");
|
|
385
|
-
if (entry.isSymbolicLink()) {
|
|
386
|
-
denied.push({ path: child, reason: "symbolic links are not followed" });
|
|
387
|
-
} else if (entry.isDirectory()) {
|
|
388
|
-
walk(child, depth + 1);
|
|
389
|
-
} else if (entry.isFile()) {
|
|
390
|
-
const readable = readableFile(repoRoot, policy, child);
|
|
391
|
-
if (readable.ok) files.push(readable.path);
|
|
392
|
-
else denied.push(readable);
|
|
393
|
-
}
|
|
70
|
+
function sourceReferenceSpecifiers(contents: string): string[] {
|
|
71
|
+
const specifiers = new Set<string>();
|
|
72
|
+
const patterns = [
|
|
73
|
+
/\b(?:import|export)\s+(?:type\s+)?(?:[^'\"]*?\s+from\s+)?['\"]([^'\"]+)['\"]/g,
|
|
74
|
+
/\brequire\s*\(\s*['\"]([^'\"]+)['\"]\s*\)/g,
|
|
75
|
+
/\bimport\s*\(\s*['\"]([^'\"]+)['\"]\s*\)/g,
|
|
76
|
+
];
|
|
77
|
+
for (const pattern of patterns) {
|
|
78
|
+
for (const match of contents.matchAll(pattern)) {
|
|
79
|
+
const value = match[1]?.trim();
|
|
80
|
+
if (value?.startsWith('.')) specifiers.add(value);
|
|
394
81
|
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return { files, denied, directory: decision.relativePath, truncated };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function boundedSnippet(content: string, maxChars: number): { content: string; truncated: boolean } {
|
|
401
|
-
if (content.length <= maxChars) return { content, truncated: false };
|
|
402
|
-
return { content: `${content.slice(0, maxChars)}\n... <snippet truncated>`, truncated: true };
|
|
82
|
+
}
|
|
83
|
+
return Array.from(specifiers).slice(0, 24);
|
|
403
84
|
}
|
|
404
85
|
|
|
405
|
-
function
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
86
|
+
function resolveSourceReference(repoRoot: string, policy: McpPolicy, sourcePath: string, specifier: string): string | undefined {
|
|
87
|
+
const base = posix.normalize(posix.join(posix.dirname(sourcePath), specifier));
|
|
88
|
+
if (!base || base === '..' || base.startsWith('../') || posix.isAbsolute(base)) return undefined;
|
|
89
|
+
const hasExtension = /\.[A-Za-z0-9]+$/.test(posix.basename(base));
|
|
90
|
+
const candidates = hasExtension
|
|
91
|
+
? [base]
|
|
92
|
+
: [base, ...SOURCE_REFERENCE_EXTENSIONS.map((extension) => `${base}${extension}`), ...SOURCE_REFERENCE_EXTENSIONS.map((extension) => `${base}/index${extension}`)];
|
|
93
|
+
for (const candidate of candidates) {
|
|
94
|
+
const readable = readableFile(repoRoot, policy, candidate);
|
|
95
|
+
if (readable.ok) return readable.path;
|
|
412
96
|
}
|
|
413
|
-
return
|
|
97
|
+
return undefined;
|
|
414
98
|
}
|
|
415
99
|
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
} {
|
|
422
|
-
const result = response.result ?? {};
|
|
423
|
-
const nodes = Array.isArray(result.nodes) ? result.nodes as CodeGraphNodeSummary[] : [];
|
|
424
|
-
const entryPoints = Array.isArray(result.entryPoints) ? result.entryPoints as CodeGraphNodeSummary[] : [];
|
|
425
|
-
const relatedFiles = Array.isArray(result.relatedFiles) ? result.relatedFiles.filter((value): value is string => typeof value === "string") : [];
|
|
426
|
-
return { nodes, entryPoints, relatedFiles, truncated: result.truncated === true };
|
|
100
|
+
function contextPathPriority(path: string, reasons: string[]): number {
|
|
101
|
+
const testBonus = /(^|\/)(__tests__|tests?|spec)(\/|$)|\.(test|spec)\.[^.]+$/i.test(path) ? 6 : 0;
|
|
102
|
+
const sourceBonus = reasons.some((reason) => reason.startsWith('source-reference:')) ? 5 : 0;
|
|
103
|
+
const structuralBonus = reasons.some((reason) => reason.startsWith('codegraph:')) ? 3 : 0;
|
|
104
|
+
return testBonus + sourceBonus + structuralBonus;
|
|
427
105
|
}
|
|
428
106
|
|
|
429
107
|
export function buildControllerContextPack(
|
|
@@ -432,9 +110,13 @@ export function buildControllerContextPack(
|
|
|
432
110
|
options: ControllerContextPackOptions = {},
|
|
433
111
|
dependencies: ControllerContextPackDependencies = {},
|
|
434
112
|
): ControllerContextPackProjection {
|
|
113
|
+
const contextStartedAt = performance.now();
|
|
114
|
+
const timingsMs = { gitAndFocus: 0, structural: 0, lexical: 0, materialization: 0, governance: 0, total: 0, waveCount: 1, expansionBudgetUsed: 0, expansionBudgetMax: 2 };
|
|
435
115
|
const retrievalMode = options.retrievalMode ?? "implementation";
|
|
436
|
-
const
|
|
437
|
-
const
|
|
116
|
+
const requestedMaxFiles = clamp(options.maxFiles, DEFAULT_MAX_FILES, 1, 30);
|
|
117
|
+
const requestedMaxSnippets = clamp(options.maxSnippets, DEFAULT_MAX_SNIPPETS, 1, 80);
|
|
118
|
+
let maxFiles = requestedMaxFiles;
|
|
119
|
+
let maxSnippets = requestedMaxSnippets;
|
|
438
120
|
const maxCharsPerSnippet = clamp(options.maxCharsPerSnippet, DEFAULT_MAX_CHARS_PER_SNIPPET, 500, 50_000);
|
|
439
121
|
const knownPaths = cleanList(options.knownPaths);
|
|
440
122
|
const knownGlobs = knownPaths.filter(looksLikeGlob);
|
|
@@ -447,13 +129,16 @@ export function buildControllerContextPack(
|
|
|
447
129
|
// Independent investigations must not inherit an unrelated repository focus.
|
|
448
130
|
// Only bind Issue/Task context when the caller explicitly requests it.
|
|
449
131
|
const hasExplicitFocus = Boolean(options.issueId || options.taskId);
|
|
450
|
-
const git = gitSnapshot(repoRoot);
|
|
132
|
+
const git = gitSnapshot(repoRoot, options.session);
|
|
451
133
|
const board = hasExplicitFocus && !legacyIssueAuthorityRetired(repoRoot) ? projectBoard(repoRoot) : undefined;
|
|
452
134
|
const focus = board ? issueTaskFocus(board, options.issueId, options.taskId) : {};
|
|
453
135
|
const ledger = board ? buildControllerTaskLedgerProjection(repoRoot, board) : undefined;
|
|
454
136
|
const compactTask = ledger ? ledgerTask(ledger, focus.issue?.id, focus.task?.id) : undefined;
|
|
455
|
-
|
|
456
|
-
|
|
137
|
+
timingsMs.gitAndFocus = Math.round((performance.now() - contextStartedAt) * 100) / 100;
|
|
138
|
+
// Work allowed paths are an authorization boundary, not a claim that early
|
|
139
|
+
// investigation already discovered the complete semantic scope. Only an
|
|
140
|
+
// explicit retrieval include narrows discovery.
|
|
141
|
+
const searchIncludeGlobs = includeGlobs;
|
|
457
142
|
const taskChecks = cleanList(compactTask?.checks);
|
|
458
143
|
const goalParts = [
|
|
459
144
|
options.description,
|
|
@@ -467,6 +152,7 @@ export function buildControllerContextPack(
|
|
|
467
152
|
const impactDomains = cleanList(options.impactDomains)
|
|
468
153
|
.filter((domain): domain is ControllerContextImpactDomain => CONTROLLER_CONTEXT_IMPACT_DOMAINS.includes(domain as ControllerContextImpactDomain))
|
|
469
154
|
.slice(0, 6);
|
|
155
|
+
const retrievalIntent = [goal, ...terms].filter(Boolean).join(" ");
|
|
470
156
|
const impactTermDomains = new Map<string, ControllerContextImpactDomain[]>();
|
|
471
157
|
for (const domain of impactDomains) {
|
|
472
158
|
for (const term of IMPACT_DOMAIN_TERMS[domain]) {
|
|
@@ -480,6 +166,11 @@ export function buildControllerContextPack(
|
|
|
480
166
|
...impactDomains.flatMap((domain) => IMPACT_DOMAIN_TERMS[domain]),
|
|
481
167
|
...terms.slice(1),
|
|
482
168
|
]).slice(0, 32);
|
|
169
|
+
// Preserve the guarantee for explicit code symbols while broad natural-
|
|
170
|
+
// language terms remain eligible for early discovery completion.
|
|
171
|
+
const requiredSearchQueries = cleanList(options.searchTerms)
|
|
172
|
+
.filter(isCodeShapedTerm)
|
|
173
|
+
.slice(0, 2);
|
|
483
174
|
const impactCandidatePaths = new Map<ControllerContextImpactDomain, Set<string>>(impactDomains.map((domain) => [domain, new Set<string>()]));
|
|
484
175
|
const candidates = new Map<string, { reasons: Set<string>; lines: Set<number> }>();
|
|
485
176
|
const deniedPaths: Array<{ path: string; reason: string }> = [];
|
|
@@ -489,10 +180,15 @@ export function buildControllerContextPack(
|
|
|
489
180
|
let skippedLargeFiles = 0;
|
|
490
181
|
let skippedBinaryFiles = 0;
|
|
491
182
|
let searchTruncated = false;
|
|
183
|
+
let lexicalCacheHit = false;
|
|
184
|
+
let structuralCacheHits = 0;
|
|
185
|
+
let structuralCacheMisses = 0;
|
|
186
|
+
let rangeCacheHits = 0;
|
|
187
|
+
let rangeCacheMisses = 0;
|
|
492
188
|
const structuralMode = options.structuralContext ?? "off";
|
|
493
189
|
const structuralIndexRoot = options.structuralIndexRoot?.trim() || repoRoot;
|
|
494
190
|
const indexSource = structuralIndexRoot === repoRoot ? "current_checkout" : "repository_baseline";
|
|
495
|
-
|
|
191
|
+
let overlayChangedFiles = indexSource === "repository_baseline" ? gitStatusChangedPaths(git.status) : [];
|
|
496
192
|
const baselineRevisionMatches = indexSource === "current_checkout" || gitSnapshot(structuralIndexRoot).head === git.head;
|
|
497
193
|
const graphImpactFiles = new Set<string>();
|
|
498
194
|
let structuralContext: ControllerContextPackProjection["structuralContext"] = {
|
|
@@ -507,9 +203,26 @@ export function buildControllerContextPack(
|
|
|
507
203
|
relatedFiles: [],
|
|
508
204
|
truncated: false,
|
|
509
205
|
};
|
|
206
|
+
const repositorySessionCache = getRepositoryReadSessionCache(repoRoot, options.session);
|
|
207
|
+
const queryCodeGraph = dependencies.queryCodeGraph ?? queryCodeGraphReadProvider;
|
|
208
|
+
const queryStructural = (queryRoot: string, request: Parameters<typeof queryCodeGraphReadProvider>[1]): CodeGraphReadProviderResponse => {
|
|
209
|
+
const key = JSON.stringify({ queryRoot, request });
|
|
210
|
+
const cached = repositorySessionCache?.getStructural(key);
|
|
211
|
+
if (cached && typeof cached === "object") {
|
|
212
|
+
structuralCacheHits += 1;
|
|
213
|
+
return cached as CodeGraphReadProviderResponse;
|
|
214
|
+
}
|
|
215
|
+
structuralCacheMisses += 1;
|
|
216
|
+
const response = queryCodeGraph(queryRoot, request);
|
|
217
|
+
if (response.error?.code !== CODEGRAPH_PREFETCH_DEFERRED_CODE) {
|
|
218
|
+
repositorySessionCache?.putStructural(key, response);
|
|
219
|
+
}
|
|
220
|
+
return response;
|
|
221
|
+
};
|
|
510
222
|
|
|
223
|
+
const structuralStartedAt = performance.now();
|
|
511
224
|
if (structuralMode !== "off") {
|
|
512
|
-
const structuralQuery =
|
|
225
|
+
const structuralQuery = structuralIntentQuery(goal, terms, impactDomains);
|
|
513
226
|
if (!structuralQuery) {
|
|
514
227
|
structuralContext = {
|
|
515
228
|
...structuralContext,
|
|
@@ -518,8 +231,7 @@ export function buildControllerContextPack(
|
|
|
518
231
|
fallbackReason: "No task description or search term was available for structural context.",
|
|
519
232
|
};
|
|
520
233
|
} else {
|
|
521
|
-
const
|
|
522
|
-
const structural = queryCodeGraph(structuralIndexRoot, {
|
|
234
|
+
const structural = queryStructural(structuralIndexRoot, {
|
|
523
235
|
operation: "context",
|
|
524
236
|
query: structuralQuery,
|
|
525
237
|
limit: Math.min(Math.max(maxFiles, 4), 12),
|
|
@@ -538,6 +250,10 @@ export function buildControllerContextPack(
|
|
|
538
250
|
ignoredChangedFileCount: structural.metadata.ignoredChangedFileCount,
|
|
539
251
|
}
|
|
540
252
|
: undefined;
|
|
253
|
+
const providerChangedFiles = metadata
|
|
254
|
+
? [...metadata.changedFiles.added, ...metadata.changedFiles.modified]
|
|
255
|
+
: [];
|
|
256
|
+
overlayChangedFiles = Array.from(new Set([...overlayChangedFiles, ...providerChangedFiles]));
|
|
541
257
|
structuralContext = {
|
|
542
258
|
provider: "codegraph",
|
|
543
259
|
requestedMode: structuralMode,
|
|
@@ -547,7 +263,9 @@ export function buildControllerContextPack(
|
|
|
547
263
|
overlayChangedFiles,
|
|
548
264
|
baselineRevisionMatches,
|
|
549
265
|
query: structuralQuery,
|
|
550
|
-
entryPoints: graph.entryPoints
|
|
266
|
+
entryPoints: graph.entryPoints
|
|
267
|
+
.filter((node) => pathNoisePenalty(node.filePath, retrievalIntent, retrievalMode) < 15)
|
|
268
|
+
.slice(0, 12).map((node) => ({
|
|
551
269
|
id: node.id,
|
|
552
270
|
kind: node.kind,
|
|
553
271
|
name: node.name,
|
|
@@ -555,7 +273,9 @@ export function buildControllerContextPack(
|
|
|
555
273
|
startLine: node.startLine,
|
|
556
274
|
endLine: node.endLine,
|
|
557
275
|
})),
|
|
558
|
-
relatedFiles: Array.from(new Set(graph.relatedFiles))
|
|
276
|
+
relatedFiles: Array.from(new Set(graph.relatedFiles))
|
|
277
|
+
.filter((path) => pathNoisePenalty(path, retrievalIntent, retrievalMode) < 50)
|
|
278
|
+
.slice(0, 80),
|
|
559
279
|
metadata,
|
|
560
280
|
...(structural.error ? { fallbackReason: `${structural.error.code}: ${structural.error.message}` } : {}),
|
|
561
281
|
timingsMs: structural.timingsMs,
|
|
@@ -585,7 +305,7 @@ export function buildControllerContextPack(
|
|
|
585
305
|
if (structural.status === "ready") {
|
|
586
306
|
const primaryFiles = Array.from(new Set(graph.entryPoints.map((node) => node.filePath).filter(Boolean))).slice(0, 4);
|
|
587
307
|
for (const filePath of primaryFiles) {
|
|
588
|
-
const adjacency =
|
|
308
|
+
const adjacency = queryStructural(structuralIndexRoot, { operation: "file_dependencies", filePath, limit: 40 });
|
|
589
309
|
if (!adjacency.ok || adjacency.status !== "ready") continue;
|
|
590
310
|
const dependencies = Array.isArray(adjacency.result?.dependencies)
|
|
591
311
|
? adjacency.result.dependencies.filter((value): value is string => typeof value === "string")
|
|
@@ -610,12 +330,13 @@ export function buildControllerContextPack(
|
|
|
610
330
|
}
|
|
611
331
|
}
|
|
612
332
|
|
|
333
|
+
timingsMs.structural = Math.round((performance.now() - structuralStartedAt) * 100) / 100;
|
|
613
334
|
const exactKnownFiles: string[] = [];
|
|
614
335
|
let exactKnownFileScope = explicitKnownPaths.length > 0;
|
|
615
336
|
for (const path of explicitKnownPaths) {
|
|
616
337
|
const expanded = expandKnownPath(repoRoot, policy, path, Math.max(maxFiles * 4, 40));
|
|
617
338
|
for (const file of expanded.files) {
|
|
618
|
-
addReason(candidates, file, expanded.directory ? `explicit-known-directory:${expanded.directory}` : "explicit-known-path"
|
|
339
|
+
addReason(candidates, file, expanded.directory ? `explicit-known-directory:${expanded.directory}` : "explicit-known-path");
|
|
619
340
|
}
|
|
620
341
|
if (!expanded.directory && expanded.files.length === 1 && expanded.denied.length === 0 && !expanded.truncated) {
|
|
621
342
|
exactKnownFiles.push(expanded.files[0]!);
|
|
@@ -626,10 +347,10 @@ export function buildControllerContextPack(
|
|
|
626
347
|
scannedFiles += expanded.files.length;
|
|
627
348
|
searchTruncated = searchTruncated || expanded.truncated;
|
|
628
349
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
350
|
+
// Exact caller-selected files own a reserved file slot. The inferred-file
|
|
351
|
+
// budget may be smaller, but another ranked candidate must never displace an
|
|
352
|
+
// exact known file. The overall response remains bounded by the hard cap.
|
|
353
|
+
maxFiles = Math.min(30, Math.max(requestedMaxFiles, exactKnownFiles.length));
|
|
633
354
|
|
|
634
355
|
// Exact known files are already a caller-selected implementation scope. When
|
|
635
356
|
// no broader impact/structural/include scope was requested, search those files
|
|
@@ -644,7 +365,9 @@ export function buildControllerContextPack(
|
|
|
644
365
|
// Lexical retrieval is one bounded pass across candidate files for all terms.
|
|
645
366
|
// The old per-term loop reread the same source files up to 14 times even
|
|
646
367
|
// though inventory itself was cached.
|
|
647
|
-
|
|
368
|
+
const lexicalStartedAt = performance.now();
|
|
369
|
+
const requiredStructuralFallbackNeedsLexical = structuralMode === "required" && !structuralContext.requiredSatisfied;
|
|
370
|
+
if (searchQueries.length > 0 && (requiredStructuralFallbackNeedsLexical || candidates.size < maxFiles * 3)) {
|
|
648
371
|
const search = searchRepositoryMany(repoRoot, policy, {
|
|
649
372
|
queries: searchQueries,
|
|
650
373
|
files: scopedExactKnownFileSearch ? exactKnownFiles : undefined,
|
|
@@ -654,7 +377,15 @@ export function buildControllerContextPack(
|
|
|
654
377
|
maxFiles: scopedExactKnownFileSearch ? exactKnownFiles.length : MAX_TOTAL_SEARCHED_FILES,
|
|
655
378
|
caseSensitive: false,
|
|
656
379
|
cacheKey: JSON.stringify({ head: git.head, status: git.status, diffStat: git.diffStat }),
|
|
380
|
+
...(!scopedExactKnownFileSearch ? {
|
|
381
|
+
completionMode: 'discovery' as const,
|
|
382
|
+
discoveryTargetFiles: Math.max(maxFiles * 2, 12),
|
|
383
|
+
discoveryMinQueryCoverage: Math.min(3, searchQueries.length),
|
|
384
|
+
requiredQueries: requiredSearchQueries,
|
|
385
|
+
} : {}),
|
|
386
|
+
session: options.session,
|
|
657
387
|
});
|
|
388
|
+
lexicalCacheHit = search.cacheHit === true;
|
|
658
389
|
scannedFiles += search.scannedFiles;
|
|
659
390
|
policyDeniedFiles += search.policyDeniedFiles;
|
|
660
391
|
skippedLargeFiles += search.skippedLargeFiles;
|
|
@@ -676,21 +407,42 @@ export function buildControllerContextPack(
|
|
|
676
407
|
}
|
|
677
408
|
}
|
|
678
409
|
|
|
410
|
+
timingsMs.lexical = Math.round((performance.now() - lexicalStartedAt) * 100) / 100;
|
|
679
411
|
const primarySearchReason = terms.length > 0 ? `search:${terms[0]}` : undefined;
|
|
680
412
|
const rankedCandidates = Array.from(candidates.entries())
|
|
681
413
|
.map(([path, entry]) => ({ path, reasons: Array.from(entry.reasons), lines: Array.from(entry.lines) }))
|
|
682
414
|
.sort((left, right) => {
|
|
683
|
-
const leftExplicit = left.reasons.
|
|
684
|
-
|
|
415
|
+
const leftExplicit = left.reasons.includes("explicit-known-path")
|
|
416
|
+
? 2
|
|
417
|
+
: left.reasons.some((reason) => reason.startsWith("explicit-known-directory:")) ? 1 : 0;
|
|
418
|
+
const rightExplicit = right.reasons.includes("explicit-known-path")
|
|
419
|
+
? 2
|
|
420
|
+
: right.reasons.some((reason) => reason.startsWith("explicit-known-directory:")) ? 1 : 0;
|
|
685
421
|
if (leftExplicit !== rightExplicit) return rightExplicit - leftExplicit;
|
|
686
422
|
const leftPrimary = primarySearchReason && left.reasons.includes(primarySearchReason) ? 1 : 0;
|
|
687
423
|
const rightPrimary = primarySearchReason && right.reasons.includes(primarySearchReason) ? 1 : 0;
|
|
688
424
|
if (leftPrimary !== rightPrimary) return rightPrimary - leftPrimary;
|
|
425
|
+
const noise = pathNoisePenalty(left.path, retrievalIntent, retrievalMode)
|
|
426
|
+
- pathNoisePenalty(right.path, retrievalIntent, retrievalMode);
|
|
427
|
+
if (noise !== 0) return noise;
|
|
689
428
|
if (left.reasons.length !== right.reasons.length) return right.reasons.length - left.reasons.length;
|
|
690
429
|
if (left.lines.length !== right.lines.length) return right.lines.length - left.lines.length;
|
|
691
430
|
return left.path.localeCompare(right.path);
|
|
692
431
|
});
|
|
693
|
-
const
|
|
432
|
+
const isExactKnownCandidate = (entry: { reasons: string[] }): boolean => entry.reasons.includes("explicit-known-path");
|
|
433
|
+
const multiWaveEligible = retrievalMode !== "implementation";
|
|
434
|
+
timingsMs.expansionBudgetMax = multiWaveEligible ? Math.min(6, Math.max(2, Math.ceil(maxFiles / 2))) : 0;
|
|
435
|
+
const rankedExactKnownCount = rankedCandidates.filter(isExactKnownCandidate).length;
|
|
436
|
+
const initialCandidateLimit = multiWaveEligible
|
|
437
|
+
? Math.min(maxFiles, Math.max(rankedExactKnownCount, Math.max(1, maxFiles - timingsMs.expansionBudgetMax)))
|
|
438
|
+
: maxFiles;
|
|
439
|
+
const selected = rankedCandidates.slice(0, initialCandidateLimit);
|
|
440
|
+
const deferredSelected = rankedCandidates.slice(initialCandidateLimit, maxFiles);
|
|
441
|
+
const selectedExactKnownCount = selected.filter(isExactKnownCandidate).length;
|
|
442
|
+
// Reserve at least one materialization for every selected exact known file.
|
|
443
|
+
// This may raise a caller's inferred snippet budget, but never beyond the
|
|
444
|
+
// existing hard response cap.
|
|
445
|
+
maxSnippets = Math.min(80, Math.max(requestedMaxSnippets, selectedExactKnownCount));
|
|
694
446
|
const selectedPaths = new Set(selected.map((entry) => entry.path));
|
|
695
447
|
const impactCoverage = impactDomains.map((domain) => {
|
|
696
448
|
const matchedFiles = [...(impactCandidatePaths.get(domain) ?? new Set<string>())].sort();
|
|
@@ -708,40 +460,45 @@ export function buildControllerContextPack(
|
|
|
708
460
|
|
|
709
461
|
let remainingSnippets = maxSnippets;
|
|
710
462
|
const files: ControllerContextPackFile[] = [];
|
|
711
|
-
|
|
463
|
+
const expansionDiscoveredPaths: string[] = [];
|
|
464
|
+
const expansionMaterializedPaths: string[] = [];
|
|
465
|
+
const materializationStartedAt = performance.now();
|
|
466
|
+
for (let selectedIndex = 0; selectedIndex < selected.length; selectedIndex += 1) {
|
|
467
|
+
const entry = selected[selectedIndex]!;
|
|
712
468
|
if (remainingSnippets <= 0) {
|
|
713
469
|
omitted.push({ path: entry.path, reason: "max_snippets" });
|
|
714
470
|
continue;
|
|
715
471
|
}
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
} catch (error) {
|
|
743
|
-
deniedPaths.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
|
|
472
|
+
const exactKnown = isExactKnownCandidate(entry);
|
|
473
|
+
const exactKnownAfter = selected
|
|
474
|
+
.slice(selectedIndex + 1)
|
|
475
|
+
.filter(isExactKnownCandidate)
|
|
476
|
+
.length;
|
|
477
|
+
const waveOnePerFileCap = multiWaveEligible
|
|
478
|
+
? Math.max(1, Math.ceil(maxSnippets / Math.max(1, initialCandidateLimit + timingsMs.expansionBudgetMax)))
|
|
479
|
+
: remainingSnippets;
|
|
480
|
+
const fileSnippetBudget = exactKnown
|
|
481
|
+
? Math.max(1, remainingSnippets - exactKnownAfter)
|
|
482
|
+
: Math.min(remainingSnippets, waveOnePerFileCap);
|
|
483
|
+
let snippets: ControllerContextPackSnippet[] = [];
|
|
484
|
+
try {
|
|
485
|
+
snippets = materializeSource({
|
|
486
|
+
repoRoot,
|
|
487
|
+
policy,
|
|
488
|
+
path: entry.path,
|
|
489
|
+
hitLines: entry.lines.length > 0 ? entry.lines : [1],
|
|
490
|
+
reasons: entry.reasons,
|
|
491
|
+
maxSnippets: Math.min(fileSnippetBudget, remainingSnippets),
|
|
492
|
+
maxCharsPerSnippet,
|
|
493
|
+
session: options.session,
|
|
494
|
+
});
|
|
495
|
+
for (const snippet of snippets) {
|
|
496
|
+
if (snippet.cacheHit) rangeCacheHits += 1;
|
|
497
|
+
else rangeCacheMisses += 1;
|
|
744
498
|
}
|
|
499
|
+
remainingSnippets -= snippets.length;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
deniedPaths.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
|
|
745
502
|
}
|
|
746
503
|
if (snippets.length > 0) {
|
|
747
504
|
files.push({
|
|
@@ -754,11 +511,152 @@ export function buildControllerContextPack(
|
|
|
754
511
|
}
|
|
755
512
|
}
|
|
756
513
|
|
|
514
|
+
// Investigation modes perform bounded internal evidence-closure waves after
|
|
515
|
+
// Wave 1 raw source materialization. Each later wave derives concrete paths
|
|
516
|
+
// from the source that was just read plus current CodeGraph adjacency, then
|
|
517
|
+
// materializes those paths in the same context request.
|
|
518
|
+
const materializedPathSet = new Set(files.map((file) => file.path));
|
|
519
|
+
let frontier = files.slice();
|
|
520
|
+
if (multiWaveEligible && frontier.length > 0 && timingsMs.expansionBudgetMax > 0) {
|
|
521
|
+
for (let wave = 2; wave <= MAX_CONTEXT_EVIDENCE_WAVES; wave += 1) {
|
|
522
|
+
if (frontier.length === 0 || timingsMs.expansionBudgetUsed >= timingsMs.expansionBudgetMax || files.length >= maxFiles || remainingSnippets <= 0) break;
|
|
523
|
+
const expansionReasons = new Map<string, { reasons: Set<string>; lines: Set<number> }>();
|
|
524
|
+
const addExpansionCandidate = (path: string, reason: string, line = 1): void => {
|
|
525
|
+
if (!path || materializedPathSet.has(path) || !coveredByGlob(path, searchIncludeGlobs) || excludeGlobs.some((glob) => globMatches(glob, path))) return;
|
|
526
|
+
const readable = readableFile(repoRoot, policy, path);
|
|
527
|
+
if (!readable.ok) {
|
|
528
|
+
if (/denied|policy/i.test(readable.reason)) deniedPaths.push({ path: readable.path, reason: readable.reason });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const current = expansionReasons.get(readable.path) ?? { reasons: new Set<string>(), lines: new Set<number>() };
|
|
532
|
+
current.reasons.add(reason);
|
|
533
|
+
current.lines.add(line);
|
|
534
|
+
expansionReasons.set(readable.path, current);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
for (const file of frontier.slice(0, 6)) {
|
|
538
|
+
const contents = file.snippets.map((snippet) => snippet.content).join('\n');
|
|
539
|
+
for (const specifier of sourceReferenceSpecifiers(contents)) {
|
|
540
|
+
const resolved = resolveSourceReference(repoRoot, policy, file.path, specifier);
|
|
541
|
+
if (resolved) addExpansionCandidate(resolved, `source-reference:${file.path}`);
|
|
542
|
+
}
|
|
543
|
+
if (structuralContext.status !== 'ready') continue;
|
|
544
|
+
const adjacency = queryStructural(structuralIndexRoot, { operation: 'file_dependencies', filePath: file.path, limit: 40 });
|
|
545
|
+
if (!adjacency.ok || adjacency.status !== 'ready') continue;
|
|
546
|
+
const dependencies = Array.isArray(adjacency.result?.dependencies)
|
|
547
|
+
? adjacency.result.dependencies.filter((value): value is string => typeof value === 'string')
|
|
548
|
+
: [];
|
|
549
|
+
const dependents = Array.isArray(adjacency.result?.dependents)
|
|
550
|
+
? adjacency.result.dependents.filter((value): value is string => typeof value === 'string')
|
|
551
|
+
: [];
|
|
552
|
+
for (const path of dependencies.slice(0, 40)) {
|
|
553
|
+
graphImpactFiles.add(path);
|
|
554
|
+
addExpansionCandidate(path, `codegraph:dependency:${file.path}`);
|
|
555
|
+
}
|
|
556
|
+
for (const path of dependents.slice(0, 40)) {
|
|
557
|
+
graphImpactFiles.add(path);
|
|
558
|
+
addExpansionCandidate(path, `codegraph:dependent:${file.path}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const expansionCandidates = Array.from(expansionReasons.entries())
|
|
563
|
+
.map(([path, entry]) => ({ path, reasons: Array.from(entry.reasons), lines: Array.from(entry.lines) }))
|
|
564
|
+
.sort((left, right) => {
|
|
565
|
+
const priority = contextPathPriority(right.path, right.reasons) - contextPathPriority(left.path, left.reasons);
|
|
566
|
+
if (priority !== 0) return priority;
|
|
567
|
+
if (left.reasons.length !== right.reasons.length) return right.reasons.length - left.reasons.length;
|
|
568
|
+
return left.path.localeCompare(right.path);
|
|
569
|
+
});
|
|
570
|
+
expansionDiscoveredPaths.push(...expansionCandidates.map((entry) => entry.path));
|
|
571
|
+
if (expansionCandidates.length === 0) break;
|
|
572
|
+
|
|
573
|
+
const nextFrontier: ControllerContextPackFile[] = [];
|
|
574
|
+
for (const entry of expansionCandidates) {
|
|
575
|
+
if (timingsMs.expansionBudgetUsed >= timingsMs.expansionBudgetMax || files.length >= maxFiles || remainingSnippets <= 0) break;
|
|
576
|
+
try {
|
|
577
|
+
const snippets = materializeSource({
|
|
578
|
+
repoRoot,
|
|
579
|
+
policy,
|
|
580
|
+
path: entry.path,
|
|
581
|
+
hitLines: entry.lines.length > 0 ? entry.lines : [1],
|
|
582
|
+
reasons: entry.reasons,
|
|
583
|
+
maxSnippets: Math.min(3, remainingSnippets),
|
|
584
|
+
maxCharsPerSnippet,
|
|
585
|
+
session: options.session,
|
|
586
|
+
});
|
|
587
|
+
for (const snippet of snippets) {
|
|
588
|
+
if (snippet.cacheHit) rangeCacheHits += 1;
|
|
589
|
+
else rangeCacheMisses += 1;
|
|
590
|
+
}
|
|
591
|
+
remainingSnippets -= snippets.length;
|
|
592
|
+
if (snippets.length === 0) continue;
|
|
593
|
+
const materialized: ControllerContextPackFile = {
|
|
594
|
+
path: entry.path,
|
|
595
|
+
reasons: entry.reasons,
|
|
596
|
+
hitLines: Array.from(new Set(entry.lines)).sort((a, b) => a - b).slice(0, 30),
|
|
597
|
+
snippetCount: snippets.length,
|
|
598
|
+
snippets,
|
|
599
|
+
};
|
|
600
|
+
files.push(materialized);
|
|
601
|
+
materializedPathSet.add(entry.path);
|
|
602
|
+
expansionMaterializedPaths.push(entry.path);
|
|
603
|
+
nextFrontier.push(materialized);
|
|
604
|
+
timingsMs.expansionBudgetUsed += 1;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
deniedPaths.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (nextFrontier.length === 0) break;
|
|
610
|
+
timingsMs.waveCount = wave;
|
|
611
|
+
frontier = nextFrontier;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Fill any reserved first-pass candidates only after expansion has had a
|
|
616
|
+
// chance to use its bounded slots; this preserves ordinary discovery evidence
|
|
617
|
+
// when no higher-value relationship is found.
|
|
618
|
+
for (const entry of deferredSelected) {
|
|
619
|
+
if (files.length >= maxFiles || remainingSnippets <= 0 || materializedPathSet.has(entry.path)) continue;
|
|
620
|
+
try {
|
|
621
|
+
const snippets = materializeSource({
|
|
622
|
+
repoRoot,
|
|
623
|
+
policy,
|
|
624
|
+
path: entry.path,
|
|
625
|
+
hitLines: entry.lines.length > 0 ? entry.lines : [1],
|
|
626
|
+
reasons: entry.reasons,
|
|
627
|
+
maxSnippets: remainingSnippets,
|
|
628
|
+
maxCharsPerSnippet,
|
|
629
|
+
session: options.session,
|
|
630
|
+
});
|
|
631
|
+
for (const snippet of snippets) {
|
|
632
|
+
if (snippet.cacheHit) rangeCacheHits += 1;
|
|
633
|
+
else rangeCacheMisses += 1;
|
|
634
|
+
}
|
|
635
|
+
remainingSnippets -= snippets.length;
|
|
636
|
+
if (snippets.length > 0) {
|
|
637
|
+
files.push({
|
|
638
|
+
path: entry.path,
|
|
639
|
+
reasons: entry.reasons,
|
|
640
|
+
hitLines: Array.from(new Set(entry.lines)).sort((a, b) => a - b).slice(0, 30),
|
|
641
|
+
snippetCount: snippets.length,
|
|
642
|
+
snippets,
|
|
643
|
+
});
|
|
644
|
+
materializedPathSet.add(entry.path);
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
deniedPaths.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
timingsMs.materialization = Math.round((performance.now() - materializationStartedAt) * 100) / 100;
|
|
757
652
|
if (files.length === 0 && terms.length === 0 && explicitKnownPaths.length === 0) {
|
|
758
653
|
omitted.push({ path: "<none>", reason: "No description, search_terms, issue/task focus, or known_paths produced search terms." });
|
|
759
654
|
}
|
|
760
655
|
|
|
761
656
|
const rawSnippetTruncated = files.some((file) => file.snippets.some((snippet) => snippet.truncated));
|
|
657
|
+
const unresolvedExpansionPaths = Array.from(new Set(expansionDiscoveredPaths)).filter((path) => !files.some((file) => file.path === path));
|
|
658
|
+
const expansionBudgetExhausted = unresolvedExpansionPaths.length > 0
|
|
659
|
+
&& (timingsMs.expansionBudgetUsed >= timingsMs.expansionBudgetMax || files.length >= maxFiles || remainingSnippets <= 0);
|
|
762
660
|
const expansionSignals = [
|
|
763
661
|
...(files.length === 0 ? ["no_current_raw_source"] : []),
|
|
764
662
|
...(rawSnippetTruncated ? ["raw_snippet_truncated"] : []),
|
|
@@ -768,6 +666,7 @@ export function buildControllerContextPack(
|
|
|
768
666
|
...(structuralMode === "required" && !structuralContext.requiredSatisfied ? ["required_structural_context_unavailable"] : []),
|
|
769
667
|
...(policyDeniedFiles > 0 || deniedPaths.length > 0 ? ["policy_denied_candidates"] : []),
|
|
770
668
|
...(skippedLargeFiles > 0 ? ["large_candidates_skipped"] : []),
|
|
669
|
+
...(expansionBudgetExhausted ? ["evidence_expansion_budget_exhausted"] : []),
|
|
771
670
|
...impactCoverage.flatMap((coverage) => coverage.status === "no_evidence"
|
|
772
671
|
? [`impact_domain_without_evidence:${coverage.domain}`]
|
|
773
672
|
: coverage.status === "matched_not_selected"
|
|
@@ -777,12 +676,18 @@ export function buildControllerContextPack(
|
|
|
777
676
|
const rawCodeRequiredForImplementation = retrievalMode !== "implementation"
|
|
778
677
|
|| files.length === 0
|
|
779
678
|
|| rawSnippetTruncated;
|
|
780
|
-
const
|
|
679
|
+
const graphEntryPaths = Array.from(new Set(structuralContext.entryPoints.map((entry) => entry.filePath).filter(Boolean))).slice(0, 20);
|
|
680
|
+
const structuralEvidenceCurrent = structuralContext.status === "ready";
|
|
681
|
+
const primaryTargets = structuralEvidenceCurrent ? graphEntryPaths : [];
|
|
682
|
+
const structuralHints = structuralEvidenceCurrent
|
|
683
|
+
? []
|
|
684
|
+
: Array.from(new Set([...graphEntryPaths, ...graphImpactFiles, ...structuralContext.relatedFiles])).slice(0, 80);
|
|
781
685
|
const structuralUniverse = Array.from(new Set([
|
|
782
|
-
...
|
|
686
|
+
...graphEntryPaths,
|
|
783
687
|
...graphImpactFiles,
|
|
784
688
|
...structuralContext.relatedFiles,
|
|
785
689
|
...selected.map((entry) => entry.path),
|
|
690
|
+
...files.map((file) => file.path),
|
|
786
691
|
])).slice(0, 160);
|
|
787
692
|
const relevantTests = structuralUniverse.filter((path) => /(^|\/)(__tests__|tests?|spec)(\/|$)|\.(test|spec)\.[^.]+$/i.test(path)).slice(0, 40);
|
|
788
693
|
const architectureContracts = structuralUniverse.filter((path) =>
|
|
@@ -791,7 +696,9 @@ export function buildControllerContextPack(
|
|
|
791
696
|
|| path.startsWith("docs/architecture/")
|
|
792
697
|
|| path.startsWith(".ai/context/"),
|
|
793
698
|
).slice(0, 40);
|
|
794
|
-
const mustInspect =
|
|
699
|
+
const mustInspect = structuralEvidenceCurrent
|
|
700
|
+
? Array.from(new Set([...primaryTargets, ...graphImpactFiles, ...relevantTests, ...architectureContracts])).slice(0, 80)
|
|
701
|
+
: [];
|
|
795
702
|
const mustInspectSet = new Set(mustInspect);
|
|
796
703
|
const likelyAffected = structuralUniverse.filter((path) => !mustInspectSet.has(path)).slice(0, 80);
|
|
797
704
|
const changedFileCount = structuralContext.metadata
|
|
@@ -816,6 +723,7 @@ export function buildControllerContextPack(
|
|
|
816
723
|
? "medium"
|
|
817
724
|
: "low",
|
|
818
725
|
primaryTargets,
|
|
726
|
+
structuralHints,
|
|
819
727
|
mustInspect,
|
|
820
728
|
likelyAffected,
|
|
821
729
|
relevantTests,
|
|
@@ -836,6 +744,107 @@ export function buildControllerContextPack(
|
|
|
836
744
|
baselineRevisionMatches: structuralContext.baselineRevisionMatches,
|
|
837
745
|
},
|
|
838
746
|
};
|
|
747
|
+
const inspectedFiles = files.map((file) => file.path);
|
|
748
|
+
const instructionContext = buildRepositoryInstructionContext(
|
|
749
|
+
repoRoot,
|
|
750
|
+
policy,
|
|
751
|
+
[...exactKnownFiles, ...inspectedFiles],
|
|
752
|
+
{ maxCharsPerContract: Math.min(maxCharsPerSnippet, 12_000) },
|
|
753
|
+
);
|
|
754
|
+
const governanceStartedAt = performance.now();
|
|
755
|
+
const governanceResolution = resolveRepositoryGovernance(repoRoot, policy, {
|
|
756
|
+
goal,
|
|
757
|
+
targetPaths: [...exactKnownFiles, ...inspectedFiles],
|
|
758
|
+
changedPaths: gitStatusChangedPaths(git.status),
|
|
759
|
+
});
|
|
760
|
+
const availableChecks = governanceResolution.recommendedCheckIds.length > 0
|
|
761
|
+
? new Map(listControllerChecks(repoRoot).map((check) => [check.id, check]))
|
|
762
|
+
: new Map();
|
|
763
|
+
const governanceContext: ControllerContextPackProjection['governanceContext'] = {
|
|
764
|
+
...governanceResolution,
|
|
765
|
+
recommendedChecks: governanceResolution.recommendedCheckIds.map((checkId) => {
|
|
766
|
+
const check = availableChecks.get(checkId);
|
|
767
|
+
const ruleIds = governanceResolution.activeRules.filter((rule) => rule.checkIds.includes(checkId)).map((rule) => rule.id).sort();
|
|
768
|
+
if (!check) return { checkId, ruleIds, available: false };
|
|
769
|
+
const selection = controllerCheckSelection(check);
|
|
770
|
+
return { checkId, ruleIds, available: true, ...selection };
|
|
771
|
+
}),
|
|
772
|
+
coverageGaps: [
|
|
773
|
+
...governanceResolution.coverageGaps,
|
|
774
|
+
...governanceResolution.recommendedCheckIds
|
|
775
|
+
.filter((checkId) => !availableChecks.has(checkId))
|
|
776
|
+
.map((checkId) => `governance_check_unregistered:${checkId}`),
|
|
777
|
+
].filter((value, index, values) => values.indexOf(value) === index).slice(0, 80),
|
|
778
|
+
};
|
|
779
|
+
if (governanceContext.coverageGaps.length > 0 && governanceContext.status === 'ready') governanceContext.status = 'degraded';
|
|
780
|
+
timingsMs.governance = Math.round((performance.now() - governanceStartedAt) * 100) / 100;
|
|
781
|
+
const inspectedSet = new Set(inspectedFiles);
|
|
782
|
+
const likelyRelatedNotInspected = Array.from(new Set([
|
|
783
|
+
...rankedCandidates.map((entry) => entry.path),
|
|
784
|
+
...structuralContext.relatedFiles,
|
|
785
|
+
...graphImpactFiles,
|
|
786
|
+
...expansionDiscoveredPaths,
|
|
787
|
+
])).filter((path) => !inspectedSet.has(path)).slice(0, 80);
|
|
788
|
+
const materializedExactKnownPaths = files
|
|
789
|
+
.filter((file) => file.reasons.includes("explicit-known-path"))
|
|
790
|
+
.map((file) => file.path);
|
|
791
|
+
const materializedExactKnownSet = new Set(materializedExactKnownPaths);
|
|
792
|
+
const unresolvedRelationships = Array.from(new Set([
|
|
793
|
+
...coverageGaps,
|
|
794
|
+
...expansionSignals,
|
|
795
|
+
...(likelyRelatedNotInspected.length > 0 ? ["likely_related_files_not_inspected"] : []),
|
|
796
|
+
])).slice(0, 80);
|
|
797
|
+
const readinessReasonCodes = Array.from(new Set([
|
|
798
|
+
...(files.length === 0 ? ["raw_source_unavailable"] : []),
|
|
799
|
+
...(rawSnippetTruncated ? ["raw_source_truncated"] : []),
|
|
800
|
+
...(searchTruncated ? ["retrieval_truncated"] : []),
|
|
801
|
+
...(structuralMode === "required" && !structuralContext.requiredSatisfied ? ["required_structural_context_unsatisfied"] : []),
|
|
802
|
+
...(structuralContext.status === "stale" ? ["structural_context_stale"] : []),
|
|
803
|
+
...(structuralContext.status === "unavailable" || structuralContext.status === "degraded" ? ["structural_provider_degraded"] : []),
|
|
804
|
+
...(policyDeniedFiles > 0 || deniedPaths.length > 0 ? ["policy_denied_evidence"] : []),
|
|
805
|
+
...(skippedLargeFiles > 0 ? ["large_evidence_skipped"] : []),
|
|
806
|
+
...(expansionBudgetExhausted ? ["expansion_budget_exhausted"] : []),
|
|
807
|
+
...(likelyRelatedNotInspected.length > 0 ? ["related_evidence_not_materialized"] : []),
|
|
808
|
+
])).slice(0, 40);
|
|
809
|
+
const readinessStatus = files.length === 0 || (structuralMode === "required" && !structuralContext.requiredSatisfied)
|
|
810
|
+
? "insufficient" as const
|
|
811
|
+
: readinessReasonCodes.length > 0
|
|
812
|
+
? "degraded" as const
|
|
813
|
+
: "ready" as const;
|
|
814
|
+
const readiness: ControllerContextPackProjection["readiness"] = {
|
|
815
|
+
status: readinessStatus,
|
|
816
|
+
sourceRevision: git.head,
|
|
817
|
+
rawSource: {
|
|
818
|
+
status: files.length === 0 ? "unavailable" : rawSnippetTruncated ? "partial" : "current",
|
|
819
|
+
materializedFileCount: files.length,
|
|
820
|
+
snippetTruncated: rawSnippetTruncated,
|
|
821
|
+
},
|
|
822
|
+
structural: {
|
|
823
|
+
requested: structuralMode,
|
|
824
|
+
status: structuralContext.status,
|
|
825
|
+
requiredSatisfied: structuralContext.requiredSatisfied,
|
|
826
|
+
baselineRevisionMatches: structuralContext.baselineRevisionMatches,
|
|
827
|
+
overlayChangedFileCount: structuralContext.overlayChangedFiles.length,
|
|
828
|
+
},
|
|
829
|
+
semantic: { status: "not_requested", reasonCodes: ["semantic_navigation_not_requested"] },
|
|
830
|
+
retrieval: {
|
|
831
|
+
searchTruncated,
|
|
832
|
+
omittedCandidateCount: rankedCandidates.filter((entry) => !inspectedSet.has(entry.path)).length,
|
|
833
|
+
policyDeniedCandidateCount: policyDeniedFiles + deniedPaths.length,
|
|
834
|
+
likelyRelatedNotInspectedCount: likelyRelatedNotInspected.length,
|
|
835
|
+
},
|
|
836
|
+
unresolvedReasonCodes: readinessReasonCodes,
|
|
837
|
+
readyForHighConfidenceMutation: readinessStatus === "ready",
|
|
838
|
+
};
|
|
839
|
+
const expansion: ControllerContextPackProjection["expansion"] = {
|
|
840
|
+
waveCount: timingsMs.waveCount,
|
|
841
|
+
expansionPerformed: timingsMs.waveCount > 1,
|
|
842
|
+
expansionBudgetUsed: timingsMs.expansionBudgetUsed,
|
|
843
|
+
expansionBudgetMax: timingsMs.expansionBudgetMax,
|
|
844
|
+
discoveredPaths: Array.from(new Set(expansionDiscoveredPaths)).slice(0, 30),
|
|
845
|
+
materializedPaths: Array.from(new Set(expansionMaterializedPaths)).slice(0, 30),
|
|
846
|
+
};
|
|
847
|
+
timingsMs.total = Math.round((performance.now() - contextStartedAt) * 100) / 100;
|
|
839
848
|
|
|
840
849
|
return {
|
|
841
850
|
schemaVersion: CONTEXT_PACK_SCHEMA_VERSION,
|
|
@@ -861,31 +870,83 @@ export function buildControllerContextPack(
|
|
|
861
870
|
skippedLargeFiles,
|
|
862
871
|
skippedBinaryFiles,
|
|
863
872
|
truncated: searchTruncated,
|
|
873
|
+
cacheHit: lexicalCacheHit,
|
|
864
874
|
},
|
|
875
|
+
instructionContext,
|
|
876
|
+
governanceContext,
|
|
865
877
|
structuralContext,
|
|
866
878
|
impactContext,
|
|
879
|
+
readiness,
|
|
880
|
+
expansion,
|
|
867
881
|
files,
|
|
882
|
+
coverage: {
|
|
883
|
+
inspectedFiles,
|
|
884
|
+
likelyRelatedNotInspected,
|
|
885
|
+
unresolvedRelationships,
|
|
886
|
+
relevantTests,
|
|
887
|
+
exactKnownPaths: {
|
|
888
|
+
requested: exactKnownFiles,
|
|
889
|
+
materialized: materializedExactKnownPaths,
|
|
890
|
+
missing: exactKnownFiles.filter((path) => !materializedExactKnownSet.has(path)),
|
|
891
|
+
},
|
|
892
|
+
skippedCandidates: {
|
|
893
|
+
omittedFiles: Math.max(0, rankedCandidates.length - selected.length),
|
|
894
|
+
policyDeniedFiles: policyDeniedFiles + deniedPaths.length,
|
|
895
|
+
largeFiles: skippedLargeFiles,
|
|
896
|
+
binaryFiles: skippedBinaryFiles,
|
|
897
|
+
searchTruncated,
|
|
898
|
+
structuralTruncated: structuralContext.truncated,
|
|
899
|
+
},
|
|
900
|
+
materialization: {
|
|
901
|
+
completeFiles: files.flatMap((file) => file.snippets).filter((snippet) => snippet.materialization === "complete_file").length,
|
|
902
|
+
symbols: files.flatMap((file) => file.snippets).filter((snippet) => snippet.materialization === "symbol").length,
|
|
903
|
+
fallbackWindows: files.flatMap((file) => file.snippets).filter((snippet) => snippet.materialization === "line_window").length,
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
cache: {
|
|
907
|
+
sessionBound: Boolean(options.session),
|
|
908
|
+
lexicalHit: lexicalCacheHit,
|
|
909
|
+
structuralHits: structuralCacheHits,
|
|
910
|
+
structuralMisses: structuralCacheMisses,
|
|
911
|
+
rangeHits: rangeCacheHits,
|
|
912
|
+
rangeMisses: rangeCacheMisses,
|
|
913
|
+
reused: lexicalCacheHit || structuralCacheHits > 0 || rangeCacheHits > 0,
|
|
914
|
+
},
|
|
915
|
+
timingsMs,
|
|
868
916
|
deniedPaths: deniedPaths.slice(0, 20),
|
|
869
917
|
omitted: omitted.slice(0, 30),
|
|
870
|
-
limits: {
|
|
918
|
+
limits: {
|
|
919
|
+
maxFiles,
|
|
920
|
+
maxSnippets,
|
|
921
|
+
maxCharsPerSnippet,
|
|
922
|
+
requestedMaxFiles,
|
|
923
|
+
requestedMaxSnippets,
|
|
924
|
+
reservedExactKnownFiles: selectedExactKnownCount,
|
|
925
|
+
},
|
|
871
926
|
validation: {
|
|
872
927
|
policy: taskChecks.length > 0 ? "task-targeted" : "minimal",
|
|
873
928
|
checks: taskChecks,
|
|
874
929
|
},
|
|
875
930
|
contextContract: {
|
|
876
931
|
strategy: retrievalMode === "implementation"
|
|
877
|
-
? "Use
|
|
878
|
-
: "Use
|
|
932
|
+
? "Use one broad current-source pack to discover credible entry points, then progressively expand from returned paths and symbols. Lexical terms are heuristic hints rather than completeness requirements. ChatGPT owns semantic sufficiency and should prefer targeted known_paths, compiler semantic navigation for concrete TypeScript symbols, or structural relationships before repeating broad lexical discovery."
|
|
933
|
+
: "Use the bounded evidence-closure waves already performed inside this rh_context call. Repeat rh_context only when unresolved material relationships remain after the internal expansion budget, or when the Controller intentionally changes evidence scope.",
|
|
879
934
|
retrievalMode,
|
|
880
935
|
semanticSufficiencyAuthority: "chatgpt",
|
|
881
936
|
rawCodeRequiredForImplementation,
|
|
882
937
|
expansionSignals,
|
|
883
938
|
notes: [
|
|
884
939
|
retrievalMode === "implementation"
|
|
885
|
-
? "The pack
|
|
886
|
-
:
|
|
887
|
-
"Search/CodeGraph ranking is discovery evidence, not a business-semantics authority. impactContext
|
|
940
|
+
? "The pack contains policy-approved current raw source with file SHA identities, but returned snippets are bounded evidence rather than proof of semantic completeness. After reading this evidence, derive new exact paths, symbols, tests, or relationships from the source itself. A guessed lexical term with no result is not by itself a reason to keep scanning or repeat the same broad query."
|
|
941
|
+
: `Investigation mode performed ${timingsMs.waveCount} bounded evidence wave(s) inside this request; expansion budget ${timingsMs.expansionBudgetUsed}/${timingsMs.expansionBudgetMax}.`,
|
|
942
|
+
"Search/CodeGraph ranking is discovery evidence, not a business-semantics authority. readiness and impactContext make the bounded evidence surface and unresolved gaps explicit; ChatGPT still decides semantic sufficiency.",
|
|
943
|
+
instructionContext.contracts.length > 0
|
|
944
|
+
? `Applicable repository guidance was resolved hierarchically for selected source paths: ${instructionContext.contracts.map((entry) => entry.path).join(", ")}. These AGENTS.md/CLAUDE.md files are guidance-only evidence and do not define semantic scope.`
|
|
945
|
+
: "No applicable AGENTS.md/CLAUDE.md guidance file was found for the selected source paths.",
|
|
888
946
|
impactDomains.length > 0 ? `Impact domains were selected by ChatGPT and expanded mechanically in this same retrieval call: ${impactDomains.join(", ")}. Missing or omitted domain evidence is an ambiguity signal, not proof that the domain is irrelevant.` : "No explicit cross-cutting impact domains were requested; ChatGPT may add them when state, scheduling, notifications, events, caching, API, or concurrency could materially change the implementation.",
|
|
947
|
+
governanceContext.status === 'none'
|
|
948
|
+
? "No repository governance rule registry is present; existing guidance and explicit Task checks behave as before."
|
|
949
|
+
: `Repository governance resolved ${governanceContext.activeRules.length} active rule(s), ${governanceContext.suppressedRules.length} suppressed rule(s), and ${governanceContext.recommendedCheckIds.length} recommended check(s) through bounded local matching. Recommendations are evidence only and never execute checks or create Work by themselves.`,
|
|
889
950
|
"CodeGraph structural evidence is discovery evidence. Raw source access still passes through Forge repository policy and current-file reads.",
|
|
890
951
|
structuralContext.status === "stale" ? "CodeGraph reports stale structural evidence. Treat graph relationships as hints and prefer the returned current raw source for changed files." : structuralContext.status === "unavailable" || structuralContext.status === "degraded" ? "Structural context was unavailable or degraded; bounded text search remains the fallback." : structuralContext.status === "ready" ? "CodeGraph structural context was queried read-only with index sync disabled." : "Structural context was not requested.",
|
|
891
952
|
"Run focused validation after a coherent edit batch; expensive full-suite checks belong at candidate/release boundaries unless debugging specifically needs them earlier.",
|
|
@@ -898,7 +959,7 @@ export function buildControllerContextPack(
|
|
|
898
959
|
: []),
|
|
899
960
|
files.length > 0
|
|
900
961
|
? retrievalMode === "implementation"
|
|
901
|
-
? "ChatGPT should decide whether
|
|
962
|
+
? "ChatGPT should decide whether impact coverage is sufficient. Edit when it is; otherwise derive the next exact paths/symbols/tests from this source and repeat rh_context narrowly. For concrete TypeScript symbols prefer semantic navigation; use another broad lexical pass only when no credible entry point exists or a dynamic/string/config edge still needs discovery."
|
|
902
963
|
: "Expand exact ranges or neighboring evidence when that investigation can materially change the plan, diagnosis, or review conclusion."
|
|
903
964
|
: "Provide known_paths or narrower search_terms before attempting implementation.",
|
|
904
965
|
"After editing, review the bounded diff/evidence for the coherent edit batch.",
|
|
@@ -906,3 +967,258 @@ export function buildControllerContextPack(
|
|
|
906
967
|
],
|
|
907
968
|
};
|
|
908
969
|
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Broad first-call fan-in for the canonical rh_context path. Lexical retrieval
|
|
973
|
+
* remains required evidence. Auto CodeGraph is opportunistic enrichment: it may
|
|
974
|
+
* finish inside the first-call latency budget, otherwise the request returns
|
|
975
|
+
* bounded lexical/raw evidence while the same source-bound structural request
|
|
976
|
+
* continues warming the session cache. Required structural mode still waits.
|
|
977
|
+
*/
|
|
978
|
+
export const AUTO_STRUCTURAL_PREFETCH_BUDGET_MS = 100;
|
|
979
|
+
export const AUTO_STRUCTURAL_PREFETCH_TIMEOUT_MS = 1_000;
|
|
980
|
+
const CODEGRAPH_PREFETCH_DEFERRED_CODE = 'CODEGRAPH_PREFETCH_DEFERRED';
|
|
981
|
+
const structuralPrefetchInflight = new Map<string, Promise<CodeGraphReadProviderResponse>>();
|
|
982
|
+
|
|
983
|
+
function codeGraphPrefetchDeferredResponse(elapsedMs: number): CodeGraphReadProviderResponse {
|
|
984
|
+
return {
|
|
985
|
+
schemaVersion: 1,
|
|
986
|
+
provider: 'codegraph',
|
|
987
|
+
operation: 'context',
|
|
988
|
+
ok: false,
|
|
989
|
+
status: 'degraded',
|
|
990
|
+
error: {
|
|
991
|
+
code: CODEGRAPH_PREFETCH_DEFERRED_CODE,
|
|
992
|
+
message: `Optional CodeGraph enrichment exceeded the ${AUTO_STRUCTURAL_PREFETCH_BUDGET_MS}ms first-call budget and continues warming the source-bound session cache.`,
|
|
993
|
+
},
|
|
994
|
+
timingsMs: { total: Math.round(elapsedMs * 100) / 100 },
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function settleWithinBudget<T>(promise: Promise<T>, budgetMs: number): Promise<{ settled: true; value: T } | { settled: false }> {
|
|
999
|
+
if (budgetMs <= 0) return { settled: false };
|
|
1000
|
+
let timer: NodeJS.Timeout | undefined;
|
|
1001
|
+
try {
|
|
1002
|
+
return await Promise.race([
|
|
1003
|
+
promise.then((value) => ({ settled: true as const, value })),
|
|
1004
|
+
new Promise<{ settled: false }>((resolveBudget) => {
|
|
1005
|
+
timer = setTimeout(() => resolveBudget({ settled: false }), budgetMs);
|
|
1006
|
+
timer.unref();
|
|
1007
|
+
}),
|
|
1008
|
+
]);
|
|
1009
|
+
} finally {
|
|
1010
|
+
if (timer) clearTimeout(timer);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function cacheDeferredStructuralResult(
|
|
1015
|
+
sessionCache: ReturnType<typeof getRepositoryReadSessionCache>,
|
|
1016
|
+
structuralCacheKey: string,
|
|
1017
|
+
sourceIdentity: string | undefined,
|
|
1018
|
+
response: CodeGraphReadProviderResponse,
|
|
1019
|
+
): void {
|
|
1020
|
+
if (!sessionCache || !sourceIdentity) return;
|
|
1021
|
+
if (response.error?.code === 'CODEGRAPH_TIMEOUT' || response.error?.code === CODEGRAPH_PREFETCH_DEFERRED_CODE) return;
|
|
1022
|
+
if (JSON.stringify(sessionCache.currentIdentity()) !== sourceIdentity) return;
|
|
1023
|
+
sessionCache.putStructural(structuralCacheKey, response);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export async function buildControllerContextPackAsync(
|
|
1027
|
+
repoRoot: string,
|
|
1028
|
+
policy: McpPolicy,
|
|
1029
|
+
options: ControllerContextPackOptions = {},
|
|
1030
|
+
dependencies: ControllerContextPackDependencies = {},
|
|
1031
|
+
): Promise<ControllerContextPackProjection> {
|
|
1032
|
+
const structuralMode = options.structuralContext ?? 'off';
|
|
1033
|
+
if ((dependencies.queryCodeGraph && !dependencies.queryCodeGraphAsync) || !options.session || options.issueId || options.taskId) {
|
|
1034
|
+
return buildControllerContextPack(repoRoot, policy, options, dependencies);
|
|
1035
|
+
}
|
|
1036
|
+
const prefetchStartedAt = performance.now();
|
|
1037
|
+
const retrievalMode = options.retrievalMode ?? 'implementation';
|
|
1038
|
+
const requestedMaxFiles = clamp(options.maxFiles, DEFAULT_MAX_FILES, 1, 30);
|
|
1039
|
+
const knownPaths = cleanList(options.knownPaths);
|
|
1040
|
+
const knownGlobs = knownPaths.filter(looksLikeGlob);
|
|
1041
|
+
const explicitKnownPaths = knownPaths.filter((path) => !looksLikeGlob(path));
|
|
1042
|
+
const includeGlobs = cleanList([...(options.includeGlobs ?? []), ...knownGlobs]);
|
|
1043
|
+
const excludeGlobs = cleanList([...DEFAULT_SEARCH_EXCLUDE_GLOBS, ...(options.excludeGlobs ?? [])]);
|
|
1044
|
+
const terms = cleanList([...(options.searchTerms ?? []), ...textTokens(options.description ?? '')]).slice(0, 14);
|
|
1045
|
+
const impactDomains = cleanList(options.impactDomains)
|
|
1046
|
+
.filter((domain): domain is ControllerContextImpactDomain => CONTROLLER_CONTEXT_IMPACT_DOMAINS.includes(domain as ControllerContextImpactDomain))
|
|
1047
|
+
.slice(0, 6);
|
|
1048
|
+
const exactKnownFiles: string[] = [];
|
|
1049
|
+
let exactKnownFileScope = explicitKnownPaths.length > 0;
|
|
1050
|
+
for (const path of explicitKnownPaths) {
|
|
1051
|
+
const expanded = expandKnownPath(repoRoot, policy, path, Math.max(requestedMaxFiles * 4, 40));
|
|
1052
|
+
if (!expanded.directory && expanded.files.length === 1 && expanded.denied.length === 0 && !expanded.truncated) {
|
|
1053
|
+
exactKnownFiles.push(expanded.files[0]!);
|
|
1054
|
+
} else {
|
|
1055
|
+
exactKnownFileScope = false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const maxFiles = Math.min(30, Math.max(requestedMaxFiles, exactKnownFiles.length));
|
|
1059
|
+
const scopedExactKnownFileSearch = exactKnownFileScope
|
|
1060
|
+
&& retrievalMode === 'implementation'
|
|
1061
|
+
&& structuralMode === 'off'
|
|
1062
|
+
&& impactDomains.length === 0
|
|
1063
|
+
&& includeGlobs.length === 0;
|
|
1064
|
+
const searchQueries = cleanList([
|
|
1065
|
+
...(terms[0] ? [terms[0]] : []),
|
|
1066
|
+
...impactDomains.flatMap((domain) => IMPACT_DOMAIN_TERMS[domain]),
|
|
1067
|
+
...terms.slice(1),
|
|
1068
|
+
]).slice(0, 32);
|
|
1069
|
+
const requiredSearchQueries = cleanList(options.searchTerms)
|
|
1070
|
+
.filter(isCodeShapedTerm)
|
|
1071
|
+
.slice(0, 2);
|
|
1072
|
+
const structuralQuery = structuralMode === 'off'
|
|
1073
|
+
? ''
|
|
1074
|
+
: structuralIntentQuery(options.description ?? '', terms, impactDomains);
|
|
1075
|
+
const structuralIndexRoot = options.structuralIndexRoot?.trim() || repoRoot;
|
|
1076
|
+
const structuralRequest = {
|
|
1077
|
+
operation: 'context' as const,
|
|
1078
|
+
query: structuralQuery,
|
|
1079
|
+
limit: Math.min(Math.max(maxFiles, 4), 12),
|
|
1080
|
+
maxNodes: Math.min(Math.max(maxFiles * 5, 20), 60),
|
|
1081
|
+
maxDepth: 2,
|
|
1082
|
+
};
|
|
1083
|
+
const structuralCacheKey = JSON.stringify({ queryRoot: structuralIndexRoot, request: structuralRequest });
|
|
1084
|
+
const sessionCache = getRepositoryReadSessionCache(repoRoot, options.session);
|
|
1085
|
+
const structuralSourceIdentity = sessionCache ? JSON.stringify(sessionCache.currentIdentity()) : undefined;
|
|
1086
|
+
const cachedStructural = structuralQuery
|
|
1087
|
+
? sessionCache?.peekStructural(structuralCacheKey) as CodeGraphReadProviderResponse | undefined
|
|
1088
|
+
: undefined;
|
|
1089
|
+
const lexicalOptions = searchQueries.length > 0
|
|
1090
|
+
? (() => {
|
|
1091
|
+
const git = gitSnapshot(repoRoot, options.session);
|
|
1092
|
+
return {
|
|
1093
|
+
queries: searchQueries,
|
|
1094
|
+
files: scopedExactKnownFileSearch ? exactKnownFiles : undefined,
|
|
1095
|
+
includeGlobs: scopedExactKnownFileSearch ? undefined : includeGlobs,
|
|
1096
|
+
excludeGlobs,
|
|
1097
|
+
maxResultsPerQuery: Math.max(maxFiles * 4, 12),
|
|
1098
|
+
maxFiles: scopedExactKnownFileSearch ? exactKnownFiles.length : MAX_TOTAL_SEARCHED_FILES,
|
|
1099
|
+
caseSensitive: false,
|
|
1100
|
+
cacheKey: JSON.stringify({ head: git.head, status: git.status, diffStat: git.diffStat }),
|
|
1101
|
+
...(!scopedExactKnownFileSearch ? {
|
|
1102
|
+
completionMode: 'discovery' as const,
|
|
1103
|
+
discoveryTargetFiles: Math.max(maxFiles * 2, 12),
|
|
1104
|
+
discoveryMinQueryCoverage: Math.min(3, searchQueries.length),
|
|
1105
|
+
requiredQueries: requiredSearchQueries,
|
|
1106
|
+
} : {}),
|
|
1107
|
+
};
|
|
1108
|
+
})()
|
|
1109
|
+
: undefined;
|
|
1110
|
+
const lexicalIdentity = lexicalOptions ? searchRepositoryManyCacheIdentity(lexicalOptions) : undefined;
|
|
1111
|
+
const cachedLexical = lexicalIdentity && sessionCache
|
|
1112
|
+
? sessionCache.getSearch(lexicalIdentity.batchQueryKey, lexicalIdentity.includeKey)
|
|
1113
|
+
: undefined;
|
|
1114
|
+
if ((!structuralQuery || cachedStructural) && (!lexicalIdentity || cachedLexical?.result)) {
|
|
1115
|
+
const pack = buildControllerContextPack(repoRoot, policy, options, dependencies);
|
|
1116
|
+
pack.timingsMs.parallelFirstPass = false;
|
|
1117
|
+
pack.timingsMs.parallelPrefetch = 0;
|
|
1118
|
+
pack.timingsMs.structuralPrefetchBudgetMs = structuralMode === 'auto' && structuralQuery ? AUTO_STRUCTURAL_PREFETCH_BUDGET_MS : 0;
|
|
1119
|
+
pack.timingsMs.structuralPrefetchDeferred = false;
|
|
1120
|
+
pack.timingsMs.structuralPrefetchReusedInFlight = false;
|
|
1121
|
+
return pack;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const noStructuralResponse: CodeGraphReadProviderResponse = {
|
|
1125
|
+
schemaVersion: 1,
|
|
1126
|
+
provider: 'codegraph',
|
|
1127
|
+
operation: 'context',
|
|
1128
|
+
ok: false,
|
|
1129
|
+
status: 'degraded',
|
|
1130
|
+
error: { code: 'CODEGRAPH_QUERY_REQUIRED', message: 'No context query was provided.' },
|
|
1131
|
+
timingsMs: { total: 0 },
|
|
1132
|
+
};
|
|
1133
|
+
let structuralSettled = Boolean(cachedStructural) || !structuralQuery;
|
|
1134
|
+
let structuralSettledValue: CodeGraphReadProviderResponse | undefined = cachedStructural || (!structuralQuery ? noStructuralResponse : undefined);
|
|
1135
|
+
const queryCodeGraphAsync = dependencies.queryCodeGraphAsync ?? queryCodeGraphReadProviderAsync;
|
|
1136
|
+
const structuralInflightKey = structuralQuery && structuralSourceIdentity
|
|
1137
|
+
? JSON.stringify({ structuralCacheKey, sourceIdentity: structuralSourceIdentity, structuralMode })
|
|
1138
|
+
: undefined;
|
|
1139
|
+
const existingStructuralInflight = structuralInflightKey ? structuralPrefetchInflight.get(structuralInflightKey) : undefined;
|
|
1140
|
+
const structuralPrefetchReusedInFlight = Boolean(existingStructuralInflight);
|
|
1141
|
+
const launchedStructuralPromise = cachedStructural
|
|
1142
|
+
? Promise.resolve(cachedStructural)
|
|
1143
|
+
: structuralQuery
|
|
1144
|
+
? existingStructuralInflight ?? queryCodeGraphAsync(
|
|
1145
|
+
structuralIndexRoot,
|
|
1146
|
+
structuralRequest,
|
|
1147
|
+
structuralMode === 'auto' ? { timeoutMs: AUTO_STRUCTURAL_PREFETCH_TIMEOUT_MS } : {},
|
|
1148
|
+
)
|
|
1149
|
+
: Promise.resolve(noStructuralResponse);
|
|
1150
|
+
if (structuralInflightKey && structuralQuery && !cachedStructural && !existingStructuralInflight) {
|
|
1151
|
+
structuralPrefetchInflight.set(structuralInflightKey, launchedStructuralPromise);
|
|
1152
|
+
void launchedStructuralPromise.then(
|
|
1153
|
+
() => { if (structuralPrefetchInflight.get(structuralInflightKey) === launchedStructuralPromise) structuralPrefetchInflight.delete(structuralInflightKey); },
|
|
1154
|
+
() => { if (structuralPrefetchInflight.get(structuralInflightKey) === launchedStructuralPromise) structuralPrefetchInflight.delete(structuralInflightKey); },
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const structuralPromise = launchedStructuralPromise.then((response) => {
|
|
1158
|
+
structuralSettled = true;
|
|
1159
|
+
structuralSettledValue = response;
|
|
1160
|
+
return response;
|
|
1161
|
+
});
|
|
1162
|
+
const lexicalPromise = lexicalOptions && lexicalIdentity && !cachedLexical?.result
|
|
1163
|
+
? searchRepositoryManyAsync(repoRoot, policy, lexicalOptions).then((result) => {
|
|
1164
|
+
sessionCache?.putSearch({
|
|
1165
|
+
query: lexicalIdentity.batchQueryKey,
|
|
1166
|
+
includeKey: lexicalIdentity.includeKey,
|
|
1167
|
+
result,
|
|
1168
|
+
scannedFiles: result.scannedFiles,
|
|
1169
|
+
});
|
|
1170
|
+
return result;
|
|
1171
|
+
})
|
|
1172
|
+
: Promise.resolve(cachedLexical?.result);
|
|
1173
|
+
|
|
1174
|
+
// Lexical/raw discovery is required first-call evidence. Auto structural work is
|
|
1175
|
+
// allowed to consume only the remainder of its budget measured from the start
|
|
1176
|
+
// of the parallel prefetch, so a slow lexical pass never gains an extra 100ms
|
|
1177
|
+
// structural tax after it completes.
|
|
1178
|
+
await lexicalPromise;
|
|
1179
|
+
let prefetched = structuralSettledValue ?? noStructuralResponse;
|
|
1180
|
+
let structuralPrefetchDeferred = false;
|
|
1181
|
+
if (structuralQuery && !cachedStructural) {
|
|
1182
|
+
if (structuralMode === 'required') {
|
|
1183
|
+
prefetched = await structuralPromise;
|
|
1184
|
+
} else if (structuralSettled && structuralSettledValue) {
|
|
1185
|
+
prefetched = structuralSettledValue;
|
|
1186
|
+
} else {
|
|
1187
|
+
const elapsedMs = performance.now() - prefetchStartedAt;
|
|
1188
|
+
// Reusing an already-deferred in-flight request should be effectively free:
|
|
1189
|
+
// do not make a progressive follow-up repay the first call's latency budget.
|
|
1190
|
+
const remainingBudgetMs = structuralPrefetchReusedInFlight
|
|
1191
|
+
? 0
|
|
1192
|
+
: Math.max(0, AUTO_STRUCTURAL_PREFETCH_BUDGET_MS - elapsedMs);
|
|
1193
|
+
const outcome = await settleWithinBudget(structuralPromise, remainingBudgetMs);
|
|
1194
|
+
if (outcome.settled) {
|
|
1195
|
+
prefetched = outcome.value;
|
|
1196
|
+
} else {
|
|
1197
|
+
structuralPrefetchDeferred = true;
|
|
1198
|
+
prefetched = codeGraphPrefetchDeferredResponse(performance.now() - prefetchStartedAt);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const fallbackQueryCodeGraph = dependencies.queryCodeGraph ?? queryCodeGraphReadProvider;
|
|
1204
|
+
const pack = buildControllerContextPack(repoRoot, policy, options, {
|
|
1205
|
+
queryCodeGraph: (queryRoot, request) => request.operation === 'context' && queryRoot === structuralIndexRoot
|
|
1206
|
+
? prefetched
|
|
1207
|
+
: fallbackQueryCodeGraph(queryRoot, request),
|
|
1208
|
+
});
|
|
1209
|
+
pack.timingsMs.parallelFirstPass = true;
|
|
1210
|
+
pack.timingsMs.parallelPrefetch = Math.round((performance.now() - prefetchStartedAt) * 100) / 100;
|
|
1211
|
+
pack.timingsMs.structuralPrefetchBudgetMs = structuralMode === 'auto' && structuralQuery ? AUTO_STRUCTURAL_PREFETCH_BUDGET_MS : 0;
|
|
1212
|
+
pack.timingsMs.structuralPrefetchDeferred = structuralPrefetchDeferred;
|
|
1213
|
+
pack.timingsMs.structuralPrefetchReusedInFlight = structuralPrefetchReusedInFlight;
|
|
1214
|
+
|
|
1215
|
+
if (structuralPrefetchDeferred) {
|
|
1216
|
+
// The provider request is intentionally not cancelled. Cache only if the
|
|
1217
|
+
// repository source identity still matches the request that launched it;
|
|
1218
|
+
// a mutation must never let late structural evidence attach to newer source.
|
|
1219
|
+
void structuralPromise
|
|
1220
|
+
.then((response) => cacheDeferredStructuralResult(sessionCache, structuralCacheKey, structuralSourceIdentity, response))
|
|
1221
|
+
.catch(() => undefined);
|
|
1222
|
+
}
|
|
1223
|
+
return pack;
|
|
1224
|
+
}
|