@flyingrobots/graft 0.4.0 → 0.5.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/ARCHITECTURE.md +386 -0
- package/CHANGELOG.md +47 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/README.md +153 -17
- package/bin/graft.js +4 -14
- package/docs/ADVANCED_GUIDE.md +49 -0
- package/docs/CLI.md +43 -0
- package/docs/GUIDE.md +321 -32
- package/docs/MCP.md +44 -0
- package/package.json +15 -4
- package/src/adapters/node-fs.ts +4 -0
- package/src/adapters/node-git.ts +47 -0
- package/src/adapters/node-process-runner.ts +27 -0
- package/src/cli/index-cmd.ts +75 -11
- package/src/cli/init.ts +808 -57
- package/src/cli/main.ts +437 -0
- package/src/contracts/capabilities.ts +341 -0
- package/src/contracts/causal-ontology.ts +622 -0
- package/src/contracts/causal-surface-next-action.ts +18 -0
- package/src/contracts/output-schemas.ts +1169 -0
- package/src/git/diff.ts +25 -21
- package/src/git/target-git-hook-bootstrap.ts +56 -0
- package/src/hooks/posttooluse-read.ts +21 -74
- package/src/hooks/pretooluse-read.ts +20 -56
- package/src/hooks/read-governor.ts +95 -0
- package/src/hooks/read-messages.ts +53 -0
- package/src/mcp/burden.ts +123 -0
- package/src/mcp/cache.ts +51 -0
- package/src/mcp/cached-file.ts +10 -8
- package/src/mcp/context.ts +65 -2
- package/src/mcp/daemon-control-plane.ts +554 -0
- package/src/mcp/daemon-job-scheduler.ts +279 -0
- package/src/mcp/daemon-repos.ts +216 -0
- package/src/mcp/daemon-server.ts +396 -0
- package/src/mcp/daemon-worker-pool.ts +310 -0
- package/src/mcp/daemon-worker-process.ts +52 -0
- package/src/mcp/metrics.ts +108 -1
- package/src/mcp/monitor-tick-job.ts +99 -0
- package/src/mcp/persisted-local-history.ts +1246 -0
- package/src/mcp/persistent-monitor-runtime.ts +549 -0
- package/src/mcp/policy.ts +84 -0
- package/src/mcp/receipt.ts +82 -12
- package/src/mcp/repo-concurrency.ts +318 -0
- package/src/mcp/repo-state.ts +777 -0
- package/src/mcp/repo-tool-job.ts +302 -0
- package/src/mcp/run-capture-config.ts +33 -0
- package/src/mcp/runtime-causal-context.ts +72 -0
- package/src/mcp/runtime-observability.ts +219 -0
- package/src/mcp/runtime-staged-target.ts +161 -0
- package/src/mcp/runtime-workspace-overlay.ts +255 -0
- package/src/mcp/semantic-transition-guidance.ts +60 -0
- package/src/mcp/semantic-transition-summary.ts +130 -0
- package/src/mcp/server.ts +696 -55
- package/src/mcp/stdio-server.ts +12 -0
- package/src/mcp/stdio.ts +2 -5
- package/src/mcp/tools/activity-view.ts +325 -0
- package/src/mcp/tools/causal-attach.ts +67 -0
- package/src/mcp/tools/causal-status.ts +58 -0
- package/src/mcp/tools/changed-since.ts +13 -11
- package/src/mcp/tools/code-find.ts +164 -0
- package/src/mcp/tools/code-refs.ts +466 -0
- package/src/mcp/tools/code-show.ts +252 -0
- package/src/mcp/tools/daemon-monitors.ts +14 -0
- package/src/mcp/tools/daemon-repos.ts +22 -0
- package/src/mcp/tools/daemon-sessions.ts +14 -0
- package/src/mcp/tools/daemon-status.ts +12 -0
- package/src/mcp/tools/doctor.ts +45 -2
- package/src/mcp/tools/explain.ts +4 -0
- package/src/mcp/tools/file-outline.ts +7 -3
- package/src/mcp/tools/git-files.ts +73 -0
- package/src/mcp/tools/graft-diff.ts +12 -4
- package/src/mcp/tools/map.ts +92 -38
- package/src/mcp/tools/monitor-pause.ts +18 -0
- package/src/mcp/tools/monitor-resume.ts +18 -0
- package/src/mcp/tools/monitor-start.ts +20 -0
- package/src/mcp/tools/monitor-stop.ts +18 -0
- package/src/mcp/tools/precision-match.ts +51 -0
- package/src/mcp/tools/precision-query.ts +127 -0
- package/src/mcp/tools/precision.ts +312 -0
- package/src/mcp/tools/run-capture.ts +126 -44
- package/src/mcp/tools/safe-read.ts +14 -12
- package/src/mcp/tools/since.ts +7 -2
- package/src/mcp/tools/state.ts +11 -3
- package/src/mcp/tools/stats.ts +5 -1
- package/src/mcp/tools/workspace-authorizations.ts +14 -0
- package/src/mcp/tools/workspace-authorize.ts +20 -0
- package/src/mcp/tools/workspace-bind.ts +25 -0
- package/src/mcp/tools/workspace-rebind.ts +25 -0
- package/src/mcp/tools/workspace-revoke.ts +18 -0
- package/src/mcp/tools/workspace-status.ts +12 -0
- package/src/mcp/warp-pool.ts +36 -0
- package/src/mcp/workspace-router.ts +984 -0
- package/src/operations/file-outline.ts +12 -2
- package/src/operations/graft-diff.ts +56 -10
- package/src/operations/safe-read.ts +27 -4
- package/src/operations/state.ts +6 -9
- package/src/parser/lang.ts +19 -3
- package/src/parser/outline.ts +191 -2
- package/src/parser/types.ts +9 -1
- package/src/policy/types.ts +4 -3
- package/src/ports/filesystem.ts +1 -0
- package/src/ports/git.ts +16 -0
- package/src/ports/process-runner.ts +22 -0
- package/src/release/security-gate.ts +102 -0
- package/src/session/tracker.ts +31 -0
- package/src/version.ts +3 -0
- package/src/warp/indexer.ts +171 -56
- package/src/warp/observers.ts +1 -1
- package/src/warp/open.ts +4 -3
- package/src/warp/plumbing.d.ts +5 -1
- package/src/warp/writer-id.ts +30 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const severitySchema = z.enum([ "info", "low", "moderate", "high", "critical" ]);
|
|
4
|
+
|
|
5
|
+
const auditReportSchema = z.object({
|
|
6
|
+
metadata: z.object({
|
|
7
|
+
vulnerabilities: z.object({
|
|
8
|
+
info: z.number().int().nonnegative(),
|
|
9
|
+
low: z.number().int().nonnegative(),
|
|
10
|
+
moderate: z.number().int().nonnegative(),
|
|
11
|
+
high: z.number().int().nonnegative(),
|
|
12
|
+
critical: z.number().int().nonnegative(),
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
15
|
+
advisories: z.record(z.string(), z.object({
|
|
16
|
+
id: z.number().int().nonnegative(),
|
|
17
|
+
module_name: z.string(),
|
|
18
|
+
severity: severitySchema,
|
|
19
|
+
title: z.string(),
|
|
20
|
+
recommendation: z.string(),
|
|
21
|
+
github_advisory_id: z.string().nullable().optional(),
|
|
22
|
+
findings: z.array(z.object({
|
|
23
|
+
version: z.string(),
|
|
24
|
+
paths: z.array(z.string()),
|
|
25
|
+
})).optional(),
|
|
26
|
+
})),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export interface SecurityGateViolation {
|
|
30
|
+
readonly id: number;
|
|
31
|
+
readonly moduleName: string;
|
|
32
|
+
readonly severity: "high" | "critical";
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly advisoryId: string | null;
|
|
35
|
+
readonly recommendation: string;
|
|
36
|
+
readonly paths: readonly string[];
|
|
37
|
+
readonly versions: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SecurityGateResult {
|
|
41
|
+
readonly blocking: boolean;
|
|
42
|
+
readonly counts: {
|
|
43
|
+
readonly info: number;
|
|
44
|
+
readonly low: number;
|
|
45
|
+
readonly moderate: number;
|
|
46
|
+
readonly high: number;
|
|
47
|
+
readonly critical: number;
|
|
48
|
+
};
|
|
49
|
+
readonly blockingFindings: readonly SecurityGateViolation[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function evaluateSecurityGate(report: unknown): SecurityGateResult {
|
|
53
|
+
const parsed = auditReportSchema.parse(report);
|
|
54
|
+
const blockingFindings: SecurityGateViolation[] = [];
|
|
55
|
+
|
|
56
|
+
for (const advisory of Object.values(parsed.advisories)) {
|
|
57
|
+
if (advisory.severity !== "high" && advisory.severity !== "critical") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
blockingFindings.push({
|
|
61
|
+
id: advisory.id,
|
|
62
|
+
moduleName: advisory.module_name,
|
|
63
|
+
severity: advisory.severity,
|
|
64
|
+
title: advisory.title,
|
|
65
|
+
advisoryId: advisory.github_advisory_id ?? null,
|
|
66
|
+
recommendation: advisory.recommendation,
|
|
67
|
+
paths: advisory.findings?.flatMap((finding) => finding.paths) ?? [],
|
|
68
|
+
versions: advisory.findings?.map((finding) => finding.version) ?? [],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
blocking: blockingFindings.length > 0,
|
|
74
|
+
counts: parsed.metadata.vulnerabilities,
|
|
75
|
+
blockingFindings,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatSecurityGateResult(result: SecurityGateResult): string {
|
|
80
|
+
const lines = [
|
|
81
|
+
`audit summary: critical=${String(result.counts.critical)} high=${String(result.counts.high)} moderate=${String(result.counts.moderate)} low=${String(result.counts.low)} info=${String(result.counts.info)}`,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
if (!result.blocking) {
|
|
85
|
+
lines.push("release security gate: pass");
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
lines.push("release security gate: fail");
|
|
90
|
+
for (const finding of result.blockingFindings) {
|
|
91
|
+
const advisoryId = finding.advisoryId ?? `npm:${String(finding.id)}`;
|
|
92
|
+
const paths = finding.paths.length > 0 ? finding.paths.join(", ") : "unknown path";
|
|
93
|
+
const versions = finding.versions.length > 0 ? finding.versions.join(", ") : "unknown version";
|
|
94
|
+
lines.push(
|
|
95
|
+
`- ${finding.severity.toUpperCase()} ${advisoryId} ${finding.moduleName}@${versions} via ${paths}`,
|
|
96
|
+
);
|
|
97
|
+
lines.push(` ${finding.title}`);
|
|
98
|
+
lines.push(` ${finding.recommendation}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
package/src/session/tracker.ts
CHANGED
|
@@ -5,6 +5,15 @@ const EDIT_BASH_TOOLS = new Set(["Edit", "Bash"]);
|
|
|
5
5
|
const LATE_READ_BYTE_THRESHOLD = 20480;
|
|
6
6
|
const LATE_READ_MESSAGE_THRESHOLD = 300;
|
|
7
7
|
|
|
8
|
+
export interface SessionTrackerSnapshot {
|
|
9
|
+
readonly totalMessages: number;
|
|
10
|
+
readonly toolCallsSinceUser: number;
|
|
11
|
+
readonly editBashTransitions: number;
|
|
12
|
+
readonly lastEditBashTool: string | null;
|
|
13
|
+
readonly budgetBytes: number | null;
|
|
14
|
+
readonly consumedBytes: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export class SessionTracker {
|
|
9
18
|
private totalMessages = 0;
|
|
10
19
|
private toolCallsSinceUser = 0;
|
|
@@ -13,6 +22,17 @@ export class SessionTracker {
|
|
|
13
22
|
private budgetBytes: number | null = null;
|
|
14
23
|
private consumedBytes = 0;
|
|
15
24
|
|
|
25
|
+
static fromSnapshot(snapshot: SessionTrackerSnapshot): SessionTracker {
|
|
26
|
+
const tracker = new SessionTracker();
|
|
27
|
+
tracker.totalMessages = snapshot.totalMessages;
|
|
28
|
+
tracker.toolCallsSinceUser = snapshot.toolCallsSinceUser;
|
|
29
|
+
tracker.editBashTransitions = snapshot.editBashTransitions;
|
|
30
|
+
tracker.lastEditBashTool = snapshot.lastEditBashTool;
|
|
31
|
+
tracker.budgetBytes = snapshot.budgetBytes;
|
|
32
|
+
tracker.consumedBytes = snapshot.consumedBytes;
|
|
33
|
+
return tracker;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
getMessageCount(): number {
|
|
17
37
|
return this.totalMessages;
|
|
18
38
|
}
|
|
@@ -102,6 +122,17 @@ export class SessionTracker {
|
|
|
102
122
|
};
|
|
103
123
|
}
|
|
104
124
|
|
|
125
|
+
snapshot(): SessionTrackerSnapshot {
|
|
126
|
+
return {
|
|
127
|
+
totalMessages: this.totalMessages,
|
|
128
|
+
toolCallsSinceUser: this.toolCallsSinceUser,
|
|
129
|
+
editBashTransitions: this.editBashTransitions,
|
|
130
|
+
lastEditBashTool: this.lastEditBashTool,
|
|
131
|
+
budgetBytes: this.budgetBytes,
|
|
132
|
+
consumedBytes: this.consumedBytes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
getSessionDepth(): SessionDepth {
|
|
106
137
|
if (this.totalMessages < 100) {
|
|
107
138
|
return "early";
|
package/src/version.ts
ADDED
package/src/warp/indexer.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type WarpApp from "@git-stunts/git-warp";
|
|
10
|
-
import {
|
|
10
|
+
import type { GitClient } from "../ports/git.js";
|
|
11
11
|
import { extractOutline } from "../parser/outline.js";
|
|
12
12
|
import { diffOutlines } from "../parser/diff.js";
|
|
13
13
|
import { detectLang } from "../parser/lang.js";
|
|
@@ -17,6 +17,7 @@ import type { DiffEntry } from "../parser/diff.js";
|
|
|
17
17
|
|
|
18
18
|
export interface IndexOptions {
|
|
19
19
|
readonly cwd: string;
|
|
20
|
+
readonly git: GitClient;
|
|
20
21
|
readonly from?: string;
|
|
21
22
|
readonly to?: string;
|
|
22
23
|
}
|
|
@@ -36,22 +37,46 @@ interface PatchOps {
|
|
|
36
37
|
removeEdge(from: string, to: string, label: string): PatchOps;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
interface PreparedChange {
|
|
41
|
+
readonly status: string;
|
|
42
|
+
readonly filePath: string;
|
|
43
|
+
readonly fileId: string;
|
|
44
|
+
readonly lang: string | null;
|
|
45
|
+
readonly parentExists: boolean;
|
|
46
|
+
readonly oldOutline: readonly OutlineEntry[];
|
|
47
|
+
readonly newOutline?: readonly OutlineEntry[] | undefined;
|
|
48
|
+
readonly jumpLookup?: Map<string, { start: number; end: number }> | undefined;
|
|
49
|
+
readonly diff?: ReturnType<typeof diffOutlines> | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function git(gitClient: GitClient, cwd: string, args: readonly string[]): Promise<string> {
|
|
53
|
+
const result = await gitClient.run({ cwd, args });
|
|
54
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
55
|
+
throw result.error ?? new Error(result.stderr.trim() || `git exited with status ${String(result.status)}`);
|
|
56
|
+
}
|
|
57
|
+
return result.stdout;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function listCommits(gitClient: GitClient, cwd: string, from?: string, to?: string): Promise<string[]> {
|
|
40
61
|
const range = from !== undefined ? `${from}..${to ?? "HEAD"}` : to ?? "HEAD";
|
|
41
62
|
const args = ["log", "--reverse", "--format=%H", range];
|
|
42
63
|
try {
|
|
43
|
-
return
|
|
64
|
+
return (await git(gitClient, cwd, args))
|
|
44
65
|
.trim().split("\n").filter((l) => l.length > 0);
|
|
45
66
|
} catch {
|
|
46
67
|
return [];
|
|
47
68
|
}
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
function getCommitChanges(
|
|
71
|
+
async function getCommitChanges(
|
|
72
|
+
gitClient: GitClient,
|
|
73
|
+
sha: string,
|
|
74
|
+
cwd: string,
|
|
75
|
+
): Promise<{ status: string; path: string }[]> {
|
|
51
76
|
// --root handles the initial commit (no parent to diff against)
|
|
52
77
|
const args = ["diff-tree", "--root", "--no-commit-id", "-r", "--name-status", sha];
|
|
53
78
|
try {
|
|
54
|
-
return
|
|
79
|
+
return (await git(gitClient, cwd, args))
|
|
55
80
|
.trim().split("\n").filter((l) => l.length > 0).map((line) => {
|
|
56
81
|
const parts = line.split("\t");
|
|
57
82
|
return { status: parts[0] ?? "", path: parts[1] ?? "" };
|
|
@@ -61,9 +86,13 @@ function getCommitChanges(sha: string, cwd: string): { status: string; path: str
|
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
function getCommitMeta(
|
|
89
|
+
async function getCommitMeta(
|
|
90
|
+
gitClient: GitClient,
|
|
91
|
+
sha: string,
|
|
92
|
+
cwd: string,
|
|
93
|
+
): Promise<{ message: string; author: string; email: string; timestamp: string }> {
|
|
65
94
|
try {
|
|
66
|
-
const output =
|
|
95
|
+
const output = await git(gitClient, cwd, ["log", "-1", "--format=%s%n%aN%n%aE%n%aI", sha]);
|
|
67
96
|
const lines = output.trim().split("\n");
|
|
68
97
|
return { message: lines[0] ?? "", author: lines[1] ?? "", email: lines[2] ?? "", timestamp: lines[3] ?? "" };
|
|
69
98
|
} catch {
|
|
@@ -74,9 +103,9 @@ function getCommitMeta(sha: string, cwd: string): { message: string; author: str
|
|
|
74
103
|
/**
|
|
75
104
|
* Check if a commit has a parent (is not the root commit).
|
|
76
105
|
*/
|
|
77
|
-
function hasParent(sha: string, cwd: string): boolean {
|
|
106
|
+
async function hasParent(gitClient: GitClient, sha: string, cwd: string): Promise<boolean> {
|
|
78
107
|
try {
|
|
79
|
-
|
|
108
|
+
await git(gitClient, cwd, ["rev-parse", "--verify", `${sha}~1`]);
|
|
80
109
|
return true;
|
|
81
110
|
} catch {
|
|
82
111
|
return false;
|
|
@@ -262,6 +291,104 @@ function applyChildDiffs(
|
|
|
262
291
|
}
|
|
263
292
|
}
|
|
264
293
|
|
|
294
|
+
async function prepareChange(
|
|
295
|
+
gitClient: GitClient,
|
|
296
|
+
cwd: string,
|
|
297
|
+
sha: string,
|
|
298
|
+
parentRef: string,
|
|
299
|
+
parentExists: boolean,
|
|
300
|
+
change: { status: string; path: string },
|
|
301
|
+
): Promise<PreparedChange> {
|
|
302
|
+
const filePath = change.path;
|
|
303
|
+
const fileId = fileNodeId(filePath);
|
|
304
|
+
const lang = detectLang(filePath);
|
|
305
|
+
|
|
306
|
+
if (change.status === "D") {
|
|
307
|
+
let oldOutline: readonly OutlineEntry[] = [];
|
|
308
|
+
if (lang !== null && parentExists) {
|
|
309
|
+
const oldContent = await getFileAtRef(parentRef, filePath, { cwd, git: gitClient });
|
|
310
|
+
if (oldContent !== null) {
|
|
311
|
+
oldOutline = extractOutline(oldContent, lang).entries;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
status: change.status,
|
|
316
|
+
filePath,
|
|
317
|
+
fileId,
|
|
318
|
+
lang,
|
|
319
|
+
parentExists,
|
|
320
|
+
oldOutline,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (lang === null) {
|
|
325
|
+
return {
|
|
326
|
+
status: change.status,
|
|
327
|
+
filePath,
|
|
328
|
+
fileId,
|
|
329
|
+
lang,
|
|
330
|
+
parentExists,
|
|
331
|
+
oldOutline: [],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const newContent = await getFileAtRef(sha, filePath, { cwd, git: gitClient });
|
|
336
|
+
if (newContent === null) {
|
|
337
|
+
return {
|
|
338
|
+
status: change.status,
|
|
339
|
+
filePath,
|
|
340
|
+
fileId,
|
|
341
|
+
lang,
|
|
342
|
+
parentExists,
|
|
343
|
+
oldOutline: [],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const newResult = extractOutline(newContent, lang);
|
|
348
|
+
const newOutline = newResult.entries;
|
|
349
|
+
const jumpLookup = buildJumpLookup(newResult.jumpTable ?? []);
|
|
350
|
+
|
|
351
|
+
if (change.status === "A" || !parentExists) {
|
|
352
|
+
return {
|
|
353
|
+
status: change.status,
|
|
354
|
+
filePath,
|
|
355
|
+
fileId,
|
|
356
|
+
lang,
|
|
357
|
+
parentExists,
|
|
358
|
+
oldOutline: [],
|
|
359
|
+
newOutline,
|
|
360
|
+
jumpLookup,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const oldContent = await getFileAtRef(parentRef, filePath, { cwd, git: gitClient });
|
|
365
|
+
if (oldContent === null) {
|
|
366
|
+
return {
|
|
367
|
+
status: change.status,
|
|
368
|
+
filePath,
|
|
369
|
+
fileId,
|
|
370
|
+
lang,
|
|
371
|
+
parentExists,
|
|
372
|
+
oldOutline: [],
|
|
373
|
+
newOutline,
|
|
374
|
+
jumpLookup,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const oldOutline = extractOutline(oldContent, lang).entries;
|
|
379
|
+
return {
|
|
380
|
+
status: change.status,
|
|
381
|
+
filePath,
|
|
382
|
+
fileId,
|
|
383
|
+
lang,
|
|
384
|
+
parentExists,
|
|
385
|
+
oldOutline,
|
|
386
|
+
newOutline,
|
|
387
|
+
jumpLookup,
|
|
388
|
+
diff: diffOutlines(oldOutline, newOutline),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
265
392
|
/**
|
|
266
393
|
* Index a range of commits into the WARP graph.
|
|
267
394
|
*/
|
|
@@ -269,14 +396,14 @@ export async function indexCommits(
|
|
|
269
396
|
warp: WarpApp,
|
|
270
397
|
options: IndexOptions,
|
|
271
398
|
): Promise<IndexResult> {
|
|
272
|
-
const { cwd } = options;
|
|
273
|
-
const commits = listCommits(cwd, options.from, options.to);
|
|
399
|
+
const { cwd, git: gitClient } = options;
|
|
400
|
+
const commits = await listCommits(gitClient, cwd, options.from, options.to);
|
|
274
401
|
|
|
275
402
|
let patchesWritten = 0;
|
|
276
403
|
const commitTicks = new Map<string, number>();
|
|
277
404
|
|
|
278
405
|
for (const sha of commits) {
|
|
279
|
-
const changes = getCommitChanges(sha, cwd);
|
|
406
|
+
const changes = await getCommitChanges(gitClient, sha, cwd);
|
|
280
407
|
|
|
281
408
|
// Only materialize when removals are possible (D or M status).
|
|
282
409
|
// Materialization is expensive — O(n) replay of all prior patches.
|
|
@@ -286,9 +413,12 @@ export async function indexCommits(
|
|
|
286
413
|
await warp.core().materialize();
|
|
287
414
|
}
|
|
288
415
|
|
|
289
|
-
const meta = getCommitMeta(sha, cwd);
|
|
290
|
-
const parentExists = hasParent(sha, cwd);
|
|
416
|
+
const meta = await getCommitMeta(gitClient, sha, cwd);
|
|
417
|
+
const parentExists = await hasParent(gitClient, sha, cwd);
|
|
291
418
|
const parentRef = `${sha}~1`;
|
|
419
|
+
const preparedChanges = await Promise.all(changes.map((change) =>
|
|
420
|
+
prepareChange(gitClient, cwd, sha, parentRef, parentExists, change)
|
|
421
|
+
));
|
|
292
422
|
|
|
293
423
|
await warp.patch((p) => {
|
|
294
424
|
const patch = p as unknown as PatchOps;
|
|
@@ -301,62 +431,40 @@ export async function indexCommits(
|
|
|
301
431
|
patch.setProperty(commitId, "email", meta.email);
|
|
302
432
|
patch.setProperty(commitId, "timestamp", meta.timestamp);
|
|
303
433
|
|
|
304
|
-
for (const change of
|
|
305
|
-
const filePath = change.path;
|
|
306
|
-
const fileId = fileNodeId(filePath);
|
|
307
|
-
const lang = detectLang(filePath);
|
|
308
|
-
|
|
434
|
+
for (const change of preparedChanges) {
|
|
309
435
|
if (change.status === "D") {
|
|
310
|
-
if (lang !== null
|
|
311
|
-
|
|
312
|
-
if (oldContent !== null) {
|
|
313
|
-
const oldOutline = extractOutline(oldContent, lang).entries;
|
|
314
|
-
removeSymbols(patch, filePath, oldOutline);
|
|
315
|
-
}
|
|
436
|
+
if (change.lang !== null) {
|
|
437
|
+
removeSymbols(patch, change.filePath, change.oldOutline);
|
|
316
438
|
}
|
|
317
|
-
patch.removeNode(fileId);
|
|
439
|
+
patch.removeNode(change.fileId);
|
|
318
440
|
continue;
|
|
319
441
|
}
|
|
320
442
|
|
|
321
443
|
// Added or modified — ensure file + directory nodes exist
|
|
322
|
-
patch.addNode(fileId);
|
|
323
|
-
patch.setProperty(fileId, "path", filePath);
|
|
324
|
-
patch.setProperty(fileId, "lang", lang ?? "unknown");
|
|
325
|
-
patch.addEdge(commitId, fileId, "touches");
|
|
326
|
-
emitDirectoryChain(patch, filePath);
|
|
444
|
+
patch.addNode(change.fileId);
|
|
445
|
+
patch.setProperty(change.fileId, "path", change.filePath);
|
|
446
|
+
patch.setProperty(change.fileId, "lang", change.lang ?? "unknown");
|
|
447
|
+
patch.addEdge(commitId, change.fileId, "touches");
|
|
448
|
+
emitDirectoryChain(patch, change.filePath);
|
|
327
449
|
|
|
328
|
-
if (lang === null) continue;
|
|
450
|
+
if (change.lang === null || change.newOutline === undefined || change.jumpLookup === undefined) continue;
|
|
329
451
|
|
|
330
|
-
|
|
331
|
-
if (newContent === null) continue;
|
|
332
|
-
const newResult = extractOutline(newContent, lang);
|
|
333
|
-
const newOutline = newResult.entries;
|
|
334
|
-
const jumpLookup = buildJumpLookup(newResult.jumpTable ?? []);
|
|
335
|
-
|
|
336
|
-
if (change.status === "A" || !parentExists) {
|
|
452
|
+
if (change.status === "A" || !change.parentExists || change.diff === undefined) {
|
|
337
453
|
// New file or root commit — emit all symbols
|
|
338
|
-
emitSymbols(patch, filePath, newOutline, jumpLookup);
|
|
454
|
+
emitSymbols(patch, change.filePath, change.newOutline, change.jumpLookup);
|
|
339
455
|
} else {
|
|
340
|
-
|
|
341
|
-
const oldContent = getFileAtRef(parentRef, filePath, cwd);
|
|
342
|
-
if (oldContent === null) {
|
|
343
|
-
emitSymbols(patch, filePath, newOutline, jumpLookup);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const oldOutline = extractOutline(oldContent, lang).entries;
|
|
348
|
-
const diff = diffOutlines(oldOutline, newOutline);
|
|
456
|
+
const diff = change.diff;
|
|
349
457
|
|
|
350
458
|
// Remove deleted symbols
|
|
351
459
|
for (const removed of diff.removed) {
|
|
352
|
-
const symId = symNodeId(filePath, removed.name);
|
|
460
|
+
const symId = symNodeId(change.filePath, removed.name);
|
|
353
461
|
patch.addEdge(commitId, symId, "removes");
|
|
354
|
-
removeDiffSymbols(patch, filePath, fileId, [removed]);
|
|
462
|
+
removeDiffSymbols(patch, change.filePath, change.fileId, [removed]);
|
|
355
463
|
}
|
|
356
464
|
|
|
357
465
|
// Add new symbols (preserve actual exported status)
|
|
358
466
|
for (const added of diff.added) {
|
|
359
|
-
const symId = symNodeId(filePath, added.name);
|
|
467
|
+
const symId = symNodeId(change.filePath, added.name);
|
|
360
468
|
patch.addNode(symId);
|
|
361
469
|
patch.setProperty(symId, "name", added.name);
|
|
362
470
|
patch.setProperty(symId, "kind", added.kind);
|
|
@@ -366,18 +474,18 @@ export async function indexCommits(
|
|
|
366
474
|
if (added.signature !== undefined) {
|
|
367
475
|
patch.setProperty(symId, "signature", added.signature);
|
|
368
476
|
}
|
|
369
|
-
const jump = jumpLookup.get(added.name);
|
|
477
|
+
const jump = change.jumpLookup.get(added.name);
|
|
370
478
|
if (jump !== undefined) {
|
|
371
479
|
patch.setProperty(symId, "startLine", jump.start);
|
|
372
480
|
patch.setProperty(symId, "endLine", jump.end);
|
|
373
481
|
}
|
|
374
|
-
patch.addEdge(fileId, symId, "contains");
|
|
482
|
+
patch.addEdge(change.fileId, symId, "contains");
|
|
375
483
|
patch.addEdge(commitId, symId, "adds");
|
|
376
484
|
}
|
|
377
485
|
|
|
378
486
|
// Update changed symbols
|
|
379
487
|
for (const changed of diff.changed) {
|
|
380
|
-
const symId = symNodeId(filePath, changed.name);
|
|
488
|
+
const symId = symNodeId(change.filePath, changed.name);
|
|
381
489
|
patch.setProperty(symId, "kind", changed.kind);
|
|
382
490
|
if (changed.signature !== undefined) {
|
|
383
491
|
patch.setProperty(symId, "signature", changed.signature);
|
|
@@ -386,7 +494,14 @@ export async function indexCommits(
|
|
|
386
494
|
}
|
|
387
495
|
|
|
388
496
|
// Apply nested child diffs (methods in classes)
|
|
389
|
-
applyChildDiffs(
|
|
497
|
+
applyChildDiffs(
|
|
498
|
+
patch,
|
|
499
|
+
change.filePath,
|
|
500
|
+
change.fileId,
|
|
501
|
+
commitId,
|
|
502
|
+
[...diff.changed],
|
|
503
|
+
change.jumpLookup,
|
|
504
|
+
);
|
|
390
505
|
}
|
|
391
506
|
}
|
|
392
507
|
});
|
package/src/warp/observers.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function fileSymbolsLens(filePath: string): Lens {
|
|
|
37
37
|
export function allSymbolsLens(): Lens {
|
|
38
38
|
return {
|
|
39
39
|
match: "sym:*",
|
|
40
|
-
expose: ["name", "kind", "signature", "exported"],
|
|
40
|
+
expose: ["name", "kind", "signature", "exported", "startLine", "endLine"],
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
package/src/warp/open.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import WarpApp, { GitGraphAdapter } from "@git-stunts/git-warp";
|
|
10
10
|
import GitPlumbing from "@git-stunts/plumbing";
|
|
11
|
+
import { DEFAULT_WARP_WRITER_ID } from "./writer-id.js";
|
|
11
12
|
|
|
12
|
-
const GRAPH_NAME = "graft-ast";
|
|
13
|
-
const WRITER_ID = "graft";
|
|
13
|
+
export const GRAPH_NAME = "graft-ast";
|
|
14
14
|
|
|
15
15
|
export interface OpenWarpOptions {
|
|
16
16
|
readonly cwd: string;
|
|
17
|
+
readonly writerId?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export async function openWarp(options: OpenWarpOptions): Promise<WarpApp> {
|
|
@@ -24,7 +25,7 @@ export async function openWarp(options: OpenWarpOptions): Promise<WarpApp> {
|
|
|
24
25
|
return WarpApp.open({
|
|
25
26
|
persistence,
|
|
26
27
|
graphName: GRAPH_NAME,
|
|
27
|
-
writerId:
|
|
28
|
+
writerId: options.writerId ?? DEFAULT_WARP_WRITER_ID,
|
|
28
29
|
onDeleteWithData: "cascade",
|
|
29
30
|
});
|
|
30
31
|
}
|
package/src/warp/plumbing.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ declare module "@git-stunts/plumbing" {
|
|
|
6
6
|
constructor(options: { runner: unknown; cwd?: string });
|
|
7
7
|
static createDefault(options?: { cwd?: string; env?: string }): GitPlumbing;
|
|
8
8
|
execute(options: { args: string[]; input?: string | Uint8Array }): Promise<string>;
|
|
9
|
-
executeStream(options: { args: string[] }): Promise<
|
|
9
|
+
executeStream(options: { args: string[] }): Promise<{
|
|
10
|
+
finished: Promise<{ code: number; stderr: string; error?: Error }>;
|
|
11
|
+
collect(opts?: { maxBytes?: number; asString?: boolean; encoding?: string }): Promise<Uint8Array | string>;
|
|
12
|
+
[Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
|
|
13
|
+
}>;
|
|
10
14
|
}
|
|
11
15
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_WARP_WRITER_ID = "graft";
|
|
4
|
+
|
|
5
|
+
function sanitizeSegment(input: string): string {
|
|
6
|
+
const normalized = input
|
|
7
|
+
.trim()
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9_-]+/g, "_")
|
|
10
|
+
.replace(/^_+|_+$/g, "");
|
|
11
|
+
return normalized.length > 0 ? normalized : "lane";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildWarpWriterId(kind: string, scope?: string): string {
|
|
15
|
+
const lane = sanitizeSegment(kind);
|
|
16
|
+
if (scope === undefined || scope.trim().length === 0) {
|
|
17
|
+
return `graft_${lane}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const digest = crypto.createHash("sha256").update(scope).digest("hex").slice(0, 12);
|
|
21
|
+
return `graft_${lane}_${digest}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildSessionWarpWriterId(sessionId: string): string {
|
|
25
|
+
return buildWarpWriterId("session", sessionId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildMonitorWarpWriterId(repoId: string): string {
|
|
29
|
+
return buildWarpWriterId("monitor", repoId);
|
|
30
|
+
}
|