@dev-tren/mapd 0.21.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/MASTER_PROMPT.md +134 -0
- package/README.md +494 -0
- package/SETUP.md +108 -0
- package/UAT.md +77 -0
- package/package.json +56 -0
- package/src/adapters/github-app.js +79 -0
- package/src/agents/anthropicClient.js +18 -0
- package/src/agents/llm.js +196 -0
- package/src/agents/modelResolver.js +87 -0
- package/src/agents/provider.js +222 -0
- package/src/chat/commandRunner.js +86 -0
- package/src/chat/commands.js +275 -0
- package/src/chat/intent.js +87 -0
- package/src/chat/llmIntent.js +118 -0
- package/src/chat/repl.js +471 -0
- package/src/cli.js +1408 -0
- package/src/config/index.js +197 -0
- package/src/config/schema.js +119 -0
- package/src/core/assist.js +64 -0
- package/src/core/audit.js +63 -0
- package/src/core/changes.js +110 -0
- package/src/core/confidence.js +0 -0
- package/src/core/configLint.js +141 -0
- package/src/core/diagnose.js +262 -0
- package/src/core/docs.js +140 -0
- package/src/core/doctor.js +134 -0
- package/src/core/envFiles.js +43 -0
- package/src/core/events.js +53 -0
- package/src/core/evidence.js +212 -0
- package/src/core/findingScoring.js +20 -0
- package/src/core/fix.js +192 -0
- package/src/core/fixApply.js +172 -0
- package/src/core/frameworkEntries.js +247 -0
- package/src/core/gates.js +209 -0
- package/src/core/graph.js +467 -0
- package/src/core/grounding.js +235 -0
- package/src/core/handoff.js +157 -0
- package/src/core/importResolver.js +218 -0
- package/src/core/improve.js +226 -0
- package/src/core/integrate.js +169 -0
- package/src/core/intelligence.js +212 -0
- package/src/core/modernize.js +370 -0
- package/src/core/parseCache.js +64 -0
- package/src/core/parser.js +536 -0
- package/src/core/policy.js +65 -0
- package/src/core/polyglot.js +333 -0
- package/src/core/proc.js +25 -0
- package/src/core/reachability.js +543 -0
- package/src/core/regression.js +193 -0
- package/src/core/resolution.js +92 -0
- package/src/core/retry.js +61 -0
- package/src/core/review.js +219 -0
- package/src/core/score.js +338 -0
- package/src/core/security.js +0 -0
- package/src/core/session.js +143 -0
- package/src/core/solutions.js +254 -0
- package/src/core/staleness.js +45 -0
- package/src/core/testGuidance.js +226 -0
- package/src/core/theme.js +50 -0
- package/src/core/trace.js +151 -0
- package/src/core/verify.js +123 -0
- package/src/core/view.js +221 -0
- package/src/core/viewServer.js +88 -0
- package/src/core/watch.js +76 -0
- package/src/core/workspace.js +115 -0
- package/src/mcp/server.js +48 -0
- package/src/mcp/tools.js +423 -0
- package/src/server.js +84 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* score.js — Score Intelligence. Turns the derived confidence number into an
|
|
3
|
+
* auditable account of WHY it is what it is, what it would become under a
|
|
4
|
+
* hypothetical, its honest maximum, and how it moved since the baseline.
|
|
5
|
+
*
|
|
6
|
+
* THE RULE: this file never invents a formula. It reads the signal values +
|
|
7
|
+
* weights that confidence.js already stores next to every score, and for
|
|
8
|
+
* what-if math it re-runs confidence.js's OWN scorer through the optional `sim`
|
|
9
|
+
* hook. If a signal is unavailable (e.g. no git → stability), that shows up as
|
|
10
|
+
* reduced signalCoverage, never as a faked value — including at the ceiling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { buildScoredGraph } from "./intelligence.js";
|
|
14
|
+
import { scoreWorkflow, aggregateRepo, REPO_COVERAGE_WEIGHT } from "./confidence.js";
|
|
15
|
+
import { loadBaseline } from "./regression.js";
|
|
16
|
+
import { bold, dim, red, green, yellow, cyan, confidenceColor } from "./theme.js";
|
|
17
|
+
|
|
18
|
+
const r3 = (x) => Number(x.toFixed(3));
|
|
19
|
+
|
|
20
|
+
/** Signal weights are fixed in confidence.js; mirror their intent for prose. */
|
|
21
|
+
const SIGNAL_BLURB = {
|
|
22
|
+
parseIntegrity: "workflow files parsed cleanly (heuristic-parsed files earn half credit)",
|
|
23
|
+
resolutionRate: "this workflow's own call edges resolved to a definition",
|
|
24
|
+
testPresence: "workflow files with a matching test file",
|
|
25
|
+
stability: "inverse code churn (needs git history)",
|
|
26
|
+
coverageOfRepo: "repo files reached by any workflow (repo-level, not per workflow)",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decompose a scored graph into per-signal contributions. For each available
|
|
31
|
+
* signal in a workflow: effectiveWeight = weight / (sum of available weights),
|
|
32
|
+
* contribution = value × effectiveWeight (these sum to the workflow score),
|
|
33
|
+
* cost = (1 − value) × effectiveWeight (what this weak signal costs the score).
|
|
34
|
+
* Repo-wide figures weight each workflow by its file share and by the
|
|
35
|
+
* workflow part of the repo formula (1 − REPO_COVERAGE_WEIGHT), then add the
|
|
36
|
+
* repo-level coverageOfRepo term — exactly aggregateRepo's formula, so repo
|
|
37
|
+
* contributions sum back to repoConfidence.
|
|
38
|
+
*/
|
|
39
|
+
export function explainScore(graph) {
|
|
40
|
+
const totalFiles = graph.workflows.reduce((a, w) => a + w.files.length, 0) || 1;
|
|
41
|
+
|
|
42
|
+
const workflows = graph.workflows.map((w) => {
|
|
43
|
+
const c = w.confidence;
|
|
44
|
+
const availWeight = Object.values(c.signals)
|
|
45
|
+
.filter((s) => !s.unavailable && s.value !== null)
|
|
46
|
+
.reduce((a, s) => a + s.weight, 0) || 1;
|
|
47
|
+
const fileShare = r3(w.files.length / totalFiles);
|
|
48
|
+
|
|
49
|
+
const signals = Object.entries(c.signals).map(([signal, s]) => {
|
|
50
|
+
if (s.unavailable || s.value === null) {
|
|
51
|
+
return { signal, value: null, weight: s.weight, unavailable: true, reason: s.reason ?? null, effectiveWeight: 0, contribution: 0, cost: 0 };
|
|
52
|
+
}
|
|
53
|
+
const eff = s.weight / availWeight;
|
|
54
|
+
return {
|
|
55
|
+
signal, value: s.value, weight: s.weight,
|
|
56
|
+
rawContribution: s.value * eff, rawCost: (1 - s.value) * eff, // unrounded, for exact repo sums
|
|
57
|
+
effectiveWeight: r3(eff),
|
|
58
|
+
contribution: r3(s.value * eff),
|
|
59
|
+
cost: r3((1 - s.value) * eff),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return { id: w.id, score: c.score, signalCoverage: c.signalCoverage, files: w.files.length, fileShare, rawShare: w.files.length / totalFiles, signals };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const wfPart = 1 - REPO_COVERAGE_WEIGHT;
|
|
66
|
+
const repoSignals = {};
|
|
67
|
+
for (const w of workflows) {
|
|
68
|
+
for (const s of w.signals) {
|
|
69
|
+
const acc = (repoSignals[s.signal] ??= { signal: s.signal, weight: s.weight, contribution: 0, cost: 0, availableShare: 0, unavailableIn: 0 });
|
|
70
|
+
if (s.unavailable) { acc.unavailableIn++; continue; }
|
|
71
|
+
// accumulate UNROUNDED values: rounding 40 shares × 4 signals first drifted the sum ~0.006 off repoConfidence
|
|
72
|
+
acc.contribution += s.rawContribution * w.rawShare * wfPart;
|
|
73
|
+
acc.cost += s.rawCost * w.rawShare * wfPart;
|
|
74
|
+
acc.availableShare += w.fileShare;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const cov = graph.repoSignals?.coverageOfRepo;
|
|
78
|
+
if (cov) {
|
|
79
|
+
repoSignals.coverageOfRepo = {
|
|
80
|
+
signal: "coverageOfRepo", weight: cov.weight, repoLevel: true,
|
|
81
|
+
contribution: cov.value * cov.weight, cost: (1 - cov.value) * cov.weight, availableShare: 1, unavailableIn: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
for (const w of workflows) {
|
|
85
|
+
delete w.rawShare;
|
|
86
|
+
for (const s of w.signals) { delete s.rawContribution; delete s.rawCost; }
|
|
87
|
+
}
|
|
88
|
+
for (const acc of Object.values(repoSignals)) {
|
|
89
|
+
acc.contribution = r3(acc.contribution);
|
|
90
|
+
acc.cost = r3(acc.cost);
|
|
91
|
+
acc.availableShare = r3(acc.availableShare);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
repoConfidence: graph.repoConfidence,
|
|
96
|
+
method: graph.workflows[0]?.confidence.method ?? null,
|
|
97
|
+
workflows,
|
|
98
|
+
repoSignals: Object.values(repoSignals).sort((a, b) => b.cost - a.cost),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Rescore under a `sim` hook WITHOUT mutating or cloning the graph. scoreWorkflow
|
|
104
|
+
* is pure over the graph (it reads, returns a fresh confidence, and the honest-
|
|
105
|
+
* test set is WeakMap-cached on the real graph so it's computed once), so we just
|
|
106
|
+
* call it per workflow and re-aggregate exactly as scoreGraph does — turning each
|
|
107
|
+
* what-if from a full deep-clone (~1s on a mid repo) into a few milliseconds,
|
|
108
|
+
* which is what makes `mapd improve` (dozens of sims) fast enough for chat.
|
|
109
|
+
*/
|
|
110
|
+
function simScores(rootDir, graph, sim) {
|
|
111
|
+
const workflows = graph.workflows.map((wf) => ({
|
|
112
|
+
id: wf.id,
|
|
113
|
+
files: wf.files,
|
|
114
|
+
confidence: scoreWorkflow(rootDir, graph, wf, sim),
|
|
115
|
+
}));
|
|
116
|
+
const { repoConfidence } = aggregateRepo(graph, workflows, sim);
|
|
117
|
+
return { repoConfidence, workflows };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The honest maximum reachable by in-repo verification work — writing tests,
|
|
122
|
+
* fixing parse errors on AST files, resolving calls, wiring uncovered files —
|
|
123
|
+
* while HOLDING git-derived stability at its currently observed value (you
|
|
124
|
+
* can't fabricate churn history) and leaving heuristic-parsed files at their
|
|
125
|
+
* structural half credit (regex-tier extraction genuinely knows less than an
|
|
126
|
+
* AST). Anything the ceiling can't close is reported as a structural cap, not
|
|
127
|
+
* hidden.
|
|
128
|
+
*/
|
|
129
|
+
export function ceilingScore(rootDir, graph) {
|
|
130
|
+
const astFiles = new Set(graph.files.filter((f) => f.parserKind !== "heuristic").map((f) => f.file));
|
|
131
|
+
const wfFiles = new Set(graph.workflows.flatMap((w) => w.files));
|
|
132
|
+
const heuristicWfFiles = [...wfFiles].filter((f) => !astFiles.has(f));
|
|
133
|
+
|
|
134
|
+
const sim = {
|
|
135
|
+
addTests: wfFiles, // every workflow file could get a test
|
|
136
|
+
fixParse: astFiles, // AST parse errors are fixable; heuristic files are not
|
|
137
|
+
resolutionRate: 1, // assumes every call is resolvable (see caveat)
|
|
138
|
+
coverageOfRepo: 1, // assumes every repo file is intentionally covered (see caveat)
|
|
139
|
+
};
|
|
140
|
+
const ceiling = simScores(rootDir, graph, sim);
|
|
141
|
+
|
|
142
|
+
const noGit = graph.workflows.some((w) => w.confidence.signals.stability?.reason === "no-git");
|
|
143
|
+
const noCalls = graph.workflows.filter((w) => w.confidence.signals.resolutionRate?.reason === "no-calls");
|
|
144
|
+
|
|
145
|
+
const caps = [];
|
|
146
|
+
if (heuristicWfFiles.length) {
|
|
147
|
+
caps.push({
|
|
148
|
+
cap: "heuristic-parse",
|
|
149
|
+
structural: true,
|
|
150
|
+
detail: `${heuristicWfFiles.length} workflow file(s) are parsed by heuristic language adapters and cap parseIntegrity at half credit — only switching to a full parser (or the language gaining one) lifts this.`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (noGit) {
|
|
154
|
+
caps.push({
|
|
155
|
+
cap: "no-git-stability",
|
|
156
|
+
structural: true,
|
|
157
|
+
detail: "No git history → the stability signal stays unavailable; even at the ceiling, signalCoverage is below 1.0 (the score is honest, but backed by less evidence).",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (noCalls.length) {
|
|
162
|
+
caps.push({
|
|
163
|
+
cap: "no-calls",
|
|
164
|
+
structural: true,
|
|
165
|
+
detail: `${noCalls.length} workflow(s) make no statically-resolvable calls, so resolutionRate has no evidence there and stays unavailable — honest, not fixable by resolving anything.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
current: graph.repoConfidence,
|
|
171
|
+
ceiling: ceiling.repoConfidence,
|
|
172
|
+
headroom: r3(ceiling.repoConfidence - graph.repoConfidence),
|
|
173
|
+
ceilingSignalCoverage: r3(
|
|
174
|
+
ceiling.workflows.reduce((a, w) => a + w.confidence.signalCoverage * w.files.length, 0) /
|
|
175
|
+
(ceiling.workflows.reduce((a, w) => a + w.files.length, 0) || 1)
|
|
176
|
+
),
|
|
177
|
+
assumptions: [
|
|
178
|
+
"all workflow files gain a test",
|
|
179
|
+
"all AST parse errors are fixed",
|
|
180
|
+
"resolutionRate reaches 1.0 (optimistic: some calls into external packages or dynamic dispatch may never resolve)",
|
|
181
|
+
"coverageOfRepo reaches 1.0 (optimistic: intentionally-dormant files should be annotated, not wired in)",
|
|
182
|
+
"stability held at its current observed value",
|
|
183
|
+
],
|
|
184
|
+
caps,
|
|
185
|
+
workflows: ceiling.workflows.map((w) => ({ id: w.id, current: graph.workflows.find((g) => g.id === w.id)?.confidence.score, ceiling: w.confidence.score })),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* What-if. Applies a hypothetical change and re-runs confidence.js's own scorer.
|
|
191
|
+
* mutations: { addTests?: string[], fixParse?: string[], resolutionRate?: number, coverageOfRepo?: number }
|
|
192
|
+
* Returns before/after with per-workflow deltas and a note for any named file
|
|
193
|
+
* that matches no workflow file (so a typo can't masquerade as "no effect").
|
|
194
|
+
*/
|
|
195
|
+
export function simulateScore(rootDir, graph, mutations = {}) {
|
|
196
|
+
const wfFiles = new Set(graph.workflows.flatMap((w) => w.files));
|
|
197
|
+
const norm = (arr) => (arr ?? []).map((f) => f.replace(/^\.\//, ""));
|
|
198
|
+
|
|
199
|
+
const addTests = norm(mutations.addTests);
|
|
200
|
+
const fixParse = norm(mutations.fixParse);
|
|
201
|
+
const unmatched = [...addTests, ...fixParse].filter((f) => !wfFiles.has(f));
|
|
202
|
+
|
|
203
|
+
const sim = {};
|
|
204
|
+
if (addTests.length) sim.addTests = new Set(addTests);
|
|
205
|
+
if (fixParse.length) sim.fixParse = new Set(fixParse);
|
|
206
|
+
if (mutations.resolutionRate != null) sim.resolutionRate = mutations.resolutionRate;
|
|
207
|
+
if (mutations.coverageOfRepo != null) sim.coverageOfRepo = mutations.coverageOfRepo;
|
|
208
|
+
|
|
209
|
+
const after = simScores(rootDir, graph, sim);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
before: graph.repoConfidence,
|
|
213
|
+
after: after.repoConfidence,
|
|
214
|
+
delta: r3(after.repoConfidence - graph.repoConfidence),
|
|
215
|
+
mutations: {
|
|
216
|
+
addTests, fixParse,
|
|
217
|
+
...(mutations.resolutionRate != null ? { resolutionRate: mutations.resolutionRate } : {}),
|
|
218
|
+
...(mutations.coverageOfRepo != null ? { coverageOfRepo: mutations.coverageOfRepo } : {}),
|
|
219
|
+
},
|
|
220
|
+
unmatched,
|
|
221
|
+
workflows: after.workflows.map((w) => {
|
|
222
|
+
const b = graph.workflows.find((g) => g.id === w.id)?.confidence.score ?? null;
|
|
223
|
+
return { id: w.id, before: b, after: w.confidence.score, delta: b == null ? null : r3(w.confidence.score - b) };
|
|
224
|
+
}).filter((w) => w.delta == null || w.delta !== 0),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Why confidence changed since the baseline snapshot (no git required — the
|
|
230
|
+
* baseline graph is a stored snapshot). Attributes the repoConfidence move to
|
|
231
|
+
* per-signal contribution changes (which fold in both value moves and file-share
|
|
232
|
+
* moves), plus workflows that appeared or disappeared.
|
|
233
|
+
*/
|
|
234
|
+
export function deltaScore(baselineGraph, currentGraph) {
|
|
235
|
+
const be = explainScore(baselineGraph);
|
|
236
|
+
const ce = explainScore(currentGraph);
|
|
237
|
+
const bySignal = new Map(be.repoSignals.map((s) => [s.signal, s]));
|
|
238
|
+
|
|
239
|
+
const signals = ce.repoSignals.map((s) => {
|
|
240
|
+
const b = bySignal.get(s.signal);
|
|
241
|
+
return { signal: s.signal, from: b?.contribution ?? 0, to: s.contribution, delta: r3(s.contribution - (b?.contribution ?? 0)) };
|
|
242
|
+
}).filter((s) => s.delta !== 0).sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
243
|
+
|
|
244
|
+
const baseWf = new Map(baselineGraph.workflows.map((w) => [w.id, w]));
|
|
245
|
+
const curWf = new Map(currentGraph.workflows.map((w) => [w.id, w]));
|
|
246
|
+
const added = [...curWf.keys()].filter((id) => !baseWf.has(id));
|
|
247
|
+
const removed = [...baseWf.keys()].filter((id) => !curWf.has(id));
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
from: baselineGraph.repoConfidence,
|
|
251
|
+
to: currentGraph.repoConfidence,
|
|
252
|
+
delta: r3(currentGraph.repoConfidence - baselineGraph.repoConfidence),
|
|
253
|
+
signals,
|
|
254
|
+
workflowsAdded: added,
|
|
255
|
+
workflowsRemoved: removed,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── renderers ────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
const pct = (x) => (x >= 0 ? "+" : "") + x.toFixed(3);
|
|
262
|
+
const signed = (x) => (x > 0 ? green(pct(x)) : x < 0 ? red(pct(x)) : dim(pct(x)));
|
|
263
|
+
|
|
264
|
+
export function renderExplain(data, { workflow } = {}) {
|
|
265
|
+
const lines = [`\n${bold("Score explain")} — repo confidence ${confidenceColor(data.repoConfidence)(data.repoConfidence)}`];
|
|
266
|
+
lines.push(dim(` ${data.method ?? ""}`));
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(bold(" Repo-wide — what each signal contributes / costs"));
|
|
269
|
+
for (const s of data.repoSignals) {
|
|
270
|
+
const tail = s.repoLevel ? dim(" (repo-level)") : s.unavailableIn ? dim(` (unavailable in ${s.unavailableIn} workflow(s))`) : "";
|
|
271
|
+
lines.push(` ${cyan(s.signal.padEnd(16))} contributes ${green(s.contribution.toFixed(3))} costs ${s.cost > 0 ? yellow(s.cost.toFixed(3)) : dim("0.000")}${tail}`);
|
|
272
|
+
lines.push(dim(` ${SIGNAL_BLURB[s.signal] ?? ""}`));
|
|
273
|
+
}
|
|
274
|
+
const shown = workflow ? data.workflows.filter((w) => w.id === workflow) : data.workflows;
|
|
275
|
+
for (const w of shown) {
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push(` ${confidenceColor(w.score)(`[${w.score}]`)} ${bold(w.id)} ${dim(`${w.files} files · share ${w.fileShare} · signalCoverage ${w.signalCoverage}`)}`);
|
|
278
|
+
for (const s of w.signals) {
|
|
279
|
+
if (s.unavailable) { lines.push(` ${dim(s.signal.padEnd(16))} ${dim(`unavailable${s.reason ? ` (${s.reason})` : ""} — weight redistributed`)}`); continue; }
|
|
280
|
+
lines.push(` ${s.signal.padEnd(16)} value ${s.value.toFixed(3)} → contributes ${s.contribution.toFixed(3)} ${s.cost > 0 ? yellow(`(costs ${s.cost.toFixed(3)})`) : dim("(maxed)")}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function renderCeiling(data) {
|
|
287
|
+
const lines = [`\n${bold("Score ceiling")} — honest maximum under current constraints`];
|
|
288
|
+
lines.push(` current ${confidenceColor(data.current)(data.current)} → ceiling ${confidenceColor(data.ceiling)(data.ceiling)} ${dim(`(headroom ${signed(data.headroom)})`)}`);
|
|
289
|
+
lines.push(` ceiling signalCoverage: ${data.ceilingSignalCoverage < 1 ? yellow(data.ceilingSignalCoverage) : green(data.ceilingSignalCoverage)}${data.ceilingSignalCoverage < 1 ? dim(" (below 1.0 — even maxed, some evidence is missing)") : ""}`);
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push(bold(" Assumes:"));
|
|
292
|
+
for (const a of data.assumptions) lines.push(` ${dim("·")} ${a}`);
|
|
293
|
+
if (data.caps.length) {
|
|
294
|
+
lines.push("");
|
|
295
|
+
lines.push(bold(" Structural caps (cannot be closed by in-repo work):"));
|
|
296
|
+
for (const c of data.caps) lines.push(` ${yellow("▪")} ${c.detail}`);
|
|
297
|
+
} else {
|
|
298
|
+
lines.push("");
|
|
299
|
+
lines.push(green(" No structural caps — 1.0 is honestly reachable with the work above."));
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function renderSimulate(data) {
|
|
305
|
+
const lines = [`\n${bold("Score simulate")} — what-if`];
|
|
306
|
+
const m = data.mutations;
|
|
307
|
+
const desc = [];
|
|
308
|
+
if (m.addTests.length) desc.push(`add tests for ${m.addTests.length} file(s)`);
|
|
309
|
+
if (m.fixParse.length) desc.push(`fix parse on ${m.fixParse.length} file(s)`);
|
|
310
|
+
if (m.resolutionRate != null) desc.push(`resolutionRate → ${m.resolutionRate}`);
|
|
311
|
+
if (m.coverageOfRepo != null) desc.push(`coverageOfRepo → ${m.coverageOfRepo}`);
|
|
312
|
+
lines.push(dim(` hypothesis: ${desc.join("; ") || "(none)"}`));
|
|
313
|
+
lines.push(` repo confidence ${confidenceColor(data.before)(data.before)} → ${confidenceColor(data.after)(data.after)} ${bold(signed(data.delta))}`);
|
|
314
|
+
if (data.unmatched.length) {
|
|
315
|
+
lines.push(yellow(` ⚠ ${data.unmatched.length} named file(s) match no workflow file (no effect): ${data.unmatched.join(", ")}`));
|
|
316
|
+
}
|
|
317
|
+
if (data.workflows.length) {
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push(bold(" Affected workflows"));
|
|
320
|
+
for (const w of data.workflows) lines.push(` ${w.id} ${w.before ?? "—"} → ${w.after} ${signed(w.delta ?? 0)}`);
|
|
321
|
+
}
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function renderDelta(data) {
|
|
326
|
+
const lines = [`\n${bold("Score delta")} — since baseline`];
|
|
327
|
+
lines.push(` repo confidence ${confidenceColor(data.from)(data.from)} → ${confidenceColor(data.to)(data.to)} ${bold(signed(data.delta))}`);
|
|
328
|
+
if (data.signals.length) {
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push(bold(" Attributed to signal contribution changes"));
|
|
331
|
+
for (const s of data.signals) lines.push(` ${signed(s.delta)} from ${cyan(s.signal)} ${dim(`(${s.from.toFixed(3)} → ${s.to.toFixed(3)})`)}`);
|
|
332
|
+
} else {
|
|
333
|
+
lines.push(dim(" no signal-level change"));
|
|
334
|
+
}
|
|
335
|
+
if (data.workflowsAdded.length) lines.push(` ${green("added:")} ${data.workflowsAdded.join(", ")}`);
|
|
336
|
+
if (data.workflowsRemoved.length) lines.push(` ${red("removed:")} ${data.workflowsRemoved.join(", ")}`);
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.js — chat session memory: append-only turn log on disk, plus the
|
|
3
|
+
* in-memory structured state (workflows discussed, files inspected, findings
|
|
4
|
+
* discussed, commands run, patches proposed/applied, gate/retry history) that
|
|
5
|
+
* chat/repl.js carries through a conversation.
|
|
6
|
+
*
|
|
7
|
+
* Summarization and budgeting here are deterministic (no LLM call) so chat
|
|
8
|
+
* memory works identically with or without a configured provider.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
|
|
15
|
+
const MAPD = ".mapd";
|
|
16
|
+
|
|
17
|
+
function sessionsDir(rootDir) { return path.join(path.resolve(rootDir), MAPD, "sessions"); }
|
|
18
|
+
|
|
19
|
+
export function createSessionId() {
|
|
20
|
+
return `sess-${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSessionState(rootDir, sessionId = createSessionId()) {
|
|
24
|
+
return {
|
|
25
|
+
id: sessionId,
|
|
26
|
+
rootDir: path.resolve(rootDir),
|
|
27
|
+
startedAt: new Date().toISOString(),
|
|
28
|
+
turns: [],
|
|
29
|
+
filesInspected: new Set(),
|
|
30
|
+
findingsDiscussed: new Set(),
|
|
31
|
+
commandsExecuted: [],
|
|
32
|
+
patchesProposed: [],
|
|
33
|
+
patchesApplied: [],
|
|
34
|
+
gateResults: [],
|
|
35
|
+
retryHistory: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sessionFile(rootDir, sessionId) { return path.join(sessionsDir(rootDir), `${sessionId}.json`); }
|
|
40
|
+
|
|
41
|
+
/** Append-only: persist the session state's turn log to disk after each turn. */
|
|
42
|
+
export function persistSession(state) {
|
|
43
|
+
fs.mkdirSync(sessionsDir(state.rootDir), { recursive: true });
|
|
44
|
+
const serializable = {
|
|
45
|
+
mapdSchema: 1,
|
|
46
|
+
id: state.id,
|
|
47
|
+
startedAt: state.startedAt,
|
|
48
|
+
turns: state.turns,
|
|
49
|
+
filesInspected: [...state.filesInspected],
|
|
50
|
+
findingsDiscussed: [...state.findingsDiscussed],
|
|
51
|
+
commandsExecuted: state.commandsExecuted,
|
|
52
|
+
patchesProposed: state.patchesProposed,
|
|
53
|
+
patchesApplied: state.patchesApplied,
|
|
54
|
+
};
|
|
55
|
+
fs.writeFileSync(sessionFile(state.rootDir, state.id), JSON.stringify(serializable, null, 2));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function recordTurn(state, role, content) {
|
|
59
|
+
state.turns.push({ role, content, at: new Date().toISOString() });
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render a chat session as readable Markdown — prompts, answers, and the facts touched. */
|
|
64
|
+
export function exportTranscriptMarkdown(state) {
|
|
65
|
+
const label = (role) => (role === "user" ? "🧑 You" : "🤖 Map'd");
|
|
66
|
+
const lines = [
|
|
67
|
+
`# Map'd chat transcript`,
|
|
68
|
+
"",
|
|
69
|
+
`- **Session:** \`${state.id}\``,
|
|
70
|
+
`- **Started:** ${state.startedAt}`,
|
|
71
|
+
`- **Project:** \`${state.rootDir}\``,
|
|
72
|
+
`- **Turns:** ${state.turns.length}`,
|
|
73
|
+
"",
|
|
74
|
+
"## Conversation",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
if (!state.turns.length) lines.push("_(no turns yet)_");
|
|
78
|
+
for (const t of state.turns) {
|
|
79
|
+
lines.push(`### ${label(t.role)} · ${t.at}`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(t.content);
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
const facts = [];
|
|
85
|
+
const listOf = (v) => (v instanceof Set ? [...v] : v ?? []);
|
|
86
|
+
const filesInspected = listOf(state.filesInspected);
|
|
87
|
+
const findings = listOf(state.findingsDiscussed);
|
|
88
|
+
const commands = (state.commandsExecuted ?? []).map((c) => c.label ?? c.command ?? String(c));
|
|
89
|
+
const applied = (state.patchesApplied ?? []).map((p) => p.id ?? String(p));
|
|
90
|
+
if (filesInspected.length) facts.push(`- **Files inspected:** ${filesInspected.join(", ")}`);
|
|
91
|
+
if (findings.length) facts.push(`- **Findings discussed:** ${findings.join(", ")}`);
|
|
92
|
+
if (commands.length) facts.push(`- **Commands run:** ${commands.join(", ")}`);
|
|
93
|
+
if (applied.length) facts.push(`- **Patches applied:** ${applied.join(", ")}`);
|
|
94
|
+
if (facts.length) { lines.push("## Session facts", "", ...facts, ""); }
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Deterministic conversation summarization: keep the most recent `keepLast`
|
|
100
|
+
* turns verbatim; compress everything older into one line per turn (role +
|
|
101
|
+
* first 80 chars). No LLM call — keeps memory functional with zero provider.
|
|
102
|
+
*/
|
|
103
|
+
export function summarizeConversation(turns, { keepLast = 6, maxChars = 4000 } = {}) {
|
|
104
|
+
if (turns.length <= keepLast) return turns.map((t) => `${t.role}: ${t.content}`).join("\n").slice(0, maxChars);
|
|
105
|
+
const older = turns.slice(0, turns.length - keepLast);
|
|
106
|
+
const recent = turns.slice(turns.length - keepLast);
|
|
107
|
+
const olderSummary = older.map((t) => `${t.role}: ${t.content.slice(0, 80)}`).join("\n");
|
|
108
|
+
const recentText = recent.map((t) => `${t.role}: ${t.content}`).join("\n");
|
|
109
|
+
return `[${older.length} earlier turn(s) summarized]\n${olderSummary}\n---\n${recentText}`.slice(0, maxChars);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Priority-ranked, deduplicated, character-budgeted context assembly. Used
|
|
114
|
+
* by chat/mcp/fix.js's context packages. `items`: [{text, priority, source}].
|
|
115
|
+
* Higher `priority` wins; ties keep original order. Truncates the last item
|
|
116
|
+
* that doesn't fully fit rather than dropping it outright.
|
|
117
|
+
*/
|
|
118
|
+
export function buildContextBudget(items, maxChars) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const deduped = items.filter((i) => {
|
|
121
|
+
const key = i.text.trim();
|
|
122
|
+
if (!key || seen.has(key)) return false;
|
|
123
|
+
seen.add(key);
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
const ranked = [...deduped].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
127
|
+
|
|
128
|
+
const included = [];
|
|
129
|
+
let used = 0;
|
|
130
|
+
let omitted = 0;
|
|
131
|
+
for (const item of ranked) {
|
|
132
|
+
if (used >= maxChars) { omitted++; continue; }
|
|
133
|
+
const remaining = maxChars - used;
|
|
134
|
+
if (item.text.length <= remaining) {
|
|
135
|
+
included.push(item);
|
|
136
|
+
used += item.text.length;
|
|
137
|
+
} else {
|
|
138
|
+
included.push({ ...item, text: item.text.slice(0, remaining), truncated: true });
|
|
139
|
+
used = maxChars;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { included, omittedCount: omitted, usedChars: used };
|
|
143
|
+
}
|