@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
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 +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { disableRestoredProjectTeam } from "./project-team-model.js";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, scrypt } from "node:crypto";
|
|
4
|
+
import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm, link, unlink } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { gzip, gunzip } from "node:zlib";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { acquireLock } from "./store.js";
|
|
9
|
+
import { disableRestoredDeferredRuns } from "./deferred-runs.js";
|
|
10
|
+
import { disableRestoredCodeHostActions } from "./code-hosts.js";
|
|
11
|
+
import { disableRestoredCoordinatedChanges } from "./repo-coordination.js";
|
|
12
|
+
import { StrictRunLogReader } from "./run-log.js";
|
|
13
|
+
import { MemoryStore } from "./memory.js";
|
|
14
|
+
const compress = promisify(gzip);
|
|
15
|
+
const decompress = promisify(gunzip);
|
|
16
|
+
const MAGIC = Buffer.from("CODEFLOW-BACKUP-1\n");
|
|
17
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
18
|
+
const MAX_MANIFEST_BYTES = Math.ceil(MAX_BYTES * 1.4);
|
|
19
|
+
const MAX_FILES = 2e4;
|
|
20
|
+
const MAX_LOG_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const sessionPath = /(^|\/)(team-connections|ssh-host|remote-worker-private|extension-cache|acp-connections\.json(?:[.-].*)?|shared-accounts\.json(?:[.-].*)?|remote\.json(?:[.-].*)?|phone-devices\.json(?:[.-].*)?|team-link\.json(?:[.-].*)?|processes\.json|sessions?\.json|(?:team[-_])?(?:credentials?|capabilit(?:y|ies))(?:\.[^/]*)?|connection\.json|[^/]*\.(?:pem|key)|\.env(?:\.[^/]*)?)(\/|$)/i;
|
|
22
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
23
|
+
const validId = (value) => /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}$/.test(value);
|
|
24
|
+
async function homePath(path) {
|
|
25
|
+
if (!(await lstat(path)).isDirectory()) throw new Error("Choose an existing code-flow data directory, without a symbolic link.");
|
|
26
|
+
return realpath(path);
|
|
27
|
+
}
|
|
28
|
+
async function offline(home, action) {
|
|
29
|
+
const path = await homePath(home);
|
|
30
|
+
const release = await acquireLock(0, path);
|
|
31
|
+
try {
|
|
32
|
+
return await action(path);
|
|
33
|
+
} finally {
|
|
34
|
+
release();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function passwordKey(password, salt) {
|
|
38
|
+
if (typeof password !== "string" || password.length < 12 || password.length > 1024) throw new Error("Use a backup password of 12 to 1,024 characters in CODEFLOW_BACKUP_PASSWORD.");
|
|
39
|
+
return new Promise((accept, reject) => scrypt(password, salt, 32, { N: 32768, r: 8, p: 1, maxmem: 128 * 1024 * 1024 }, (error, key) => error ? reject(error) : accept(key)));
|
|
40
|
+
}
|
|
41
|
+
async function syncDirectory(path) {
|
|
42
|
+
if (process.platform === "win32") return;
|
|
43
|
+
const handle = await open(path, "r");
|
|
44
|
+
try {
|
|
45
|
+
await handle.sync();
|
|
46
|
+
} finally {
|
|
47
|
+
await handle.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function writeExclusive(path, bytes) {
|
|
51
|
+
const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
const handle = await open(temp, "wx", 384);
|
|
54
|
+
try {
|
|
55
|
+
await handle.writeFile(bytes);
|
|
56
|
+
await handle.sync();
|
|
57
|
+
} finally {
|
|
58
|
+
await handle.close();
|
|
59
|
+
}
|
|
60
|
+
await link(temp, path);
|
|
61
|
+
await syncDirectory(dirname(path));
|
|
62
|
+
} finally {
|
|
63
|
+
await unlink(temp).catch(() => void 0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function createEncryptedBackup(input) {
|
|
67
|
+
const output = join(await realpath(dirname(resolve(input.output))), basename(input.output));
|
|
68
|
+
return offline(input.home, async (home) => {
|
|
69
|
+
const inside = relative(home, output);
|
|
70
|
+
if (inside !== ".." && !inside.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(inside)) throw new Error("Save backups outside the data directory.");
|
|
71
|
+
const memoryFile = join(home, "memory.sqlite");
|
|
72
|
+
const memoryExists = await lstat(memoryFile).then(() => true, (error) => error.code === "ENOENT" ? false : Promise.reject(error));
|
|
73
|
+
if (memoryExists) await new MemoryStore(memoryFile).load();
|
|
74
|
+
const files = [];
|
|
75
|
+
const omitted = [];
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
const walk = async (folder) => {
|
|
78
|
+
for (const entry of await readdir(join(home, folder), { withFileTypes: true })) {
|
|
79
|
+
const path = folder ? `${folder}/${entry.name}` : entry.name;
|
|
80
|
+
if (sessionPath.test(path)) {
|
|
81
|
+
omitted.push(`${path} (session or credential material)`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (path.startsWith("server.lock") || entry.name.endsWith(".tmp")) {
|
|
85
|
+
omitted.push(path);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (entry.isSymbolicLink()) {
|
|
89
|
+
omitted.push(`${path} (symbolic link)`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
await walk(path);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!entry.isFile()) {
|
|
97
|
+
omitted.push(`${path} (special file)`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (files.length >= MAX_FILES) throw new Error(`Backup exceeds ${MAX_FILES.toLocaleString()} files.`);
|
|
101
|
+
const handle = await open(join(home, path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
102
|
+
try {
|
|
103
|
+
const before = await handle.stat();
|
|
104
|
+
if (!before.isFile() || bytes + before.size > MAX_BYTES) throw new Error("Backup exceeds the 256 MiB data limit. Archive older runs separately first.");
|
|
105
|
+
const data = await handle.readFile();
|
|
106
|
+
const after = await handle.stat();
|
|
107
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || data.length !== before.size) throw new Error(`Data changed during backup: ${path}`);
|
|
108
|
+
bytes += data.length;
|
|
109
|
+
files.push({ path, bytes: data.length, sha256: sha256(data), data: data.toString("base64") });
|
|
110
|
+
} finally {
|
|
111
|
+
await handle.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
await walk("");
|
|
116
|
+
const manifest = { version: 1, createdAt: Date.now(), files, omitted };
|
|
117
|
+
const salt = randomBytes(16);
|
|
118
|
+
const iv = randomBytes(12);
|
|
119
|
+
const header = Buffer.concat([MAGIC, salt, iv]);
|
|
120
|
+
const cipher = createCipheriv("aes-256-gcm", await passwordKey(input.password, salt), iv);
|
|
121
|
+
cipher.setAAD(header);
|
|
122
|
+
const manifestBytes = Buffer.from(JSON.stringify(manifest));
|
|
123
|
+
if (manifestBytes.length > MAX_MANIFEST_BYTES) throw new Error("Backup metadata exceeds the archive size limit. Archive older runs separately first.");
|
|
124
|
+
const packed = await compress(manifestBytes);
|
|
125
|
+
const ciphertext = Buffer.concat([cipher.update(packed), cipher.final()]);
|
|
126
|
+
await writeExclusive(output, Buffer.concat([header, cipher.getAuthTag(), ciphertext]));
|
|
127
|
+
return { output, files: files.length, bytes, omitted, encrypted: true };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function validManifest(value) {
|
|
131
|
+
if (!value || typeof value !== "object") throw new Error("Invalid backup manifest.");
|
|
132
|
+
const manifest = value;
|
|
133
|
+
if (manifest.version !== 1 || !Array.isArray(manifest.files) || manifest.files.length > MAX_FILES || !Array.isArray(manifest.omitted) || manifest.omitted.length > MAX_FILES || manifest.omitted.some((item) => typeof item !== "string" || item.length > 8192) || !Number.isSafeInteger(manifest.createdAt) || manifest.createdAt < 0) throw new Error("Unsupported or invalid backup manifest.");
|
|
134
|
+
let bytes = 0;
|
|
135
|
+
const paths = /* @__PURE__ */ new Set();
|
|
136
|
+
for (const file of manifest.files) {
|
|
137
|
+
if (!file || typeof file.path !== "string" || file.path.length > 4096 || file.path.includes("\\") || file.path.includes("\0") || isAbsolute(file.path) || file.path.split("/").some((part) => !part || part === "." || part === "..") || file.path.startsWith("server.lock") || paths.has(file.path)) throw new Error("Backup contains an unsafe or duplicate file path.");
|
|
138
|
+
if (!Number.isSafeInteger(file.bytes) || file.bytes < 0 || typeof file.data !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(file.data) || file.data.length % 4 !== 0 || typeof file.sha256 !== "string") throw new Error("Backup contains invalid file data.");
|
|
139
|
+
const data = Buffer.from(file.data, "base64");
|
|
140
|
+
bytes += data.length;
|
|
141
|
+
if (data.length !== file.bytes || data.toString("base64") !== file.data || sha256(data) !== file.sha256 || bytes > MAX_BYTES) throw new Error("Backup file integrity or size validation failed.");
|
|
142
|
+
paths.add(file.path);
|
|
143
|
+
}
|
|
144
|
+
const privateFiles = manifest.files.filter((file) => sessionPath.test(file.path));
|
|
145
|
+
return { ...manifest, files: manifest.files.filter((file) => !sessionPath.test(file.path)), omitted: [...manifest.omitted, ...privateFiles.map((file) => `${file.path} (session or credential material excluded from restore)`)] };
|
|
146
|
+
}
|
|
147
|
+
async function restoreEncryptedBackup(input) {
|
|
148
|
+
const source = await lstat(input.input);
|
|
149
|
+
if (!source.isFile() || source.size > MAX_MANIFEST_BYTES + 1024 || source.size < MAGIC.length + 45) throw new Error("Choose a valid backup file under 360 MiB.");
|
|
150
|
+
const archive = await readFile(input.input);
|
|
151
|
+
if (!archive.subarray(0, MAGIC.length).equals(MAGIC)) throw new Error("Unsupported backup format.");
|
|
152
|
+
const salt = archive.subarray(MAGIC.length, MAGIC.length + 16);
|
|
153
|
+
const iv = archive.subarray(MAGIC.length + 16, MAGIC.length + 28);
|
|
154
|
+
const headerEnd = MAGIC.length + 28;
|
|
155
|
+
const decipher = createDecipheriv("aes-256-gcm", await passwordKey(input.password, salt), iv);
|
|
156
|
+
decipher.setAAD(archive.subarray(0, headerEnd));
|
|
157
|
+
decipher.setAuthTag(archive.subarray(headerEnd, headerEnd + 16));
|
|
158
|
+
let packed;
|
|
159
|
+
try {
|
|
160
|
+
packed = Buffer.concat([decipher.update(archive.subarray(headerEnd + 16)), decipher.final()]);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error("Backup authentication failed. Check the password and that the file is intact.");
|
|
163
|
+
}
|
|
164
|
+
let manifest;
|
|
165
|
+
try {
|
|
166
|
+
manifest = validManifest(JSON.parse((await decompress(packed, { maxOutputLength: MAX_MANIFEST_BYTES })).toString("utf8")));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new Error(`Cannot restore backup: ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
const destination = resolve(input.destination);
|
|
171
|
+
if (await lstat(destination).then(() => true, (error) => error.code === "ENOENT" ? false : Promise.reject(error))) throw new Error("Restore destination already exists. Choose a new directory.");
|
|
172
|
+
const parent = await realpath(dirname(destination));
|
|
173
|
+
const finalPath = join(parent, basename(destination));
|
|
174
|
+
const staging = join(parent, `.${basename(destination)}.restore-${randomBytes(8).toString("hex")}`);
|
|
175
|
+
await mkdir(staging, { mode: 448 });
|
|
176
|
+
let disabledAutomations = 0;
|
|
177
|
+
let disabledNotifications = 0;
|
|
178
|
+
let cancelledDeferredRuns = 0;
|
|
179
|
+
let cancelledCoordinatedChanges = 0;
|
|
180
|
+
let restoredBytes = 0;
|
|
181
|
+
try {
|
|
182
|
+
for (const file of manifest.files) {
|
|
183
|
+
const target = join(staging, file.path);
|
|
184
|
+
await mkdir(dirname(target), { recursive: true, mode: 448 });
|
|
185
|
+
let data = Buffer.from(file.data, "base64");
|
|
186
|
+
if (/^project-teams\/pt-[a-f0-9]{24}\.json$/.test(file.path)) data = Buffer.from(JSON.stringify(disableRestoredProjectTeam(JSON.parse(data.toString("utf8")))));
|
|
187
|
+
if (file.path === "triggers.json") {
|
|
188
|
+
const triggers = JSON.parse(data.toString("utf8"));
|
|
189
|
+
if (!Array.isArray(triggers) || triggers.some((trigger) => !trigger || typeof trigger !== "object" || Array.isArray(trigger))) throw new Error("Cannot restore invalid automation settings.");
|
|
190
|
+
disabledAutomations = triggers.length;
|
|
191
|
+
data = Buffer.from(JSON.stringify(triggers.map((trigger) => ({ ...trigger, enabled: false, token: randomBytes(24).toString("hex"), nextAt: void 0, queued: [], webhookStarts: [] })), null, 2));
|
|
192
|
+
}
|
|
193
|
+
if (file.path === "notifications.json") {
|
|
194
|
+
const notifications = JSON.parse(data.toString("utf8"));
|
|
195
|
+
if (!notifications || notifications.version !== 1 || !notifications.settings || typeof notifications.settings !== "object" || Array.isArray(notifications.settings) || !Array.isArray(notifications.deliveries)) throw new Error("Cannot restore invalid notification settings.");
|
|
196
|
+
for (const settings of Object.values(notifications.settings)) {
|
|
197
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error("Cannot restore invalid notification settings.");
|
|
198
|
+
settings.enabled = false;
|
|
199
|
+
disabledNotifications++;
|
|
200
|
+
}
|
|
201
|
+
notifications.deliveries = notifications.deliveries.map((delivery) => {
|
|
202
|
+
if (!delivery || typeof delivery !== "object") throw new Error("Cannot restore invalid notification deliveries.");
|
|
203
|
+
return ["pending", "sending"].includes(String(delivery.state)) ? { ...delivery, state: "cancelled", nextAttemptAt: void 0, updatedAt: Date.now(), reason: "Cancelled during backup restore; deliberately re-enable notifications after reviewing destinations." } : delivery;
|
|
204
|
+
});
|
|
205
|
+
data = Buffer.from(JSON.stringify(notifications, null, 2));
|
|
206
|
+
}
|
|
207
|
+
if (file.path === "deferred-runs.json") {
|
|
208
|
+
const restored = disableRestoredDeferredRuns(JSON.parse(data.toString("utf8")));
|
|
209
|
+
cancelledDeferredRuns = restored.cancelled;
|
|
210
|
+
data = Buffer.from(JSON.stringify(restored.document, null, 2));
|
|
211
|
+
}
|
|
212
|
+
if (file.path === "coordinated-changes.json") {
|
|
213
|
+
const restored = disableRestoredCoordinatedChanges(JSON.parse(data.toString("utf8")));
|
|
214
|
+
cancelledCoordinatedChanges = restored.cancelled;
|
|
215
|
+
data = Buffer.from(JSON.stringify(restored.document, null, 2));
|
|
216
|
+
}
|
|
217
|
+
if (file.path === "code-host-operations.json") data = Buffer.from(JSON.stringify(disableRestoredCodeHostActions(JSON.parse(data.toString("utf8"))), null, 2));
|
|
218
|
+
const handle = await open(target, "wx", 384);
|
|
219
|
+
try {
|
|
220
|
+
await handle.writeFile(data);
|
|
221
|
+
await handle.sync();
|
|
222
|
+
} finally {
|
|
223
|
+
await handle.close();
|
|
224
|
+
}
|
|
225
|
+
await syncDirectory(dirname(target));
|
|
226
|
+
restoredBytes += data.length;
|
|
227
|
+
}
|
|
228
|
+
if (await lstat(finalPath).then(() => true, (error) => error.code === "ENOENT" ? false : Promise.reject(error))) throw new Error("Restore destination was created during restore. Choose a new directory.");
|
|
229
|
+
await syncDirectory(staging);
|
|
230
|
+
await rename(staging, finalPath);
|
|
231
|
+
await syncDirectory(parent);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
await rm(staging, { recursive: true, force: true });
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
return { destination: finalPath, files: manifest.files.length, bytes: restoredBytes, createdAt: manifest.createdAt, omitted: manifest.omitted, disabledAutomations, disabledNotifications, cancelledDeferredRuns, cancelledCoordinatedChanges };
|
|
237
|
+
}
|
|
238
|
+
async function readRunLog(home, id) {
|
|
239
|
+
if (!validId(id)) throw new Error("Choose a valid run id.");
|
|
240
|
+
if (!(await lstat(join(home, "runs"))).isDirectory()) throw new Error("Run storage must be a regular directory without a symbolic link.");
|
|
241
|
+
const path = join(home, "runs", `${id}.jsonl`);
|
|
242
|
+
const info = await lstat(path);
|
|
243
|
+
if (!info.isFile() || info.size > MAX_LOG_BYTES) throw new Error("Only regular run logs up to 64 MiB can be maintained by this command.");
|
|
244
|
+
const text = await readFile(path, "utf8");
|
|
245
|
+
if (!text.endsWith("\n")) throw new Error("Run log has an incomplete last record; repair or recover it before maintenance.");
|
|
246
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
247
|
+
const events = lines.map((line) => JSON.parse(line));
|
|
248
|
+
if (events.some((event) => !event || typeof event !== "object" || Array.isArray(event) || event.type === "run" && event.run?.id !== id)) throw new Error("Run log contains invalid or mismatched records.");
|
|
249
|
+
const reader = new StrictRunLogReader(id);
|
|
250
|
+
let snapshot;
|
|
251
|
+
for (const value of events) {
|
|
252
|
+
const event = reader.decode(value);
|
|
253
|
+
if (event?.type === "run") snapshot = event.run;
|
|
254
|
+
}
|
|
255
|
+
if (!snapshot || !["done", "failed", "cancelled"].includes(snapshot.status)) throw new Error("Only completed, failed or cancelled runs can be maintained. Resumable runs are kept.");
|
|
256
|
+
return { text, lines, events, snapshot, bytes: info.size, mtimeMs: info.mtimeMs };
|
|
257
|
+
}
|
|
258
|
+
async function retentionPlan(home, days, now) {
|
|
259
|
+
if (!Number.isInteger(days) || days < 1 || days > 36500 || !Number.isFinite(now)) throw new Error("Retention days must be an integer from 1 to 36,500.");
|
|
260
|
+
const result = { days, cutoff: now - days * 864e5, candidates: [], skipped: [] };
|
|
261
|
+
const runDirectory = await lstat(join(home, "runs")).catch((error) => error.code === "ENOENT" ? void 0 : Promise.reject(error));
|
|
262
|
+
if (!runDirectory) return result;
|
|
263
|
+
if (!runDirectory.isDirectory()) throw new Error("Run storage must be a regular directory without a symbolic link.");
|
|
264
|
+
const entries = await readdir(join(home, "runs")).catch((error) => error.code === "ENOENT" ? [] : Promise.reject(error));
|
|
265
|
+
for (const file of entries.filter((file2) => file2.endsWith(".jsonl") && !file2.endsWith(".journal.jsonl"))) {
|
|
266
|
+
const id = file.slice(0, -6);
|
|
267
|
+
try {
|
|
268
|
+
const log = await readRunLog(home, id);
|
|
269
|
+
if (log.snapshot.archived !== true || log.mtimeMs >= result.cutoff) continue;
|
|
270
|
+
const files = [];
|
|
271
|
+
let bytes = 0;
|
|
272
|
+
for (const name of [`${id}.jsonl`, `${id}.journal.jsonl`, `${id}.checkpoint.json`, `${id}.cold-index.json`, `${id}.cold-detail.json`]) {
|
|
273
|
+
const info = await lstat(join(home, "runs", name)).catch((error) => error.code === "ENOENT" ? void 0 : Promise.reject(error));
|
|
274
|
+
if (info?.isFile()) {
|
|
275
|
+
files.push(name);
|
|
276
|
+
bytes += info.size;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
result.candidates.push({ id, files, bytes });
|
|
280
|
+
} catch (error) {
|
|
281
|
+
result.skipped.push({ id, reason: error.message });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
function retainArchivedRuns(input) {
|
|
287
|
+
return offline(input.home, async (home) => {
|
|
288
|
+
const plan = await retentionPlan(home, input.days, input.now ?? Date.now());
|
|
289
|
+
if (input.apply) throw new Error("Age-only retention cannot delete data. Save a retention policy, review retention-policy --output PLAN.json, then apply that exact plan with retention-policy --plan PLAN.json --apply.");
|
|
290
|
+
return { ...plan, applied: false, deletedFiles: 0, nextStep: "This legacy age-only report does not assess pins or provenance. Save a retention policy and review retention-policy --output PLAN.json before applying retention-policy --plan PLAN.json --apply." };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function compactRunLog(input) {
|
|
294
|
+
return offline(input.home, async (home) => {
|
|
295
|
+
const log = await readRunLog(home, input.runId);
|
|
296
|
+
const snapshots = log.events.flatMap((event, index) => event?.type === "run" || event?.type === "run-patch" ? [index] : []);
|
|
297
|
+
const removable = new Set(snapshots.slice(1, -1));
|
|
298
|
+
const last = snapshots.at(-1), lastEvent = last === void 0 ? void 0 : log.events[last];
|
|
299
|
+
const finalLine = removable.size && lastEvent?.type === "run-patch" ? JSON.stringify({ seq: lastEvent.seq, ts: lastEvent.ts, runId: input.runId, type: "run", run: log.snapshot }) : void 0;
|
|
300
|
+
let text = log.lines.flatMap((line, index) => removable.has(index) ? [] : [index === last && finalLine ? finalLine : line]).join("\n") + "\n";
|
|
301
|
+
let afterBytes = Buffer.byteLength(text);
|
|
302
|
+
if (afterBytes >= log.bytes) {
|
|
303
|
+
removable.clear();
|
|
304
|
+
text = log.text;
|
|
305
|
+
afterBytes = log.bytes;
|
|
306
|
+
}
|
|
307
|
+
if (input.apply && removable.size) {
|
|
308
|
+
const path = join(home, "runs", `${input.runId}.jsonl`);
|
|
309
|
+
const temp = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
310
|
+
try {
|
|
311
|
+
const handle = await open(temp, "wx", 384);
|
|
312
|
+
try {
|
|
313
|
+
await handle.writeFile(text);
|
|
314
|
+
await handle.sync();
|
|
315
|
+
} finally {
|
|
316
|
+
await handle.close();
|
|
317
|
+
}
|
|
318
|
+
const current = await lstat(path);
|
|
319
|
+
if (current.size !== log.bytes || current.mtimeMs !== log.mtimeMs || !current.isFile()) throw new Error("Run log changed during compaction; no change was applied.");
|
|
320
|
+
await rename(temp, path);
|
|
321
|
+
await syncDirectory(dirname(path));
|
|
322
|
+
for (const suffix of [".checkpoint.json", ".cold-index.json", ".cold-detail.json"]) await unlink(join(home, "runs", input.runId + suffix)).catch(() => void 0);
|
|
323
|
+
} finally {
|
|
324
|
+
await unlink(temp).catch(() => void 0);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { runId: input.runId, applied: input.apply === true, snapshotsRemoved: removable.size, beforeBytes: log.bytes, afterBytes, savedBytes: log.bytes - afterBytes };
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
compactRunLog,
|
|
332
|
+
createEncryptedBackup,
|
|
333
|
+
restoreEncryptedBackup,
|
|
334
|
+
retainArchivedRuns
|
|
335
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { git } from "./git.js";
|
|
4
|
+
import { analyzeMemoryClaims } from "./memory-claims.js";
|
|
5
|
+
const LIMITS = { entries: 500, references: 5e3, paths: 1e3, commitBaselines: 16, comparedPairs: 5e4, duplicatePairs: 100, onboardingEntries: 12, onboardingChars: 12e3 };
|
|
6
|
+
const RETENTION = "Deleting erases this entry's text and stops its future retrieval. Derived entries retain their own text and provenance. Source run transcripts, provider conversations and backups have separate retention.";
|
|
7
|
+
const ref = (entry) => ({ id: entry.id, revision: entry.revision });
|
|
8
|
+
const key = (entry) => `${entry.id}@${entry.revision}`;
|
|
9
|
+
const normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
10
|
+
const digest = (text) => createHash("sha256").update(normalize(text)).digest("hex");
|
|
11
|
+
async function analyzeProjectMemory(input) {
|
|
12
|
+
const scoped = input.entries.filter((entry) => entry.scope === "project" && entry.repo === input.repo && entry.projectId === input.projectId && entry.status !== "deleted").sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
|
|
13
|
+
const entries = scoped.slice(0, LIMITS.entries);
|
|
14
|
+
const now = input.now ?? Date.now();
|
|
15
|
+
const result = {
|
|
16
|
+
scope: { repo: input.repo, projectId: input.projectId },
|
|
17
|
+
git: { available: false },
|
|
18
|
+
graph: { nodes: [], edges: [] },
|
|
19
|
+
deletionImpact: [],
|
|
20
|
+
stale: [],
|
|
21
|
+
duplicates: [],
|
|
22
|
+
claims: analyzeMemoryClaims({ repo: input.repo, projectId: input.projectId, entries }),
|
|
23
|
+
onboarding: { title: `Project onboarding \xB7 ${basename(input.repo)}`.slice(0, 200), body: "", sourceRefs: [], omitted: 0, reasons: [] },
|
|
24
|
+
limits: { ...LIMITS },
|
|
25
|
+
truncated: entries.length !== scoped.length,
|
|
26
|
+
notes: []
|
|
27
|
+
};
|
|
28
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
29
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
30
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
31
|
+
for (const entry of entries) nodes.set(key(entry), { key: key(entry), ...ref(entry), title: entry.title, status: entry.status, available: true });
|
|
32
|
+
let references = 0;
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
for (const source of entry.derivedFrom) {
|
|
35
|
+
if (references++ >= LIMITS.references) {
|
|
36
|
+
result.truncated = true;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (!nodes.has(key(source))) nodes.set(key(source), { key: key(source), ...ref(source), available: false });
|
|
40
|
+
result.graph.edges.push({ from: key(source), to: key(entry) });
|
|
41
|
+
const derived = reverse.get(source.id) ?? /* @__PURE__ */ new Set();
|
|
42
|
+
derived.add(entry.id);
|
|
43
|
+
reverse.set(source.id, derived);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
result.graph.nodes = [...nodes.values()];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const descendants = /* @__PURE__ */ new Set();
|
|
49
|
+
const queue = [...reverse.get(entry.id) ?? []];
|
|
50
|
+
for (let index = 0; index < queue.length; index++) {
|
|
51
|
+
const id = queue[index];
|
|
52
|
+
if (id === entry.id || descendants.has(id)) continue;
|
|
53
|
+
descendants.add(id);
|
|
54
|
+
queue.push(...reverse.get(id) ?? []);
|
|
55
|
+
}
|
|
56
|
+
const refs = (ids) => [...ids].flatMap((id) => byId.has(id) ? [ref(byId.get(id))] : []).sort((a, b) => a.id.localeCompare(b.id));
|
|
57
|
+
result.deletionImpact.push({ ...ref(entry), direct: refs(reverse.get(entry.id) ?? []), transitive: refs(descendants), retained: RETENTION });
|
|
58
|
+
}
|
|
59
|
+
const stale = /* @__PURE__ */ new Map();
|
|
60
|
+
const incomplete = /* @__PURE__ */ new Set();
|
|
61
|
+
const add = (entry, kind, detail) => {
|
|
62
|
+
const current = stale.get(entry.id) ?? { ...ref(entry), reasons: [] };
|
|
63
|
+
current.reasons.push({ kind, detail });
|
|
64
|
+
stale.set(entry.id, current);
|
|
65
|
+
};
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) add(entry, "expired", "The entry's expiry has passed.");
|
|
68
|
+
for (const feedback of entry.feedback ?? []) if (feedback.revision === entry.revision && ["stale", "incorrect"].includes(feedback.rating)) add(entry, "feedback", `You marked this revision ${feedback.rating}${feedback.note ? `: ${feedback.note}` : "."}`);
|
|
69
|
+
}
|
|
70
|
+
const readGit = (args) => git(["--literal-pathspecs", ...args], input.repo, { signal: input.signal, timeoutMs: 2e3 });
|
|
71
|
+
const [headResult, branchResult] = await Promise.all([
|
|
72
|
+
readGit(["rev-parse", "--verify", "--end-of-options", `${input.head ?? "HEAD"}^{commit}`]),
|
|
73
|
+
input.branch ? Promise.resolve({ code: 0, stdout: input.branch }) : readGit(["symbolic-ref", "--quiet", "--short", "HEAD"])
|
|
74
|
+
]);
|
|
75
|
+
const head = headResult.code === 0 && /^[a-f0-9]{40,64}$/.test(headResult.stdout.trim()) ? headResult.stdout.trim() : void 0;
|
|
76
|
+
const branch = branchResult.code === 0 ? branchResult.stdout.trim() : void 0;
|
|
77
|
+
result.git = { ...head ? { head } : {}, ...branch ? { branch } : {}, available: !!head };
|
|
78
|
+
if (!head) {
|
|
79
|
+
result.notes.push("Git evidence is unavailable; only expiry and revision feedback were evaluated.");
|
|
80
|
+
for (const entry of entries) if (entry.paths.length || entry.verification?.commit || entry.applicability.kind === "branch" && entry.applicability.commit) incomplete.add(entry.id);
|
|
81
|
+
}
|
|
82
|
+
if (head) {
|
|
83
|
+
const allPaths = [...new Set(entries.flatMap((entry) => entry.paths))];
|
|
84
|
+
const paths = allPaths.slice(0, LIMITS.paths);
|
|
85
|
+
const checkedPaths = new Set(paths);
|
|
86
|
+
if (paths.length !== allPaths.length) result.truncated = true;
|
|
87
|
+
const tree = paths.length ? await readGit(["ls-tree", "--name-only", "-r", "-z", head, "--", ...paths]) : void 0;
|
|
88
|
+
const listed = tree?.code === 0 ? tree.stdout.split("\0").filter(Boolean) : void 0;
|
|
89
|
+
const present = listed && listed.length <= 2e4 ? new Set(listed) : void 0;
|
|
90
|
+
if (listed && !present) result.truncated = true;
|
|
91
|
+
if (present) for (const file of [...present]) {
|
|
92
|
+
const parts = file.split("/");
|
|
93
|
+
for (let index = 1; index < parts.length; index++) present.add(parts.slice(0, index).join("/"));
|
|
94
|
+
}
|
|
95
|
+
if (tree && !present) result.notes.push("Path existence could not be checked at the selected commit.");
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (entry.paths.some((path) => !checkedPaths.has(path)) || entry.paths.length && !present) incomplete.add(entry.id);
|
|
98
|
+
if (entry.applicability.kind === "branch" && branch && entry.applicability.branch !== branch) add(entry, "branch-mismatch", `This entry applies to ${entry.applicability.branch}; the selected branch is ${branch}.`);
|
|
99
|
+
if (present) for (const path of entry.paths) {
|
|
100
|
+
if (!checkedPaths.has(path)) continue;
|
|
101
|
+
if (!present.has(path.replace(/\/$/, ""))) add(entry, "missing-path", `${path} is absent at ${head.slice(0, 12)}.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const baselines = [...new Set(entries.flatMap((entry) => [entry.verification?.commit, entry.applicability.kind === "branch" ? entry.applicability.commit : void 0]).filter((value) => !!value))];
|
|
105
|
+
const checkedBaselines = new Set(baselines.slice(0, LIMITS.commitBaselines));
|
|
106
|
+
if (baselines.length > LIMITS.commitBaselines) result.truncated = true;
|
|
107
|
+
for (const entry of entries) if (entry.verification?.commit && !checkedBaselines.has(entry.verification.commit) || entry.applicability.kind === "branch" && entry.applicability.commit && !checkedBaselines.has(entry.applicability.commit)) incomplete.add(entry.id);
|
|
108
|
+
for (let index = 0; index < Math.min(baselines.length, LIMITS.commitBaselines); index += 4) {
|
|
109
|
+
const observed = await Promise.all(baselines.slice(index, Math.min(index + 4, LIMITS.commitBaselines)).map(async (base) => {
|
|
110
|
+
const exists = /^[a-f0-9]{40,64}$/i.test(base) ? await readGit(["cat-file", "-e", `${base}^{commit}`]) : { code: 1 };
|
|
111
|
+
if (exists.code !== 0) return { base, state: "missing" };
|
|
112
|
+
const ancestor = await readGit(["merge-base", "--is-ancestor", base, head]);
|
|
113
|
+
if (ancestor.code === 1) return { base, state: "diverged" };
|
|
114
|
+
if (ancestor.code !== 0) return { base, state: "unknown" };
|
|
115
|
+
const diff = paths.length ? await readGit(["diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", base, head, "--", ...paths]) : void 0;
|
|
116
|
+
return { base, state: "ancestor", changed: diff?.code === 0 ? diff.stdout.split("\0").filter(Boolean) : [], incomplete: !!diff && diff.code !== 0 };
|
|
117
|
+
}));
|
|
118
|
+
for (const observation of observed) for (const entry of entries) {
|
|
119
|
+
if (entry.verification?.commit !== observation.base && (entry.applicability.kind !== "branch" || entry.applicability.commit !== observation.base)) continue;
|
|
120
|
+
if (observation.state === "missing") add(entry, "commit-unavailable", `Recorded commit ${observation.base.slice(0, 12)} is unavailable.`);
|
|
121
|
+
else if (observation.state === "diverged") add(entry, "commit-not-ancestor", `Recorded commit ${observation.base.slice(0, 12)} is not an ancestor of the selected checkout.`);
|
|
122
|
+
else if (observation.state === "ancestor") {
|
|
123
|
+
if (observation.incomplete) incomplete.add(entry.id);
|
|
124
|
+
const changed = observation.changed.filter((path) => entry.paths.some((scope) => path === scope || path.startsWith(scope.replace(/\/$/, "") + "/")));
|
|
125
|
+
if (changed.length) add(entry, "paths-changed", `Relevant paths changed since ${observation.base.slice(0, 12)}: ${changed.slice(0, 8).join(", ")}${changed.length > 8 ? ", \u2026" : ""}. Review this guidance; a code change does not prove it incorrect.`);
|
|
126
|
+
} else incomplete.add(entry.id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
result.stale = [...stale.values()];
|
|
131
|
+
const words = (text) => new Set((text.toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) ?? []).filter((word) => !STOP_WORDS.has(word)).slice(0, 200));
|
|
132
|
+
const bodies = entries.map((entry) => ({ hash: digest(entry.body), words: words(entry.body) }));
|
|
133
|
+
let pairs = 0;
|
|
134
|
+
outer: for (let a = 0; a < entries.length; a++) for (let b = a + 1; b < entries.length; b++) {
|
|
135
|
+
if (++pairs > LIMITS.comparedPairs || result.duplicates.length >= LIMITS.duplicatePairs) {
|
|
136
|
+
result.truncated = true;
|
|
137
|
+
break outer;
|
|
138
|
+
}
|
|
139
|
+
if (bodies[a].hash === bodies[b].hash) result.duplicates.push({ a: ref(entries[a]), b: ref(entries[b]), kind: "exact", similarity: 1 });
|
|
140
|
+
else {
|
|
141
|
+
const left = bodies[a].words, right = bodies[b].words;
|
|
142
|
+
const overlap = [...left].filter((word) => right.has(word)).length;
|
|
143
|
+
const similarity = overlap / (left.size + right.size - overlap || 1);
|
|
144
|
+
if (overlap >= 5 && similarity >= 0.65) result.duplicates.push({ a: ref(entries[a]), b: ref(entries[b]), kind: "lexical-overlap", similarity: Math.round(similarity * 1e3) / 1e3 });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
result.notes.push("Lexical overlap and supported semantic claims are separate review hints. Claim analysis is bounded to its explicit grammar and never decides which guidance is correct.");
|
|
148
|
+
result.notes.push("Path changes require a recorded commit baseline. User guidance without one is checked for missing paths, expiry and feedback only.");
|
|
149
|
+
const onboarding = ["# Project onboarding", "Curated from accepted guidance written by you. Review the sources and applicability before using it."];
|
|
150
|
+
let size = onboarding.join("\n\n").length;
|
|
151
|
+
const curated = [...entries].sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
|
|
152
|
+
for (const entry of curated) {
|
|
153
|
+
const reason = entry.status !== "accepted" || entry.acceptedBy !== "user" ? "Not accepted by you" : !entry.origins.length || entry.origins.some((origin) => origin.kind !== "user" || origin.trust !== "trusted" || origin.lockedDown || origin.private) ? "Includes run, imported, private or untrusted origins" : stale.has(entry.id) ? "Needs applicability or freshness review" : entry.applicability.kind === "branch" && !branch ? "Selected branch is unknown" : incomplete.has(entry.id) ? "Freshness checks were unavailable or reached a limit" : entry.tags.includes("onboarding-summary") ? "Already an onboarding summary" : void 0;
|
|
154
|
+
if (reason) {
|
|
155
|
+
result.onboarding.reasons.push({ id: entry.id, reason });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const scope = entry.applicability.kind === "branch" ? `
|
|
159
|
+
|
|
160
|
+
Applies to branch ${entry.applicability.branch}${entry.applicability.commit ? ` at ${entry.applicability.commit}` : ""}.` : "";
|
|
161
|
+
const section = `## ${entry.title}
|
|
162
|
+
|
|
163
|
+
${entry.body}${scope}
|
|
164
|
+
|
|
165
|
+
Source: ${entry.id} (revision ${entry.revision}).`;
|
|
166
|
+
if (result.onboarding.sourceRefs.length >= LIMITS.onboardingEntries || size + section.length + 2 > LIMITS.onboardingChars) {
|
|
167
|
+
result.onboarding.reasons.push({ id: entry.id, reason: "Onboarding summary size limit" });
|
|
168
|
+
result.truncated = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
onboarding.push(section);
|
|
172
|
+
size += section.length + 2;
|
|
173
|
+
result.onboarding.sourceRefs.push(ref(entry));
|
|
174
|
+
}
|
|
175
|
+
result.onboarding.body = result.onboarding.sourceRefs.length ? onboarding.join("\n\n") : "";
|
|
176
|
+
result.onboarding.omitted = scoped.length - result.onboarding.sourceRefs.length;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
const STOP_WORDS = /* @__PURE__ */ new Set(["the", "and", "that", "this", "with", "from", "for", "before", "after", "should", "must", "use", "using", "when", "into", "are", "you", "your"]);
|
|
180
|
+
export {
|
|
181
|
+
analyzeProjectMemory
|
|
182
|
+
};
|