@mirnoorata/codexa 0.2.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/LICENSE +21 -0
- package/README.md +634 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +375 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/autonomy.d.ts +17 -0
- package/dist/autonomy.js +124 -0
- package/dist/autonomy.js.map +1 -0
- package/dist/autoverify/policy.d.ts +5 -0
- package/dist/autoverify/policy.js +18 -0
- package/dist/autoverify/policy.js.map +1 -0
- package/dist/autoverify.d.ts +45 -0
- package/dist/autoverify.js +1041 -0
- package/dist/autoverify.js.map +1 -0
- package/dist/cache-lock.d.ts +16 -0
- package/dist/cache-lock.js +181 -0
- package/dist/cache-lock.js.map +1 -0
- package/dist/cli/hooks.d.ts +5 -0
- package/dist/cli/hooks.js +264 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1034 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-contract.d.ts +2 -0
- package/dist/codex-contract.js +78 -0
- package/dist/codex-contract.js.map +1 -0
- package/dist/command.d.ts +34 -0
- package/dist/command.js +162 -0
- package/dist/command.js.map +1 -0
- package/dist/doctor.d.ts +112 -0
- package/dist/doctor.js +518 -0
- package/dist/doctor.js.map +1 -0
- package/dist/eval/baseline.d.ts +7 -0
- package/dist/eval/baseline.js +146 -0
- package/dist/eval/baseline.js.map +1 -0
- package/dist/eval/historical.d.ts +4 -0
- package/dist/eval/historical.js +663 -0
- package/dist/eval/historical.js.map +1 -0
- package/dist/eval/render.d.ts +2 -0
- package/dist/eval/render.js +53 -0
- package/dist/eval/render.js.map +1 -0
- package/dist/eval/scoring.d.ts +21 -0
- package/dist/eval/scoring.js +618 -0
- package/dist/eval/scoring.js.map +1 -0
- package/dist/eval/synthetic.d.ts +36 -0
- package/dist/eval/synthetic.js +107 -0
- package/dist/eval/synthetic.js.map +1 -0
- package/dist/eval/types.d.ts +36 -0
- package/dist/eval/types.js +2 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/eval.d.ts +140 -0
- package/dist/eval.js +551 -0
- package/dist/eval.js.map +1 -0
- package/dist/git.d.ts +17 -0
- package/dist/git.js +189 -0
- package/dist/git.js.map +1 -0
- package/dist/github-release.d.ts +47 -0
- package/dist/github-release.js +610 -0
- package/dist/github-release.js.map +1 -0
- package/dist/github-sync.d.ts +68 -0
- package/dist/github-sync.js +345 -0
- package/dist/github-sync.js.map +1 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.js +665 -0
- package/dist/graph.js.map +1 -0
- package/dist/indexer/aliases.d.ts +2 -0
- package/dist/indexer/aliases.js +190 -0
- package/dist/indexer/aliases.js.map +1 -0
- package/dist/indexer/artifact-writing.d.ts +3 -0
- package/dist/indexer/artifact-writing.js +79 -0
- package/dist/indexer/artifact-writing.js.map +1 -0
- package/dist/indexer/discovery.d.ts +2 -0
- package/dist/indexer/discovery.js +5 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/external-facts.d.ts +6 -0
- package/dist/indexer/external-facts.js +45 -0
- package/dist/indexer/external-facts.js.map +1 -0
- package/dist/indexer/freshness.d.ts +8 -0
- package/dist/indexer/freshness.js +56 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/graph-stage.d.ts +2 -0
- package/dist/indexer/graph-stage.js +21 -0
- package/dist/indexer/graph-stage.js.map +1 -0
- package/dist/indexer/parsing.d.ts +30 -0
- package/dist/indexer/parsing.js +177 -0
- package/dist/indexer/parsing.js.map +1 -0
- package/dist/indexer/pipeline.d.ts +5 -0
- package/dist/indexer/pipeline.js +8 -0
- package/dist/indexer/pipeline.js.map +1 -0
- package/dist/indexer/ranking.d.ts +4 -0
- package/dist/indexer/ranking.js +134 -0
- package/dist/indexer/ranking.js.map +1 -0
- package/dist/indexer.d.ts +13 -0
- package/dist/indexer.js +395 -0
- package/dist/indexer.js.map +1 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +566 -0
- package/dist/init.js.map +1 -0
- package/dist/language.d.ts +8 -0
- package/dist/language.js +123 -0
- package/dist/language.js.map +1 -0
- package/dist/live-index.d.ts +68 -0
- package/dist/live-index.js +215 -0
- package/dist/live-index.js.map +1 -0
- package/dist/lsp/assist.d.ts +44 -0
- package/dist/lsp/assist.js +331 -0
- package/dist/lsp/assist.js.map +1 -0
- package/dist/lsp/client.d.ts +59 -0
- package/dist/lsp/client.js +208 -0
- package/dist/lsp/client.js.map +1 -0
- package/dist/mcp/compaction.d.ts +15 -0
- package/dist/mcp/compaction.js +1249 -0
- package/dist/mcp/compaction.js.map +1 -0
- package/dist/mcp/envelope.d.ts +44 -0
- package/dist/mcp/envelope.js +425 -0
- package/dist/mcp/envelope.js.map +1 -0
- package/dist/mcp/prompts.d.ts +2 -0
- package/dist/mcp/prompts.js +109 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +2 -0
- package/dist/mcp/resources.js +132 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/runtime.d.ts +15 -0
- package/dist/mcp/runtime.js +122 -0
- package/dist/mcp/runtime.js.map +1 -0
- package/dist/mcp/session-memory.d.ts +3 -0
- package/dist/mcp/session-memory.js +61 -0
- package/dist/mcp/session-memory.js.map +1 -0
- package/dist/mcp/tool-registry.d.ts +269 -0
- package/dist/mcp/tool-registry.js +284 -0
- package/dist/mcp/tool-registry.js.map +1 -0
- package/dist/mcp/tools.d.ts +53 -0
- package/dist/mcp/tools.js +372 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp-repo-root.d.ts +16 -0
- package/dist/mcp-repo-root.js +322 -0
- package/dist/mcp-repo-root.js.map +1 -0
- package/dist/mcp-tool-catalog.d.ts +2 -0
- package/dist/mcp-tool-catalog.js +2 -0
- package/dist/mcp-tool-catalog.js.map +1 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +332 -0
- package/dist/mcp.js.map +1 -0
- package/dist/outcome-ranking.d.ts +5 -0
- package/dist/outcome-ranking.js +115 -0
- package/dist/outcome-ranking.js.map +1 -0
- package/dist/parser/context.d.ts +28 -0
- package/dist/parser/context.js +2 -0
- package/dist/parser/context.js.map +1 -0
- package/dist/parser/ecma.d.ts +5 -0
- package/dist/parser/ecma.js +388 -0
- package/dist/parser/ecma.js.map +1 -0
- package/dist/parser/facts.d.ts +12 -0
- package/dist/parser/facts.js +137 -0
- package/dist/parser/facts.js.map +1 -0
- package/dist/parser/json.d.ts +3 -0
- package/dist/parser/json.js +318 -0
- package/dist/parser/json.js.map +1 -0
- package/dist/parser/markdown.d.ts +3 -0
- package/dist/parser/markdown.js +180 -0
- package/dist/parser/markdown.js.map +1 -0
- package/dist/parser/nodes.d.ts +5 -0
- package/dist/parser/nodes.js +75 -0
- package/dist/parser/nodes.js.map +1 -0
- package/dist/parser/python.d.ts +2 -0
- package/dist/parser/python.js +307 -0
- package/dist/parser/python.js.map +1 -0
- package/dist/parser/references.d.ts +3 -0
- package/dist/parser/references.js +204 -0
- package/dist/parser/references.js.map +1 -0
- package/dist/parser/risks.d.ts +4 -0
- package/dist/parser/risks.js +62 -0
- package/dist/parser/risks.js.map +1 -0
- package/dist/parser/routes.d.ts +5 -0
- package/dist/parser/routes.js +97 -0
- package/dist/parser/routes.js.map +1 -0
- package/dist/parser/shallow.d.ts +3 -0
- package/dist/parser/shallow.js +545 -0
- package/dist/parser/shallow.js.map +1 -0
- package/dist/parser/source.d.ts +4 -0
- package/dist/parser/source.js +127 -0
- package/dist/parser/source.js.map +1 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +2 -0
- package/dist/parser.js.map +1 -0
- package/dist/placeholder-signals.d.ts +15 -0
- package/dist/placeholder-signals.js +511 -0
- package/dist/placeholder-signals.js.map +1 -0
- package/dist/post-edit-outcomes.d.ts +167 -0
- package/dist/post-edit-outcomes.js +484 -0
- package/dist/post-edit-outcomes.js.map +1 -0
- package/dist/queries.d.ts +12 -0
- package/dist/queries.js +13 -0
- package/dist/queries.js.map +1 -0
- package/dist/query/change-plan.d.ts +48 -0
- package/dist/query/change-plan.js +858 -0
- package/dist/query/change-plan.js.map +1 -0
- package/dist/query/compact-data.d.ts +25 -0
- package/dist/query/compact-data.js +74 -0
- package/dist/query/compact-data.js.map +1 -0
- package/dist/query/context.d.ts +5 -0
- package/dist/query/context.js +1162 -0
- package/dist/query/context.js.map +1 -0
- package/dist/query/diff.d.ts +5 -0
- package/dist/query/diff.js +111 -0
- package/dist/query/diff.js.map +1 -0
- package/dist/query/edge-evidence.d.ts +3 -0
- package/dist/query/edge-evidence.js +36 -0
- package/dist/query/edge-evidence.js.map +1 -0
- package/dist/query/formatting.d.ts +14 -0
- package/dist/query/formatting.js +67 -0
- package/dist/query/formatting.js.map +1 -0
- package/dist/query/graph-traversal.d.ts +22 -0
- package/dist/query/graph-traversal.js +218 -0
- package/dist/query/graph-traversal.js.map +1 -0
- package/dist/query/graph.d.ts +14 -0
- package/dist/query/graph.js +102 -0
- package/dist/query/graph.js.map +1 -0
- package/dist/query/impact.d.ts +28 -0
- package/dist/query/impact.js +568 -0
- package/dist/query/impact.js.map +1 -0
- package/dist/query/inspection.d.ts +9 -0
- package/dist/query/inspection.js +290 -0
- package/dist/query/inspection.js.map +1 -0
- package/dist/query/next-tools.d.ts +3 -0
- package/dist/query/next-tools.js +25 -0
- package/dist/query/next-tools.js.map +1 -0
- package/dist/query/placeholders.d.ts +24 -0
- package/dist/query/placeholders.js +121 -0
- package/dist/query/placeholders.js.map +1 -0
- package/dist/query/post-edit/decision.d.ts +49 -0
- package/dist/query/post-edit/decision.js +130 -0
- package/dist/query/post-edit/decision.js.map +1 -0
- package/dist/query/post-edit/dirty-scope.d.ts +16 -0
- package/dist/query/post-edit/dirty-scope.js +21 -0
- package/dist/query/post-edit/dirty-scope.js.map +1 -0
- package/dist/query/post-edit/next-actions.d.ts +22 -0
- package/dist/query/post-edit/next-actions.js +44 -0
- package/dist/query/post-edit/next-actions.js.map +1 -0
- package/dist/query/post-edit/snapshot-contract.d.ts +8 -0
- package/dist/query/post-edit/snapshot-contract.js +111 -0
- package/dist/query/post-edit/snapshot-contract.js.map +1 -0
- package/dist/query/post-edit.d.ts +5 -0
- package/dist/query/post-edit.js +1108 -0
- package/dist/query/post-edit.js.map +1 -0
- package/dist/query/quality.d.ts +43 -0
- package/dist/query/quality.js +134 -0
- package/dist/query/quality.js.map +1 -0
- package/dist/query/raw-search.d.ts +23 -0
- package/dist/query/raw-search.js +147 -0
- package/dist/query/raw-search.js.map +1 -0
- package/dist/query/runtime.d.ts +11 -0
- package/dist/query/runtime.js +79 -0
- package/dist/query/runtime.js.map +1 -0
- package/dist/query/search.d.ts +25 -0
- package/dist/query/search.js +429 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/session-memory.d.ts +3 -0
- package/dist/query/session-memory.js +108 -0
- package/dist/query/session-memory.js.map +1 -0
- package/dist/query/session.d.ts +41 -0
- package/dist/query/session.js +90 -0
- package/dist/query/session.js.map +1 -0
- package/dist/query/targets.d.ts +25 -0
- package/dist/query/targets.js +97 -0
- package/dist/query/targets.js.map +1 -0
- package/dist/query/test-commands.d.ts +10 -0
- package/dist/query/test-commands.js +110 -0
- package/dist/query/test-commands.js.map +1 -0
- package/dist/query/test-plan.d.ts +6 -0
- package/dist/query/test-plan.js +104 -0
- package/dist/query/test-plan.js.map +1 -0
- package/dist/query/tests.d.ts +48 -0
- package/dist/query/tests.js +444 -0
- package/dist/query/tests.js.map +1 -0
- package/dist/query/verification/shell.d.ts +20 -0
- package/dist/query/verification/shell.js +164 -0
- package/dist/query/verification/shell.js.map +1 -0
- package/dist/query/verification.d.ts +47 -0
- package/dist/query/verification.js +1123 -0
- package/dist/query/verification.js.map +1 -0
- package/dist/query/workflow.d.ts +17 -0
- package/dist/query/workflow.js +252 -0
- package/dist/query/workflow.js.map +1 -0
- package/dist/query/workspace-guidance.d.ts +26 -0
- package/dist/query/workspace-guidance.js +214 -0
- package/dist/query/workspace-guidance.js.map +1 -0
- package/dist/query/worktree-state.d.ts +22 -0
- package/dist/query/worktree-state.js +32 -0
- package/dist/query/worktree-state.js.map +1 -0
- package/dist/query/worktree.d.ts +16 -0
- package/dist/query/worktree.js +194 -0
- package/dist/query/worktree.js.map +1 -0
- package/dist/query-data.d.ts +4 -0
- package/dist/query-data.js +112 -0
- package/dist/query-data.js.map +1 -0
- package/dist/repo-files.d.ts +24 -0
- package/dist/repo-files.js +105 -0
- package/dist/repo-files.js.map +1 -0
- package/dist/resolver.d.ts +9 -0
- package/dist/resolver.js +555 -0
- package/dist/resolver.js.map +1 -0
- package/dist/retrieval.d.ts +46 -0
- package/dist/retrieval.js +783 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/risk-ingest.d.ts +16 -0
- package/dist/risk-ingest.js +458 -0
- package/dist/risk-ingest.js.map +1 -0
- package/dist/rules.d.ts +10 -0
- package/dist/rules.js +107 -0
- package/dist/rules.js.map +1 -0
- package/dist/semantic/python.d.ts +9 -0
- package/dist/semantic/python.js +817 -0
- package/dist/semantic/python.js.map +1 -0
- package/dist/semantic/typescript.d.ts +10 -0
- package/dist/semantic/typescript.js +714 -0
- package/dist/semantic/typescript.js.map +1 -0
- package/dist/semantic-retrieval.d.ts +53 -0
- package/dist/semantic-retrieval.js +673 -0
- package/dist/semantic-retrieval.js.map +1 -0
- package/dist/session-memory/derivation.d.ts +6 -0
- package/dist/session-memory/derivation.js +400 -0
- package/dist/session-memory/derivation.js.map +1 -0
- package/dist/session-memory/event-log.d.ts +23 -0
- package/dist/session-memory/event-log.js +126 -0
- package/dist/session-memory/event-log.js.map +1 -0
- package/dist/session-memory/formatting.d.ts +7 -0
- package/dist/session-memory/formatting.js +86 -0
- package/dist/session-memory/formatting.js.map +1 -0
- package/dist/session-memory/model.d.ts +94 -0
- package/dist/session-memory/model.js +17 -0
- package/dist/session-memory/model.js.map +1 -0
- package/dist/session-memory/runtime.d.ts +24 -0
- package/dist/session-memory/runtime.js +289 -0
- package/dist/session-memory/runtime.js.map +1 -0
- package/dist/session-memory/store.d.ts +27 -0
- package/dist/session-memory/store.js +447 -0
- package/dist/session-memory/store.js.map +1 -0
- package/dist/session-memory.d.ts +1 -0
- package/dist/session-memory.js +2 -0
- package/dist/session-memory.js.map +1 -0
- package/dist/static-analysis.d.ts +36 -0
- package/dist/static-analysis.js +505 -0
- package/dist/static-analysis.js.map +1 -0
- package/dist/symbol-report-ingest.d.ts +8 -0
- package/dist/symbol-report-ingest.js +504 -0
- package/dist/symbol-report-ingest.js.map +1 -0
- package/dist/task-snapshots.d.ts +41 -0
- package/dist/task-snapshots.js +430 -0
- package/dist/task-snapshots.js.map +1 -0
- package/dist/types.d.ts +848 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +63 -0
- package/dist/util.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +81 -0
- package/plugins/codexa/.codex-plugin/plugin.json +38 -0
- package/plugins/codexa/.mcp.json +20 -0
- package/plugins/codexa/scripts/codexa-mcp.js +100 -0
- package/plugins/codexa/skills/codexa/SKILL.md +48 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatGaps } from "./diff.js";
|
|
3
|
+
import { nextTool } from "./next-tools.js";
|
|
4
|
+
import { contextPackQuery, focusBriefQuery } from "./context.js";
|
|
5
|
+
import { formatContextQuality } from "./quality.js";
|
|
6
|
+
import { freshnessBanner } from "./runtime.js";
|
|
7
|
+
import { ensureQuerySession } from "./session.js";
|
|
8
|
+
import { normalizeSearchText } from "./search.js";
|
|
9
|
+
import { formatTestRecommendations, recommendTests, uniqueTests } from "./tests.js";
|
|
10
|
+
import { findFile, normalizeInputPaths, resolveFileTarget, resolveSymbolTarget } from "./targets.js";
|
|
11
|
+
import { compactSnapshotTests, snapshotRiskBaseline, snapshotSymbolBaseline } from "./post-edit/snapshot-contract.js";
|
|
12
|
+
import { pointerForSessionMemory } from "../session-memory.js";
|
|
13
|
+
import { loadTaskSnapshot, saveBlockedTaskSnapshot, saveTaskSnapshot } from "../task-snapshots.js";
|
|
14
|
+
import { limitText, stableId, uniqueSorted } from "../util.js";
|
|
15
|
+
export async function changePlanQuery(sessionInput, input = {}, options = {}) {
|
|
16
|
+
const session = await ensureQuerySession(sessionInput, options);
|
|
17
|
+
const repoRoot = session.repoRoot;
|
|
18
|
+
const requestedFollowCandidate = normalizeTargetCandidateSelector(input.followCandidate);
|
|
19
|
+
const followBase = requestedFollowCandidate ? await resolveChangePlanFollowBaseInput(repoRoot, input) : undefined;
|
|
20
|
+
if (requestedFollowCandidate && !followBase?.input) {
|
|
21
|
+
return changePlanFollowCandidateRejectedResult({
|
|
22
|
+
session,
|
|
23
|
+
requestedCandidate: requestedFollowCandidate,
|
|
24
|
+
reason: followBase?.reason ?? "followCandidate requires a task, query, or blocked change-plan taskId to replay",
|
|
25
|
+
snapshotLoad: followBase?.snapshotLoad
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const effectiveInput = followBase?.input ?? input;
|
|
29
|
+
const focus = await focusBriefQuery(session, { task: effectiveInput.task, tokenBudget: Math.min(effectiveInput.tokenBudget ?? 2600, 3000), limit: effectiveInput.limit ?? 8, diff: effectiveInput.diff }, options);
|
|
30
|
+
const pack = await contextPackQuery(session, { ...effectiveInput, tokenBudget: Math.min(effectiveInput.tokenBudget ?? 3200, 4000), limit: effectiveInput.limit ?? 10, includeSnippets: effectiveInput.includeSnippets ?? false }, options);
|
|
31
|
+
const packData = pack.data;
|
|
32
|
+
const focusData = focus.data;
|
|
33
|
+
const focusFiles = packData.focusFiles ?? [];
|
|
34
|
+
const tests = packData.tests ?? [];
|
|
35
|
+
const recipes = packData.recipes ?? [];
|
|
36
|
+
const quality = packData.quality ?? focus.data.quality;
|
|
37
|
+
const files = focusFiles.map((entry) => entry.file.path);
|
|
38
|
+
const explicitFiles = normalizeInputPaths(effectiveInput.files ?? [], repoRoot);
|
|
39
|
+
const explicitSymbolFiles = focusFiles
|
|
40
|
+
.filter((entry) => entry.reasons.some((reason) => reason.startsWith("requested symbol ")))
|
|
41
|
+
.map((entry) => entry.file.path);
|
|
42
|
+
const editReadiness = changePlanEditReadiness({
|
|
43
|
+
input: effectiveInput,
|
|
44
|
+
focusFiles,
|
|
45
|
+
explicitTargetProvided: explicitFiles.length > 0 || explicitSymbolFiles.length > 0,
|
|
46
|
+
dirtyScope: packData.dirtyScope,
|
|
47
|
+
quality,
|
|
48
|
+
packetVerdict: packData.packetVerdict,
|
|
49
|
+
intentConfidence: packData.intentConfidence
|
|
50
|
+
});
|
|
51
|
+
const dirtyScopeTargets = editReadiness.source === "dirty-worktree"
|
|
52
|
+
? uniqueSorted(packData.dirtyScope?.plannedEditTargets ?? [])
|
|
53
|
+
: [];
|
|
54
|
+
const plannedEditTargets = editReadiness.editable
|
|
55
|
+
? uniqueSorted(dirtyScopeTargets.length > 0
|
|
56
|
+
? dirtyScopeTargets
|
|
57
|
+
: explicitFiles.length > 0 || explicitSymbolFiles.length > 0
|
|
58
|
+
? [...explicitFiles, ...explicitSymbolFiles]
|
|
59
|
+
: files.slice(0, 6))
|
|
60
|
+
: [];
|
|
61
|
+
const focusPathSet = new Set(files);
|
|
62
|
+
const explicitWorkflowPaths = new Set(normalizeInputPaths(effectiveInput.files ?? [], repoRoot));
|
|
63
|
+
const workflowMatchPaths = explicitWorkflowPaths.size > 0 ? explicitWorkflowPaths : focusPathSet;
|
|
64
|
+
const relatedWorkflow = focusData.workflows?.find((workflow) => workflow.relatedFiles.some((file) => workflowMatchPaths.has(file)));
|
|
65
|
+
const requiredWorkflowChecks = requiredWorkflowChecksForPlan(focusData.workflows ?? [], workflowMatchPaths, effectiveInput.changeType ?? "unknown").slice(0, 8);
|
|
66
|
+
const requiredDependencyChecks = requiredDependencyChecksForPlan(session.index, plannedEditTargets, effectiveInput.changeType ?? "unknown").slice(0, 12);
|
|
67
|
+
const dirtyScopeTests = editReadiness.source === "dirty-worktree"
|
|
68
|
+
? recommendTests(session.index, plannedEditTargets, repoRoot, effectiveInput.changeType ?? "unknown")
|
|
69
|
+
: [];
|
|
70
|
+
const plannedTests = editReadiness.editable ? uniqueTests([...tests, ...dirtyScopeTests]).slice(0, 12) : [];
|
|
71
|
+
const plannedRecipes = editReadiness.editable ? recipes : [];
|
|
72
|
+
const blockedSnapshot = effectiveInput.saveSnapshot && !editReadiness.editable && !requestedFollowCandidate
|
|
73
|
+
? await saveBlockedTaskSnapshot({
|
|
74
|
+
repoRoot,
|
|
75
|
+
input: effectiveInput,
|
|
76
|
+
reason: editReadiness.reason,
|
|
77
|
+
details: editReadiness
|
|
78
|
+
})
|
|
79
|
+
: undefined;
|
|
80
|
+
const targetCandidates = editReadiness.editable
|
|
81
|
+
? []
|
|
82
|
+
: changePlanTargetCandidates({
|
|
83
|
+
input: effectiveInput,
|
|
84
|
+
taskId: blockedSnapshot?.taskId ?? effectiveInput.taskId,
|
|
85
|
+
index: session.index,
|
|
86
|
+
repoRoot,
|
|
87
|
+
focusFiles,
|
|
88
|
+
workflows: focusData.workflows ?? [],
|
|
89
|
+
tests,
|
|
90
|
+
changedEntries: packData.changedEntries ?? [],
|
|
91
|
+
missingAnchors: editReadiness.missingAnchors
|
|
92
|
+
});
|
|
93
|
+
if (requestedFollowCandidate) {
|
|
94
|
+
return changePlanFollowCandidateResult({
|
|
95
|
+
session,
|
|
96
|
+
options,
|
|
97
|
+
originalInput: input,
|
|
98
|
+
baseInput: effectiveInput,
|
|
99
|
+
requestedCandidate: requestedFollowCandidate,
|
|
100
|
+
targetCandidates,
|
|
101
|
+
editReadiness,
|
|
102
|
+
quality,
|
|
103
|
+
snapshotLoad: followBase?.snapshotLoad
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const planSteps = editReadiness.editable
|
|
107
|
+
? [
|
|
108
|
+
editReadiness.source === "dirty-worktree"
|
|
109
|
+
? `1. Treat the current dirty worktree as the planned edit scope (${plannedEditTargets.length} files); read representatives ${files.slice(0, 6).join(", ") || "returned by Codexa"} before editing.`
|
|
110
|
+
: `1. Read ${files.slice(0, 6).join(", ") || "the focus files returned by Codexa"} before editing.`,
|
|
111
|
+
relatedWorkflow
|
|
112
|
+
? `2. Inspect workflow_path for ${relatedWorkflow.title} if the change touches runtime flow.`
|
|
113
|
+
: effectiveInput.files?.length || effectiveInput.symbols?.length
|
|
114
|
+
? "2. Use callers, callees, or dependency_path if this focused edit changes an exported API or runtime contract."
|
|
115
|
+
: editReadiness.source === "dirty-worktree"
|
|
116
|
+
? "2. Use change groups, callers, or dependency_path to split the dirty scope only if the representative reads reveal unrelated work."
|
|
117
|
+
: `2. Use ${focusData.nextCall?.tool ?? "task_brief"} next if the edit target is still ambiguous.`,
|
|
118
|
+
plannedTests.length > 0
|
|
119
|
+
? `3. Keep these tests in scope: ${plannedTests.slice(0, 5).map((test) => test.path).join(", ")}.`
|
|
120
|
+
: "3. No targeted tests were proven; inspect repo test metadata before inventing a command.",
|
|
121
|
+
plannedRecipes.length > 0 ? `4. Verification: ${plannedRecipes.slice(0, 3).join(" ")}` : "4. Run the narrowest verified test or type check that covers the touched files.",
|
|
122
|
+
editReadiness.source === "dirty-worktree"
|
|
123
|
+
? "5. Run post_edit_review after edits; the snapshot dirty baseline separates pre-existing dirty files from new changes."
|
|
124
|
+
: "5. Re-run Codexa task_brief after edits if freshness reports dirty-files-changed."
|
|
125
|
+
]
|
|
126
|
+
: [
|
|
127
|
+
`1. Do not edit yet: ${editReadiness.reason}.`,
|
|
128
|
+
`2. Read ${files.slice(0, 6).join(", ") || "the orientation files returned by Codexa"} only to choose a concrete target.`,
|
|
129
|
+
targetCandidates.length > 0
|
|
130
|
+
? "3. Pick one target candidate below, then re-run change_plan with followCandidate set to its candidateId."
|
|
131
|
+
: `3. Use ${editReadiness.recommendedNextTool ?? focusData.nextCall?.tool ?? "search"} or raw search to identify the exact file or symbol.`,
|
|
132
|
+
"4. Re-run change_plan with an explicit file or symbol target and saveSnapshot=true before editing.",
|
|
133
|
+
"5. Treat any tests below as deferred until the edit target is explicit."
|
|
134
|
+
];
|
|
135
|
+
const structuredNextTools = editReadiness.editable
|
|
136
|
+
? [
|
|
137
|
+
nextTool("post_edit_review", "review drift and verification after completing the planned edit", { taskId: effectiveInput.taskId }, true, [".codex/cache/codexa-outcomes"]),
|
|
138
|
+
plannedTests.length > 0 ? nextTool("test_plan", "inspect planned targeted tests before editing", { files: plannedEditTargets.slice(0, 8) }) : undefined
|
|
139
|
+
].filter((tool) => Boolean(tool))
|
|
140
|
+
: [
|
|
141
|
+
nextTool(editReadiness.recommendedNextTool ?? focusData.nextCall?.tool ?? "search", "narrow the task to an explicit file or symbol target before editing", { task: effectiveInput.task }),
|
|
142
|
+
targetCandidates[0] ? nextTool("change_plan", "follow the highest-confidence target candidate", { taskId: blockedSnapshot?.taskId ?? effectiveInput.taskId, followCandidate: targetCandidates[0].candidateId, saveSnapshot: true }, true, [".codex/cache/codexa-task-snapshots"]) : undefined
|
|
143
|
+
].filter((tool) => Boolean(tool));
|
|
144
|
+
const snapshotIndex = effectiveInput.saveSnapshot && editReadiness.editable ? session.index : undefined;
|
|
145
|
+
const snapshotScope = uniqueSorted([...plannedEditTargets, ...files]);
|
|
146
|
+
const sessionMemoryPointer = effectiveInput.saveSnapshot && editReadiness.editable
|
|
147
|
+
? await pointerForSessionMemory({
|
|
148
|
+
repoRoot,
|
|
149
|
+
taskId: effectiveInput.taskId,
|
|
150
|
+
files: snapshotScope,
|
|
151
|
+
freshness: pack.freshness,
|
|
152
|
+
limit: 8
|
|
153
|
+
}).catch(() => undefined)
|
|
154
|
+
: undefined;
|
|
155
|
+
const savedSnapshot = effectiveInput.saveSnapshot && editReadiness.editable
|
|
156
|
+
? await saveTaskSnapshot({
|
|
157
|
+
repoRoot,
|
|
158
|
+
input: effectiveInput,
|
|
159
|
+
snapshot: {
|
|
160
|
+
task: effectiveInput.task,
|
|
161
|
+
changeType: effectiveInput.changeType ?? "unknown",
|
|
162
|
+
snapshotFreshness: pack.freshness,
|
|
163
|
+
plannedEditTargets,
|
|
164
|
+
plannedFiles: files,
|
|
165
|
+
focusFiles: focusFiles.map((entry) => ({
|
|
166
|
+
path: entry.file.path,
|
|
167
|
+
tier: entry.tier,
|
|
168
|
+
reasons: uniqueSorted(entry.reasons),
|
|
169
|
+
rank: entry.file.rank,
|
|
170
|
+
riskScore: entry.file.riskScore
|
|
171
|
+
})),
|
|
172
|
+
plannedTests: compactSnapshotTests(plannedTests, repoRoot),
|
|
173
|
+
sessionMemory: sessionMemoryPointer,
|
|
174
|
+
requiredWorkflowChecks,
|
|
175
|
+
requiredDependencyChecks,
|
|
176
|
+
symbolBaseline: snapshotIndex ? snapshotSymbolBaseline(snapshotIndex, snapshotScope) : undefined,
|
|
177
|
+
riskBaseline: snapshotIndex ? snapshotRiskBaseline(snapshotIndex, snapshotScope) : undefined,
|
|
178
|
+
recipes: plannedRecipes,
|
|
179
|
+
dirtyBaseline: {
|
|
180
|
+
changedEntries: packData.changedEntries ?? [],
|
|
181
|
+
dirtyFiles: pack.freshness.dirtyFiles,
|
|
182
|
+
dirtyFileHashes: pack.freshness.dirtyFileHashes,
|
|
183
|
+
headCommit: pack.freshness.headCommit,
|
|
184
|
+
indexedAt: pack.freshness.indexedAt
|
|
185
|
+
},
|
|
186
|
+
quality,
|
|
187
|
+
gaps: packData.gaps ?? [],
|
|
188
|
+
warnings: packData.warnings ?? []
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
: undefined;
|
|
192
|
+
const text = [
|
|
193
|
+
freshnessBanner(pack.freshness, pack.refresh),
|
|
194
|
+
quality ? formatContextQuality(quality) : undefined,
|
|
195
|
+
"Codexa change plan",
|
|
196
|
+
effectiveInput.task ? `Task: ${effectiveInput.task}` : undefined,
|
|
197
|
+
`Edit readiness: ${editReadiness.status}; ${editReadiness.reason}`,
|
|
198
|
+
savedSnapshot ? `Task snapshot: ${savedSnapshot.snapshot.taskId}` : undefined,
|
|
199
|
+
effectiveInput.saveSnapshot && !editReadiness.editable ? "Task snapshot: not saved because this packet is orientation-only." : undefined,
|
|
200
|
+
"",
|
|
201
|
+
...planSteps,
|
|
202
|
+
"",
|
|
203
|
+
"Read first:",
|
|
204
|
+
...focusFiles.slice(0, 10).map((entry) => `- ${entry.file.path}: ${entry.tier}; ${entry.reasons.join("; ")}`),
|
|
205
|
+
"",
|
|
206
|
+
"Tests:",
|
|
207
|
+
...(editReadiness.editable ? formatTestRecommendations(plannedTests.slice(0, 12)) : ["- deferred until Codexa has an explicit file, symbol, or edit-ready packet."]),
|
|
208
|
+
!editReadiness.editable ? "" : undefined,
|
|
209
|
+
!editReadiness.editable ? "Target candidates:" : undefined,
|
|
210
|
+
...(!editReadiness.editable ? formatTargetCandidates(targetCandidates) : []),
|
|
211
|
+
"",
|
|
212
|
+
"Required workflow checks:",
|
|
213
|
+
...formatRequiredChecks(editReadiness.editable ? requiredWorkflowChecks : []),
|
|
214
|
+
"",
|
|
215
|
+
"Required dependency checks:",
|
|
216
|
+
...formatRequiredChecks(editReadiness.editable ? requiredDependencyChecks : []),
|
|
217
|
+
"",
|
|
218
|
+
"Known gaps:",
|
|
219
|
+
...formatGaps(packData.gaps ?? [])
|
|
220
|
+
]
|
|
221
|
+
.filter((line) => line !== undefined)
|
|
222
|
+
.join("\n");
|
|
223
|
+
return {
|
|
224
|
+
freshness: pack.freshness,
|
|
225
|
+
refresh: pack.refresh,
|
|
226
|
+
text: limitText(text, 7000),
|
|
227
|
+
data: {
|
|
228
|
+
mode: "change_plan",
|
|
229
|
+
editReadiness,
|
|
230
|
+
steps: planSteps,
|
|
231
|
+
focus: focus.data,
|
|
232
|
+
context: pack.data,
|
|
233
|
+
files,
|
|
234
|
+
plannedEditTargets,
|
|
235
|
+
tests: plannedTests,
|
|
236
|
+
recipes: plannedRecipes,
|
|
237
|
+
targetCandidates,
|
|
238
|
+
quality,
|
|
239
|
+
requiredWorkflowChecks: editReadiness.editable ? requiredWorkflowChecks : [],
|
|
240
|
+
requiredDependencyChecks: editReadiness.editable ? requiredDependencyChecks : [],
|
|
241
|
+
nextTools: structuredNextTools,
|
|
242
|
+
systemMessage: structuredNextTools[0]?.reason,
|
|
243
|
+
snapshot: savedSnapshot?.snapshot,
|
|
244
|
+
snapshotBlock: blockedSnapshot
|
|
245
|
+
? {
|
|
246
|
+
taskId: blockedSnapshot.taskId,
|
|
247
|
+
path: path.relative(repoRoot, blockedSnapshot.path).split(path.sep).join("/"),
|
|
248
|
+
reason: editReadiness.reason
|
|
249
|
+
}
|
|
250
|
+
: undefined
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function changePlanEditReadiness(input) {
|
|
255
|
+
const packetVerdict = input.packetVerdict ?? input.intentConfidence?.verdict;
|
|
256
|
+
const qualityLevel = input.quality?.level;
|
|
257
|
+
const hasEvidenceBackedFocus = input.focusFiles.some((entry) => entry.tier === "authoritative" || entry.tier === "derived");
|
|
258
|
+
const highConfidenceContext = qualityLevel === "high" && hasEvidenceBackedFocus && (packetVerdict === undefined || packetVerdict === "edit-ready");
|
|
259
|
+
const dirtyWorktreeContext = !input.explicitTargetProvided &&
|
|
260
|
+
input.dirtyScope?.requested === true &&
|
|
261
|
+
input.dirtyScope.mode === "edit" &&
|
|
262
|
+
input.dirtyScope.canPlan === true &&
|
|
263
|
+
(input.dirtyScope.plannedEditTargets?.length ?? 0) > 0 &&
|
|
264
|
+
hasEvidenceBackedFocus &&
|
|
265
|
+
packetVerdict !== "raw-search-better";
|
|
266
|
+
const editable = input.explicitTargetProvided || highConfidenceContext || dirtyWorktreeContext;
|
|
267
|
+
const missingAnchors = uniqueSorted([
|
|
268
|
+
...(input.intentConfidence?.missingAnchors ?? []),
|
|
269
|
+
...(input.explicitTargetProvided || dirtyWorktreeContext ? [] : ["file-or-symbol-target"]),
|
|
270
|
+
...(highConfidenceContext || input.explicitTargetProvided || dirtyWorktreeContext ? [] : ["edit-ready-context"]),
|
|
271
|
+
...(input.dirtyScope?.requested && !input.dirtyScope.canPlan ? ["known-dirty-worktree-scope"] : [])
|
|
272
|
+
]);
|
|
273
|
+
const reason = input.explicitTargetProvided
|
|
274
|
+
? "explicit file or symbol target provided"
|
|
275
|
+
: dirtyWorktreeContext
|
|
276
|
+
? `current dirty worktree explicitly requested as edit scope (${input.dirtyScope?.plannedEditTargets?.length ?? 0} file(s))`
|
|
277
|
+
: highConfidenceContext
|
|
278
|
+
? "high-confidence evidence-backed packet"
|
|
279
|
+
: packetVerdict === "raw-search-better"
|
|
280
|
+
? "raw search is likely a cleaner first pass than this broad packet"
|
|
281
|
+
: packetVerdict === "needs-target"
|
|
282
|
+
? "broad change plan needs an explicit file or symbol target"
|
|
283
|
+
: qualityLevel === "low"
|
|
284
|
+
? "context quality is low"
|
|
285
|
+
: "packet is not edit-ready without an explicit file or symbol target";
|
|
286
|
+
return {
|
|
287
|
+
editable,
|
|
288
|
+
status: editable ? "edit-ready" : "orientation-only",
|
|
289
|
+
reason,
|
|
290
|
+
source: input.explicitTargetProvided ? "explicit-target" : dirtyWorktreeContext ? "dirty-worktree" : highConfidenceContext ? "high-confidence-context" : "insufficient-context",
|
|
291
|
+
explicitTargetProvided: input.explicitTargetProvided,
|
|
292
|
+
packetVerdict,
|
|
293
|
+
qualityLevel,
|
|
294
|
+
confidence: input.intentConfidence?.confidence,
|
|
295
|
+
recommendedNextTool: editable ? undefined : input.intentConfidence?.recommendedNextTool ?? (packetVerdict === "raw-search-better" || packetVerdict === "needs-target" ? "search" : "task_brief"),
|
|
296
|
+
missingAnchors,
|
|
297
|
+
snapshotBlocked: Boolean(input.input.saveSnapshot && !editable)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function resolveChangePlanFollowBaseInput(repoRoot, input) {
|
|
301
|
+
const directInput = withoutFollowCandidate(input);
|
|
302
|
+
if (!input.taskId && hasChangePlanReplaySeed(directInput)) {
|
|
303
|
+
return { input: directInput };
|
|
304
|
+
}
|
|
305
|
+
const snapshotLoad = await loadTaskSnapshot(repoRoot, input.taskId);
|
|
306
|
+
if (snapshotLoad.missingReason === "blocked-plan" && snapshotLoad.blockedSnapshot?.input) {
|
|
307
|
+
return {
|
|
308
|
+
input: {
|
|
309
|
+
...withoutFollowCandidate(snapshotLoad.blockedSnapshot.input),
|
|
310
|
+
taskId: snapshotLoad.blockedSnapshot.taskId,
|
|
311
|
+
saveSnapshot: true
|
|
312
|
+
},
|
|
313
|
+
snapshotLoad
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (hasChangePlanReplaySeed(directInput)) {
|
|
317
|
+
return { input: directInput, snapshotLoad };
|
|
318
|
+
}
|
|
319
|
+
if (snapshotLoad.missingReason === "blocked-plan") {
|
|
320
|
+
return { snapshotLoad, reason: "blocked change-plan marker does not include replayable input" };
|
|
321
|
+
}
|
|
322
|
+
if (snapshotLoad.snapshot) {
|
|
323
|
+
return { snapshotLoad, reason: "requested task already has an edit-ready snapshot; followCandidate only applies to blocked orientation plans" };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
snapshotLoad,
|
|
327
|
+
reason: snapshotLoad.missingReason ? `no blocked change-plan input available (${snapshotLoad.missingReason})` : "no blocked change-plan input available"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function withoutFollowCandidate(input) {
|
|
331
|
+
const rest = { ...input };
|
|
332
|
+
delete rest.followCandidate;
|
|
333
|
+
return rest;
|
|
334
|
+
}
|
|
335
|
+
function hasChangePlanReplaySeed(input) {
|
|
336
|
+
return Boolean(input.task?.trim() || input.query?.trim() || input.files?.length || input.symbols?.length);
|
|
337
|
+
}
|
|
338
|
+
function normalizeTargetCandidateSelector(value) {
|
|
339
|
+
const trimmed = value?.trim();
|
|
340
|
+
return trimmed || undefined;
|
|
341
|
+
}
|
|
342
|
+
async function changePlanFollowCandidateResult(input) {
|
|
343
|
+
const selected = input.targetCandidates.find((candidate) => candidate.candidateId === input.requestedCandidate);
|
|
344
|
+
if (!selected) {
|
|
345
|
+
return changePlanFollowCandidateRejectedResult({
|
|
346
|
+
session: input.session,
|
|
347
|
+
requestedCandidate: input.requestedCandidate,
|
|
348
|
+
reason: "target candidate id was not found when replayed against the current index",
|
|
349
|
+
targetCandidates: input.targetCandidates,
|
|
350
|
+
editReadiness: input.editReadiness,
|
|
351
|
+
quality: input.quality,
|
|
352
|
+
snapshotLoad: input.snapshotLoad
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const revalidation = validateChangePlanTargetCandidate(selected, { index: input.session.index, repoRoot: input.session.repoRoot });
|
|
356
|
+
const revalidatedCandidate = { ...selected, ...revalidation };
|
|
357
|
+
if (revalidation.validationStatus !== "edit-ready") {
|
|
358
|
+
return changePlanFollowCandidateRejectedResult({
|
|
359
|
+
session: input.session,
|
|
360
|
+
requestedCandidate: input.requestedCandidate,
|
|
361
|
+
reason: `target candidate revalidated as ${revalidation.validationStatus}: ${revalidation.validationReasons.join("; ")}`,
|
|
362
|
+
targetCandidates: [revalidatedCandidate, ...input.targetCandidates.filter((candidate) => candidate.candidateId !== selected.candidateId)],
|
|
363
|
+
editReadiness: input.editReadiness,
|
|
364
|
+
quality: input.quality,
|
|
365
|
+
snapshotLoad: input.snapshotLoad
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const allowRequestOverrides = !input.snapshotLoad?.blockedSnapshot;
|
|
369
|
+
const followedInput = {
|
|
370
|
+
...selected.nextChangePlanArgs,
|
|
371
|
+
taskId: input.originalInput.taskId ?? selected.nextChangePlanArgs.taskId ?? input.baseInput.taskId,
|
|
372
|
+
changeType: allowRequestOverrides ? input.originalInput.changeType ?? selected.nextChangePlanArgs.changeType : selected.nextChangePlanArgs.changeType,
|
|
373
|
+
diff: allowRequestOverrides ? input.originalInput.diff ?? selected.nextChangePlanArgs.diff : selected.nextChangePlanArgs.diff,
|
|
374
|
+
saveSnapshot: true
|
|
375
|
+
};
|
|
376
|
+
const result = await changePlanQuery(input.session, followedInput, { ...input.options, autoRefresh: false });
|
|
377
|
+
const resultData = result.data && typeof result.data === "object" ? result.data : {};
|
|
378
|
+
return {
|
|
379
|
+
...result,
|
|
380
|
+
text: limitText(`Follow candidate: accepted ${selected.candidateId}; revalidated edit-ready.\n\n${result.text}`, 7000),
|
|
381
|
+
data: {
|
|
382
|
+
...resultData,
|
|
383
|
+
followCandidate: {
|
|
384
|
+
status: "accepted",
|
|
385
|
+
requested: input.requestedCandidate,
|
|
386
|
+
candidateId: selected.candidateId,
|
|
387
|
+
rank: selected.rank,
|
|
388
|
+
kind: selected.kind,
|
|
389
|
+
path: selected.path,
|
|
390
|
+
plannedEditTargets: revalidation.wouldPlanEditTargets,
|
|
391
|
+
validationReasons: revalidation.validationReasons
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function changePlanFollowCandidateRejectedResult(input) {
|
|
397
|
+
const editReadiness = input.editReadiness ??
|
|
398
|
+
{
|
|
399
|
+
editable: false,
|
|
400
|
+
status: "orientation-only",
|
|
401
|
+
reason: input.reason,
|
|
402
|
+
source: "insufficient-context",
|
|
403
|
+
explicitTargetProvided: false,
|
|
404
|
+
recommendedNextTool: "change_plan",
|
|
405
|
+
missingAnchors: ["valid-target-candidate"],
|
|
406
|
+
snapshotBlocked: false
|
|
407
|
+
};
|
|
408
|
+
const steps = [
|
|
409
|
+
`1. Do not edit yet: ${input.reason}.`,
|
|
410
|
+
"2. Re-run the orientation change_plan if the target candidates are stale.",
|
|
411
|
+
"3. Use an edit-ready candidateId from the current Target candidates list, then retry followCandidate.",
|
|
412
|
+
"4. If no candidate is edit-ready, use search/task_brief to identify an explicit file or symbol target."
|
|
413
|
+
];
|
|
414
|
+
const text = [
|
|
415
|
+
freshnessBanner(input.session.freshness, input.session.refresh),
|
|
416
|
+
input.quality ? formatContextQuality(input.quality) : undefined,
|
|
417
|
+
"Codexa change plan",
|
|
418
|
+
`Follow candidate: rejected; ${input.reason}`,
|
|
419
|
+
"",
|
|
420
|
+
...steps,
|
|
421
|
+
"",
|
|
422
|
+
input.targetCandidates?.length ? "Target candidates:" : undefined,
|
|
423
|
+
...(input.targetCandidates?.length ? formatTargetCandidates(input.targetCandidates) : [])
|
|
424
|
+
]
|
|
425
|
+
.filter((line) => line !== undefined)
|
|
426
|
+
.join("\n");
|
|
427
|
+
return {
|
|
428
|
+
freshness: input.session.freshness,
|
|
429
|
+
refresh: input.session.refresh,
|
|
430
|
+
text: limitText(text, 7000),
|
|
431
|
+
data: {
|
|
432
|
+
mode: "change_plan",
|
|
433
|
+
editReadiness,
|
|
434
|
+
steps,
|
|
435
|
+
files: [],
|
|
436
|
+
plannedEditTargets: [],
|
|
437
|
+
tests: [],
|
|
438
|
+
recipes: [],
|
|
439
|
+
targetCandidates: input.targetCandidates ?? [],
|
|
440
|
+
quality: input.quality,
|
|
441
|
+
requiredWorkflowChecks: [],
|
|
442
|
+
requiredDependencyChecks: [],
|
|
443
|
+
followCandidate: {
|
|
444
|
+
status: "rejected",
|
|
445
|
+
requested: input.requestedCandidate,
|
|
446
|
+
reason: input.reason,
|
|
447
|
+
snapshotLoad: input.snapshotLoad
|
|
448
|
+
? {
|
|
449
|
+
latestTaskId: input.snapshotLoad.latestTaskId,
|
|
450
|
+
missingReason: input.snapshotLoad.missingReason,
|
|
451
|
+
error: input.snapshotLoad.error
|
|
452
|
+
}
|
|
453
|
+
: undefined
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function changePlanTargetCandidates(input) {
|
|
459
|
+
const taskTokens = meaningfulTaskTokens(input.input.task ?? input.input.query ?? "");
|
|
460
|
+
const changedPaths = new Set(input.changedEntries.map((entry) => entry.path));
|
|
461
|
+
const testPaths = new Set(input.tests.map((test) => test.path));
|
|
462
|
+
const symbolsByPath = new Map();
|
|
463
|
+
for (const symbol of input.index.symbols) {
|
|
464
|
+
if (["module", "unknown"].includes(symbol.kind)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const entries = symbolsByPath.get(symbol.path) ?? [];
|
|
468
|
+
entries.push(symbol);
|
|
469
|
+
symbolsByPath.set(symbol.path, entries);
|
|
470
|
+
}
|
|
471
|
+
const candidates = [];
|
|
472
|
+
for (const entry of input.focusFiles.slice(0, 10)) {
|
|
473
|
+
const file = entry.file;
|
|
474
|
+
if (file.test && input.focusFiles.some((candidate) => !candidate.file.test)) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const workflowHits = input.workflows.filter((workflow) => workflow.entryPath === file.path || workflow.relatedFiles.includes(file.path));
|
|
478
|
+
const graphHits = input.index.graphEdges.filter((edge) => edge.fromPath === file.path || edge.toPath === file.path).slice(0, 6);
|
|
479
|
+
const fileEvidence = candidateEvidence({
|
|
480
|
+
file,
|
|
481
|
+
reasons: entry.reasons,
|
|
482
|
+
workflowHits,
|
|
483
|
+
graphHits,
|
|
484
|
+
testPaths,
|
|
485
|
+
changedPaths,
|
|
486
|
+
taskTokens,
|
|
487
|
+
symbol: undefined
|
|
488
|
+
});
|
|
489
|
+
candidates.push({
|
|
490
|
+
rank: 0,
|
|
491
|
+
kind: "file",
|
|
492
|
+
path: file.path,
|
|
493
|
+
score: candidateScore(file, entry.tier, fileEvidence, undefined),
|
|
494
|
+
confidence: entry.tier,
|
|
495
|
+
evidence: fileEvidence.slice(0, 8),
|
|
496
|
+
missingAnchors: input.missingAnchors,
|
|
497
|
+
nextChangePlanArgs: {
|
|
498
|
+
task: input.input.task,
|
|
499
|
+
files: [file.path],
|
|
500
|
+
query: input.input.query,
|
|
501
|
+
taskId: input.taskId,
|
|
502
|
+
changeType: input.input.changeType ?? "unknown",
|
|
503
|
+
diff: input.input.diff,
|
|
504
|
+
saveSnapshot: true
|
|
505
|
+
},
|
|
506
|
+
rawSearchQueries: rawSearchQueries(input.input.task ?? input.input.query, file.path)
|
|
507
|
+
});
|
|
508
|
+
for (const symbol of candidateSymbols(symbolsByPath.get(file.path) ?? [], taskTokens).slice(0, 2)) {
|
|
509
|
+
const symbolEvidence = candidateEvidence({
|
|
510
|
+
file,
|
|
511
|
+
reasons: entry.reasons,
|
|
512
|
+
workflowHits,
|
|
513
|
+
graphHits: graphHits.filter((edge) => edge.fromSymbolId === symbol.id || edge.toSymbolId === symbol.id || edge.fromPath === symbol.path || edge.toPath === symbol.path),
|
|
514
|
+
testPaths,
|
|
515
|
+
changedPaths,
|
|
516
|
+
taskTokens,
|
|
517
|
+
symbol
|
|
518
|
+
});
|
|
519
|
+
candidates.push({
|
|
520
|
+
rank: 0,
|
|
521
|
+
kind: "symbol",
|
|
522
|
+
path: file.path,
|
|
523
|
+
symbol: {
|
|
524
|
+
id: symbol.id,
|
|
525
|
+
name: symbol.name,
|
|
526
|
+
qualifiedName: symbol.qualifiedName,
|
|
527
|
+
kind: symbol.kind
|
|
528
|
+
},
|
|
529
|
+
score: candidateScore(file, entry.tier, symbolEvidence, symbol),
|
|
530
|
+
confidence: entry.tier,
|
|
531
|
+
evidence: symbolEvidence.slice(0, 8),
|
|
532
|
+
missingAnchors: input.missingAnchors,
|
|
533
|
+
nextChangePlanArgs: {
|
|
534
|
+
task: input.input.task,
|
|
535
|
+
symbols: [symbol.id],
|
|
536
|
+
query: input.input.query,
|
|
537
|
+
taskId: input.taskId,
|
|
538
|
+
changeType: input.input.changeType ?? "unknown",
|
|
539
|
+
diff: input.input.diff,
|
|
540
|
+
saveSnapshot: true
|
|
541
|
+
},
|
|
542
|
+
rawSearchQueries: rawSearchQueries(input.input.task ?? input.input.query, symbol.qualifiedName)
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return dedupeTargetCandidates(candidates)
|
|
547
|
+
.map(withTargetCandidateId)
|
|
548
|
+
.map((candidate) => ({
|
|
549
|
+
...candidate,
|
|
550
|
+
...validateChangePlanTargetCandidate(candidate, { index: input.index, repoRoot: input.repoRoot })
|
|
551
|
+
}))
|
|
552
|
+
.sort(compareTargetCandidates)
|
|
553
|
+
.slice(0, 8)
|
|
554
|
+
.map((candidate, index) => ({ ...candidate, rank: index + 1 }));
|
|
555
|
+
}
|
|
556
|
+
export function validateChangePlanTargetCandidate(candidate, context) {
|
|
557
|
+
const validationReasons = [];
|
|
558
|
+
const wouldPlanEditTargets = new Set();
|
|
559
|
+
let unresolvedTarget = false;
|
|
560
|
+
let ambiguousTarget = false;
|
|
561
|
+
const requestedFiles = candidate.nextChangePlanArgs.files ?? [];
|
|
562
|
+
const requestedSymbols = candidate.nextChangePlanArgs.symbols ?? [];
|
|
563
|
+
if (requestedFiles.length === 0 && requestedSymbols.length === 0) {
|
|
564
|
+
validationReasons.push("no explicit file or symbol target in nextChangePlanArgs");
|
|
565
|
+
unresolvedTarget = true;
|
|
566
|
+
}
|
|
567
|
+
for (const requestedFile of requestedFiles) {
|
|
568
|
+
const resolved = resolveFileTarget(context.index, requestedFile, context.repoRoot);
|
|
569
|
+
if (resolved.file) {
|
|
570
|
+
wouldPlanEditTargets.add(resolved.file.path);
|
|
571
|
+
validationReasons.push(`file target resolves: ${resolved.file.path}`);
|
|
572
|
+
}
|
|
573
|
+
else if (resolved.ambiguous.length > 0) {
|
|
574
|
+
ambiguousTarget = true;
|
|
575
|
+
validationReasons.push(`file target is ambiguous: ${requestedFile}`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
unresolvedTarget = true;
|
|
579
|
+
validationReasons.push(`file target not indexed: ${requestedFile}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const requestedSymbol of requestedSymbols) {
|
|
583
|
+
const resolved = resolveSymbolTarget(context.index, requestedSymbol);
|
|
584
|
+
if (resolved.symbol) {
|
|
585
|
+
wouldPlanEditTargets.add(resolved.symbol.path);
|
|
586
|
+
validationReasons.push(`symbol target resolves: ${resolved.symbol.qualifiedName} in ${resolved.symbol.path}`);
|
|
587
|
+
}
|
|
588
|
+
else if (resolved.ambiguous.length > 0) {
|
|
589
|
+
ambiguousTarget = true;
|
|
590
|
+
validationReasons.push(`symbol target is ambiguous: ${requestedSymbol}`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
unresolvedTarget = true;
|
|
594
|
+
validationReasons.push(`symbol target not indexed: ${requestedSymbol}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const plannedTargets = uniqueSorted(wouldPlanEditTargets);
|
|
598
|
+
if (candidate.confidence === "fallback") {
|
|
599
|
+
validationReasons.push("candidate evidence is fallback");
|
|
600
|
+
}
|
|
601
|
+
else if (candidate.evidence.length > 0) {
|
|
602
|
+
validationReasons.push(`candidate has ${candidate.confidence} evidence`);
|
|
603
|
+
}
|
|
604
|
+
if (candidate.evidence.length === 0) {
|
|
605
|
+
validationReasons.push("candidate has no supporting evidence");
|
|
606
|
+
}
|
|
607
|
+
const wouldRecommendTests = plannedTargets.length > 0
|
|
608
|
+
? recommendTests(context.index, plannedTargets, context.repoRoot, candidate.nextChangePlanArgs.changeType).map((test) => test.path).slice(0, 8)
|
|
609
|
+
: [];
|
|
610
|
+
if (wouldRecommendTests.length > 0) {
|
|
611
|
+
validationReasons.push(`would recommend ${wouldRecommendTests.length} targeted test(s)`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
validationReasons.push("no targeted test recommendation proven");
|
|
615
|
+
}
|
|
616
|
+
const candidateRisk = candidateRiskForTargets(context.index, plannedTargets);
|
|
617
|
+
if (candidateRisk.score > 0) {
|
|
618
|
+
validationReasons.push(`candidate risk score ${candidateRisk.score.toFixed(1)}`);
|
|
619
|
+
}
|
|
620
|
+
const hasStrongEvidence = (candidate.confidence === "authoritative" || candidate.confidence === "derived") && candidate.evidence.length > 0;
|
|
621
|
+
const validationStatus = plannedTargets.length === 0 || unresolvedTarget || ambiguousTarget
|
|
622
|
+
? "needs-more-context"
|
|
623
|
+
: hasStrongEvidence
|
|
624
|
+
? "edit-ready"
|
|
625
|
+
: "weak";
|
|
626
|
+
return {
|
|
627
|
+
validationStatus,
|
|
628
|
+
validationReasons: uniqueInOrder(validationReasons).slice(0, 8),
|
|
629
|
+
wouldPlanEditTargets: plannedTargets,
|
|
630
|
+
wouldRecommendTests,
|
|
631
|
+
candidateRisk
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function candidateRiskForTargets(index, paths) {
|
|
635
|
+
const pathSet = new Set(paths);
|
|
636
|
+
const fileReasons = paths
|
|
637
|
+
.map((filePath) => findFile(index, filePath))
|
|
638
|
+
.filter((file) => Boolean(file))
|
|
639
|
+
.filter((file) => file.riskScore > 0)
|
|
640
|
+
.map((file) => ({ score: file.riskScore, reason: `${file.path}: indexed risk ${file.riskScore.toFixed(1)}` }));
|
|
641
|
+
const signalReasons = index.risks
|
|
642
|
+
.filter((risk) => pathSet.has(risk.path))
|
|
643
|
+
.map((risk) => ({ score: risk.score, reason: `${risk.path}: ${risk.signal} - ${risk.reason}` }));
|
|
644
|
+
const scoredReasons = [...fileReasons, ...signalReasons].sort((left, right) => right.score - left.score || left.reason.localeCompare(right.reason));
|
|
645
|
+
return {
|
|
646
|
+
score: Math.max(0, ...scoredReasons.map((entry) => entry.score)),
|
|
647
|
+
reasons: uniqueInOrder(scoredReasons.map((entry) => entry.reason)).slice(0, 6)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function compareTargetCandidates(left, right) {
|
|
651
|
+
return (targetCandidateStatusRank(left.validationStatus) - targetCandidateStatusRank(right.validationStatus) ||
|
|
652
|
+
right.score - left.score ||
|
|
653
|
+
left.path.localeCompare(right.path) ||
|
|
654
|
+
left.kind.localeCompare(right.kind) ||
|
|
655
|
+
left.candidateId.localeCompare(right.candidateId));
|
|
656
|
+
}
|
|
657
|
+
function targetCandidateStatusRank(status) {
|
|
658
|
+
return status === "edit-ready" ? 0 : status === "weak" ? 1 : 2;
|
|
659
|
+
}
|
|
660
|
+
function candidateEvidence(input) {
|
|
661
|
+
const evidence = new Set();
|
|
662
|
+
for (const reason of input.reasons.slice(0, 4)) {
|
|
663
|
+
evidence.add(reason);
|
|
664
|
+
}
|
|
665
|
+
if (input.symbol) {
|
|
666
|
+
evidence.add(`symbol ${input.symbol.qualifiedName} (${input.symbol.kind})`);
|
|
667
|
+
const normalizedSymbol = normalizeSearchText(`${input.symbol.name} ${input.symbol.qualifiedName}`);
|
|
668
|
+
if (input.taskTokens.some((token) => normalizedSymbol.includes(token))) {
|
|
669
|
+
evidence.add("keyword match on symbol name");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const normalizedPath = normalizeSearchText(input.file.path);
|
|
673
|
+
if (input.taskTokens.some((token) => normalizedPath.includes(token))) {
|
|
674
|
+
evidence.add("keyword match on file path");
|
|
675
|
+
}
|
|
676
|
+
if (input.workflowHits.length > 0) {
|
|
677
|
+
evidence.add(`workflow evidence: ${input.workflowHits.slice(0, 2).map((workflow) => workflow.title).join(", ")}`);
|
|
678
|
+
}
|
|
679
|
+
if (input.graphHits.length > 0) {
|
|
680
|
+
evidence.add(`graph evidence: ${uniqueSorted(input.graphHits.map((edge) => edge.edgeKind)).slice(0, 4).join(", ")}`);
|
|
681
|
+
}
|
|
682
|
+
if (input.testPaths.has(input.file.path) || input.file.test) {
|
|
683
|
+
evidence.add("test evidence: candidate is a known test path");
|
|
684
|
+
}
|
|
685
|
+
else if (input.graphHits.some((edge) => edge.edgeKind === "TESTS" || edge.edgeKind === "TEST_COVERS_WORKFLOW")) {
|
|
686
|
+
evidence.add("test evidence: graph links tests to this target");
|
|
687
|
+
}
|
|
688
|
+
if (input.changedPaths.has(input.file.path)) {
|
|
689
|
+
evidence.add("recent diff evidence: file is currently changed");
|
|
690
|
+
}
|
|
691
|
+
if (input.file.riskScore > 0) {
|
|
692
|
+
evidence.add(`risk evidence: score ${input.file.riskScore.toFixed(1)}`);
|
|
693
|
+
}
|
|
694
|
+
return [...evidence];
|
|
695
|
+
}
|
|
696
|
+
function candidateScore(file, tier, evidence, symbol) {
|
|
697
|
+
const tierScore = {
|
|
698
|
+
authoritative: 100,
|
|
699
|
+
derived: 70,
|
|
700
|
+
heuristic: 35,
|
|
701
|
+
fallback: 10
|
|
702
|
+
};
|
|
703
|
+
const symbolScore = symbol ? (symbol.exported || ["route", "node"].includes(symbol.kind) ? 18 : 10) : 0;
|
|
704
|
+
const sourceScore = file.test ? -12 : 12;
|
|
705
|
+
return tierScore[tier] + file.rank * 2 + file.riskScore + evidence.length * 4 + symbolScore + sourceScore;
|
|
706
|
+
}
|
|
707
|
+
function candidateSymbols(symbols, taskTokens) {
|
|
708
|
+
return symbols
|
|
709
|
+
.slice()
|
|
710
|
+
.sort((left, right) => symbolTargetScore(right, taskTokens) - symbolTargetScore(left, taskTokens) ||
|
|
711
|
+
(left.range?.startLine ?? 0) - (right.range?.startLine ?? 0) ||
|
|
712
|
+
left.qualifiedName.localeCompare(right.qualifiedName));
|
|
713
|
+
}
|
|
714
|
+
function symbolTargetScore(symbol, taskTokens) {
|
|
715
|
+
const normalized = normalizeSearchText(`${symbol.name} ${symbol.qualifiedName}`);
|
|
716
|
+
const tokenScore = taskTokens.filter((token) => normalized.includes(token)).length * 20;
|
|
717
|
+
const kindScore = symbol.kind === "route" ? 18 : symbol.exported ? 14 : ["function", "method", "class"].includes(symbol.kind) ? 10 : 4;
|
|
718
|
+
return tokenScore + kindScore;
|
|
719
|
+
}
|
|
720
|
+
function dedupeTargetCandidates(candidates) {
|
|
721
|
+
const seen = new Set();
|
|
722
|
+
const result = [];
|
|
723
|
+
for (const candidate of candidates) {
|
|
724
|
+
const key = targetCandidateStableTarget(candidate);
|
|
725
|
+
if (seen.has(key)) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
seen.add(key);
|
|
729
|
+
result.push(candidate);
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
function withTargetCandidateId(candidate) {
|
|
734
|
+
return {
|
|
735
|
+
...candidate,
|
|
736
|
+
candidateId: targetCandidateStableId(candidate)
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function targetCandidateStableId(candidate) {
|
|
740
|
+
return `candidate-${stableId("change-plan-target-candidate", targetCandidateStableTarget(candidate)).slice(0, 12)}`;
|
|
741
|
+
}
|
|
742
|
+
function targetCandidateStableTarget(candidate) {
|
|
743
|
+
const target = candidate.symbol
|
|
744
|
+
? `${candidate.symbol.kind}:${candidate.symbol.qualifiedName || candidate.symbol.name || candidate.symbol.id}`
|
|
745
|
+
: candidate.nextChangePlanArgs.files?.join("\n") ?? candidate.path;
|
|
746
|
+
return `${candidate.kind}:${candidate.path}:${target}`;
|
|
747
|
+
}
|
|
748
|
+
function uniqueInOrder(values) {
|
|
749
|
+
const seen = new Set();
|
|
750
|
+
const result = [];
|
|
751
|
+
for (const value of values) {
|
|
752
|
+
if (seen.has(value)) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
seen.add(value);
|
|
756
|
+
result.push(value);
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
function meaningfulTaskTokens(value) {
|
|
761
|
+
const stop = new Set(["a", "an", "and", "as", "for", "how", "in", "of", "on", "or", "safely", "the", "to", "with"]);
|
|
762
|
+
return uniqueSorted(normalizeSearchText(value)
|
|
763
|
+
.split(/\s+/u)
|
|
764
|
+
.map((token) => token.trim())
|
|
765
|
+
.filter((token) => token.length >= 3 && !stop.has(token))).slice(0, 8);
|
|
766
|
+
}
|
|
767
|
+
function rawSearchQueries(task, target) {
|
|
768
|
+
const taskPart = meaningfulTaskTokens(task ?? "").slice(0, 4).join(" ");
|
|
769
|
+
const targetPart = target.split(/[/.]/u).filter(Boolean).slice(-2).join(" ");
|
|
770
|
+
return uniqueSorted([taskPart, targetPart, `${taskPart} ${targetPart}`].map((entry) => entry.trim()).filter(Boolean)).slice(0, 3);
|
|
771
|
+
}
|
|
772
|
+
function formatTargetCandidates(candidates) {
|
|
773
|
+
if (candidates.length === 0) {
|
|
774
|
+
return ["- none ranked from current packet; run search/raw search to find a file or symbol target."];
|
|
775
|
+
}
|
|
776
|
+
return candidates.slice(0, 6).map((candidate) => {
|
|
777
|
+
const target = candidate.kind === "symbol" && candidate.symbol ? `${candidate.symbol.qualifiedName} in ${candidate.path}` : candidate.path;
|
|
778
|
+
const nextArg = candidate.nextChangePlanArgs.files?.[0] ?? candidate.nextChangePlanArgs.symbols?.[0] ?? target;
|
|
779
|
+
return `- #${candidate.rank} ${candidate.candidateId} ${candidate.kind} ${target}: ${candidate.validationStatus}; score ${candidate.score.toFixed(1)}; risk ${candidate.candidateRisk.score.toFixed(1)}; followCandidate ${candidate.candidateId}; next change_plan target ${nextArg}; ${candidate.evidence.slice(0, 3).join("; ")}`;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function requiredWorkflowChecksForPlan(workflows, pathScope, changeType) {
|
|
783
|
+
return workflows
|
|
784
|
+
.filter((workflow) => workflow.relatedFiles.some((filePath) => pathScope.has(filePath)) || pathScope.has(workflow.entryPath))
|
|
785
|
+
.sort((a, b) => b.rank - a.rank || a.title.localeCompare(b.title))
|
|
786
|
+
.map((workflow) => ({
|
|
787
|
+
kind: "workflow",
|
|
788
|
+
target: workflow.title,
|
|
789
|
+
reason: changeType === "style"
|
|
790
|
+
? "workflow is adjacent to the planned edit; spot-check only if behavior changed"
|
|
791
|
+
: `planned edit intersects ${workflow.workflowKind} workflow evidence`,
|
|
792
|
+
evidenceTier: workflow.confidence === "authoritative" ? "authoritative" : workflow.confidence === "derived" ? "derived" : "heuristic",
|
|
793
|
+
confidence: workflow.confidence,
|
|
794
|
+
paths: uniqueSorted([workflow.entryPath, ...workflow.relatedFiles, ...workflow.tests]).slice(0, 20)
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
function requiredDependencyChecksForPlan(index, paths, changeType) {
|
|
798
|
+
if (paths.length === 0) {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
const pathSet = new Set(paths);
|
|
802
|
+
const edgeChecks = index.graphEdges
|
|
803
|
+
.filter((edge) => pathSet.has(edge.fromPath ?? "") || pathSet.has(edge.toPath ?? ""))
|
|
804
|
+
.filter((edge) => ["IMPORTS", "CALLS", "REFERENCES", "TESTS", "EXTENDS", "IMPLEMENTS", "EXPORTS", "TYPE_EXPORTS"].includes(edge.edgeKind))
|
|
805
|
+
.filter((edge) => !(pathSet.has(edge.fromPath ?? "") && pathSet.has(edge.toPath ?? "")))
|
|
806
|
+
.sort((a, b) => b.weight - a.weight || a.edgeKind.localeCompare(b.edgeKind) || (a.fromPath ?? "").localeCompare(b.fromPath ?? "") || (a.toPath ?? "").localeCompare(b.toPath ?? ""))
|
|
807
|
+
.slice(0, 10)
|
|
808
|
+
.map((edge) => ({
|
|
809
|
+
kind: "dependency",
|
|
810
|
+
target: `${edge.edgeKind}: ${edge.fromPath ?? edge.fromId} -> ${edge.toPath ?? edge.toId}`,
|
|
811
|
+
reason: changeType === "style"
|
|
812
|
+
? "dependency edge is adjacent to the planned edit; verify if public behavior changed"
|
|
813
|
+
: `planned edit has typed ${edge.edgeKind} dependency evidence`,
|
|
814
|
+
evidenceTier: (edge.confidence === "authoritative" ? "authoritative" : edge.confidence === "derived" ? "derived" : "heuristic"),
|
|
815
|
+
confidence: edge.confidence,
|
|
816
|
+
paths: uniqueSorted([edge.fromPath, edge.toPath].filter((filePath) => Boolean(filePath)))
|
|
817
|
+
}));
|
|
818
|
+
const publicFiles = index.files
|
|
819
|
+
.filter((file) => pathSet.has(file.path))
|
|
820
|
+
.filter((file) => file.rank >= 4 || file.riskScore >= 2)
|
|
821
|
+
.sort((a, b) => b.rank - a.rank || b.riskScore - a.riskScore || a.path.localeCompare(b.path))
|
|
822
|
+
.slice(0, 4)
|
|
823
|
+
.map((file) => ({
|
|
824
|
+
kind: "dependency",
|
|
825
|
+
target: `public-surface: ${file.path}`,
|
|
826
|
+
reason: `planned target is ranked ${file.rank.toFixed(2)} with risk ${file.riskScore.toFixed(1)}; check callers/tests before completion`,
|
|
827
|
+
evidenceTier: "derived",
|
|
828
|
+
confidence: "derived",
|
|
829
|
+
paths: uniqueSorted([
|
|
830
|
+
file.path,
|
|
831
|
+
...index.graphEdges
|
|
832
|
+
.filter((edge) => edge.fromPath === file.path || edge.toPath === file.path)
|
|
833
|
+
.flatMap((edge) => [edge.fromPath, edge.toPath])
|
|
834
|
+
.filter((filePath) => Boolean(filePath) && filePath !== file.path)
|
|
835
|
+
]).slice(0, 12)
|
|
836
|
+
}));
|
|
837
|
+
return dedupeRequiredChecks([...edgeChecks, ...publicFiles]);
|
|
838
|
+
}
|
|
839
|
+
function dedupeRequiredChecks(checks) {
|
|
840
|
+
const seen = new Set();
|
|
841
|
+
const result = [];
|
|
842
|
+
for (const check of checks) {
|
|
843
|
+
const key = `${check.kind}\0${check.target}`;
|
|
844
|
+
if (seen.has(key)) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
seen.add(key);
|
|
848
|
+
result.push(check);
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
function formatRequiredChecks(checks) {
|
|
853
|
+
if (checks.length === 0) {
|
|
854
|
+
return ["- none proven from current graph evidence"];
|
|
855
|
+
}
|
|
856
|
+
return checks.slice(0, 10).map((check) => `- ${check.target}: ${check.confidence}; ${check.reason}`);
|
|
857
|
+
}
|
|
858
|
+
//# sourceMappingURL=change-plan.js.map
|