@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
package/src/memory.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbsolute, join, normalize, resolve } from "node:path";
|
|
3
|
+
import { MemoryDatabase } from "./memory-sqlite.js";
|
|
4
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
5
|
+
const MAX_ENTRIES = 5e3;
|
|
6
|
+
const MAX_RUN_ENTRIES = 512;
|
|
7
|
+
const MAX_BODY = 16e3;
|
|
8
|
+
const MAX_FILE = 32 * 1024 * 1024;
|
|
9
|
+
const queues = /* @__PURE__ */ new Map();
|
|
10
|
+
class MemoryConflictError extends Error {
|
|
11
|
+
constructor(current2) {
|
|
12
|
+
super(`Memory revision conflict: ${current2.id} is at revision ${current2.revision}`);
|
|
13
|
+
this.current = current2;
|
|
14
|
+
this.name = "MemoryConflictError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class MemoryStore {
|
|
18
|
+
file;
|
|
19
|
+
database;
|
|
20
|
+
constructor(file = join(CODEFLOW_HOME, "memory.sqlite")) {
|
|
21
|
+
this.file = resolve(file.replace(/\.json$/, ".sqlite"));
|
|
22
|
+
this.database = new MemoryDatabase(this.file, validateDocument);
|
|
23
|
+
}
|
|
24
|
+
async load() {
|
|
25
|
+
await this.read();
|
|
26
|
+
}
|
|
27
|
+
async propose(raw, authority, options = {}) {
|
|
28
|
+
return this.change((doc) => {
|
|
29
|
+
const input = validateInput(raw);
|
|
30
|
+
if (authority.kind === "run" && input.scope === "project") input.runId ??= authority.runId;
|
|
31
|
+
authorize(input, authority, true);
|
|
32
|
+
const operationHash = hashOperation(input, authority);
|
|
33
|
+
if (input.operationId) {
|
|
34
|
+
const previous = doc.entries.find((e) => e.repo === input.repo && e.projectId === input.projectId && e.operationId === input.operationId);
|
|
35
|
+
if (previous) {
|
|
36
|
+
authorize(previous, authority, true);
|
|
37
|
+
if (previous.operationHash !== operationHash) throw new Error("Memory operation ID was already used for different content");
|
|
38
|
+
return clone(previous);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (options.expectedSources !== void 0) {
|
|
42
|
+
const expected = uniqueRefs(options.expectedSources);
|
|
43
|
+
if (JSON.stringify(expected) !== JSON.stringify(input.derivedFrom ?? [])) throw new Error("Guarded publication must identify every derivation source");
|
|
44
|
+
for (const sourceRef of expected) {
|
|
45
|
+
const source = doc.entries.find((entry2) => entry2.id === sourceRef.id);
|
|
46
|
+
if (!source) throw new Error("Memory publication source does not exist");
|
|
47
|
+
authorize(source, authority, false);
|
|
48
|
+
if (source.status === "deleted" || source.revision !== sourceRef.revision || source.expiresAt !== void 0 && source.expiresAt <= Date.now() || source.feedback?.some((item) => item.revision === source.revision && (item.rating === "stale" || item.rating === "incorrect"))) throw new MemoryConflictError(clone(source));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const live = doc.entries.filter((entry2) => entry2.status !== "deleted");
|
|
52
|
+
if (input.scope === "project" && live.filter((entry2) => entry2.scope === "project").length >= MAX_ENTRIES) throw new Error("Memory limit reached; remove older records before adding more");
|
|
53
|
+
if (input.scope !== "project" && live.filter((entry2) => entry2.scope !== "project" && entry2.runId === input.runId).length >= MAX_RUN_ENTRIES) throw new Error(`This run has reached its limit of ${MAX_RUN_ENTRIES} private and team memory records`);
|
|
54
|
+
const sources = sourceEntries(doc, input.derivedFrom ?? [], input, authority);
|
|
55
|
+
const origins = unionOrigins([origin(authority, input.scope), ...sources.flatMap((e) => e.origins)]);
|
|
56
|
+
preventPrivatePublication(input.scope, origins, authority);
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const accepted = authority.kind === "user" || authority.kind === "run" && input.scope === "private";
|
|
59
|
+
const entry = {
|
|
60
|
+
...input,
|
|
61
|
+
id: `m-${randomUUID()}`,
|
|
62
|
+
revision: 1,
|
|
63
|
+
status: accepted ? "accepted" : "proposed",
|
|
64
|
+
tags: input.tags ?? [],
|
|
65
|
+
paths: input.paths ?? [],
|
|
66
|
+
applicability: input.applicability ?? { kind: "repository" },
|
|
67
|
+
derivedFrom: input.derivedFrom ?? [],
|
|
68
|
+
sourceRefs: input.sourceRefs ?? [],
|
|
69
|
+
origins,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
...accepted ? { acceptedBy: authority.kind === "user" ? "user" : "owner" } : {},
|
|
73
|
+
...input.operationId ? { operationHash } : {}
|
|
74
|
+
};
|
|
75
|
+
doc.entries.push(entry);
|
|
76
|
+
return clone(entry);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async revise(id, expectedRevision, patch, authority, options = {}) {
|
|
80
|
+
return this.change((doc) => {
|
|
81
|
+
const entry = current(doc, id, expectedRevision, authority, true);
|
|
82
|
+
const input = validateInput({ ...entry, ...patch });
|
|
83
|
+
input.derivedFrom = uniqueRefs([...entry.derivedFrom, ...input.derivedFrom ?? []]);
|
|
84
|
+
if (input.derivedFrom.some((ref) => ref.id === entry.id)) throw new Error("Memory cannot derive from itself");
|
|
85
|
+
const added = input.derivedFrom.filter((ref) => !entry.derivedFrom.some((old) => old.id === ref.id && old.revision === ref.revision));
|
|
86
|
+
const sources = sourceEntries(doc, added, input, authority);
|
|
87
|
+
const origins = unionOrigins([...entry.origins, origin(authority, entry.scope), ...sources.flatMap((e) => e.origins)]);
|
|
88
|
+
preventPrivatePublication(entry.scope, origins, authority);
|
|
89
|
+
Object.assign(entry, input, { origins });
|
|
90
|
+
if ("expiresAt" in patch && patch.expiresAt === void 0) delete entry.expiresAt;
|
|
91
|
+
if (entry.scope === "private") {
|
|
92
|
+
entry.status = "accepted";
|
|
93
|
+
entry.acceptedBy = "owner";
|
|
94
|
+
} else if (authority.kind === "user") {
|
|
95
|
+
if (options.accept || entry.status === "accepted") {
|
|
96
|
+
entry.status = "accepted";
|
|
97
|
+
entry.acceptedBy = "user";
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
entry.status = "proposed";
|
|
101
|
+
entry.acceptedBy = void 0;
|
|
102
|
+
}
|
|
103
|
+
delete entry.verification;
|
|
104
|
+
touch(entry);
|
|
105
|
+
return clone(entry);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Approve this exact revision. Tainted project content always requires the user. */
|
|
109
|
+
async accept(id, expectedRevision, authority, verification, operationId) {
|
|
110
|
+
return this.change((doc) => {
|
|
111
|
+
const entry = doc.entries.find((candidate) => candidate.id === id);
|
|
112
|
+
if (!entry || entry.status === "deleted") throw new Error("Memory entry not found");
|
|
113
|
+
authorize(entry, authority, true);
|
|
114
|
+
const operation = operationId === void 0 ? void 0 : text(operationId, 500, "acceptance operation ID");
|
|
115
|
+
const fingerprint = createHash("sha256").update(canonicalJson({ expectedRevision, authority, verification })).digest("hex");
|
|
116
|
+
const previous = operation ? entry.acceptanceOperations?.find((saved) => saved.id === operation) : void 0;
|
|
117
|
+
if (previous) {
|
|
118
|
+
if (previous.fingerprint !== fingerprint) throw new Error("Memory acceptance operation ID was already used for a different approval");
|
|
119
|
+
if (entry.revision !== previous.resultRevision) throw new MemoryConflictError(clone(entry));
|
|
120
|
+
return clone(entry);
|
|
121
|
+
}
|
|
122
|
+
if (entry.revision !== expectedRevision) throw new MemoryConflictError(clone(entry));
|
|
123
|
+
if (operation && (entry.acceptanceOperations?.length ?? 0) >= 128) throw new Error("Memory acceptance operation limit reached");
|
|
124
|
+
approval(entry, authority, verification);
|
|
125
|
+
touch(entry);
|
|
126
|
+
if (operation) (entry.acceptanceOperations ??= []).push({ id: operation, expectedRevision, resultRevision: entry.revision, fingerprint });
|
|
127
|
+
return clone(entry);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Feedback changes retrieval metadata, not the content revision or approval authority. */
|
|
131
|
+
async recordFeedback(id, expectedRevision, input, authority) {
|
|
132
|
+
if (authority.kind !== "user") throw new Error("Only the user can rate reusable knowledge");
|
|
133
|
+
if (!["useful", "irrelevant", "stale", "incorrect"].includes(input.rating)) throw new Error("Unknown memory rating");
|
|
134
|
+
const note = input.note?.trim() ? text(input.note, 1e3, "feedback note") : void 0;
|
|
135
|
+
const runId = input.runId === void 0 ? void 0 : text(input.runId, 240, "feedback run ID");
|
|
136
|
+
return this.change((doc) => {
|
|
137
|
+
const entry = current(doc, id, expectedRevision, authority, true);
|
|
138
|
+
const feedback = entry.feedback ?? [];
|
|
139
|
+
const existing = feedback.find((item) => item.revision === expectedRevision && item.runId === runId);
|
|
140
|
+
if (existing?.rating === input.rating && existing.note === note) return clone(entry);
|
|
141
|
+
const next = { id: existing?.id ?? `mf-${randomUUID()}`, revision: expectedRevision, rating: input.rating, note, runId, at: Date.now(), origin: "user" };
|
|
142
|
+
entry.feedback = [...feedback.filter((item) => item.id !== next.id), next].slice(-64);
|
|
143
|
+
return clone(entry);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Promote team knowledge; branch facts need explicit branch applicability. */
|
|
147
|
+
async promote(id, expectedRevision, destination, authority, verification) {
|
|
148
|
+
return this.change((doc) => {
|
|
149
|
+
const entry = current(doc, id, expectedRevision, authority, true);
|
|
150
|
+
const candidate = validateInput({ ...entry, scope: "project", projectId: destination.projectId, applicability: destination.applicability });
|
|
151
|
+
authorize(candidate, authority, true);
|
|
152
|
+
Object.assign(entry, candidate);
|
|
153
|
+
preventPrivatePublication("project", entry.origins, authority);
|
|
154
|
+
approval(entry, authority, verification);
|
|
155
|
+
touch(entry);
|
|
156
|
+
return clone(entry);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Redact this entry. Entries that list it in `derivedFrom` keep their own bodies: that list
|
|
161
|
+
* records what was in a member's context (provenance), not text copied from it, and the
|
|
162
|
+
* origins they inherited from it stay. Source agent output in run transcripts, provider
|
|
163
|
+
* conversations and external backups is separate history; this operation does not claim to
|
|
164
|
+
* erase those records.
|
|
165
|
+
*/
|
|
166
|
+
async remove(id, expectedRevision, authority) {
|
|
167
|
+
return this.change((doc) => {
|
|
168
|
+
const entry = current(doc, id, expectedRevision, authority, true);
|
|
169
|
+
redact(entry);
|
|
170
|
+
return clone(entry);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Archiving a run retires its private checkpoints and team records; project knowledge is untouched. */
|
|
174
|
+
async purgeRun(runId) {
|
|
175
|
+
return this.change((doc) => {
|
|
176
|
+
let count = 0;
|
|
177
|
+
for (const entry of doc.entries) {
|
|
178
|
+
if (entry.scope === "project" || entry.runId !== runId || entry.status === "deleted") continue;
|
|
179
|
+
redact(entry);
|
|
180
|
+
count++;
|
|
181
|
+
}
|
|
182
|
+
return count;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/** Offline maintenance reads provenance metadata without exporting private bodies/titles. */
|
|
186
|
+
async maintenanceEntries() {
|
|
187
|
+
const doc = await this.read();
|
|
188
|
+
return doc.entries.map(({ id, revision, scope, repo, projectId, runId, status, tags, derivedFrom, origins, sourceRefs, createdAt, updatedAt }) => clone({ id, revision, scope, repo, projectId, runId, status, tags, derivedFrom, origins, sourceRefs, createdAt, updatedAt }));
|
|
189
|
+
}
|
|
190
|
+
/** One atomic offline retirement; project guidance and live source relations stay protected. */
|
|
191
|
+
async retireMaintenance(refs) {
|
|
192
|
+
const selected = uniqueRefs(refs);
|
|
193
|
+
return this.change((doc) => {
|
|
194
|
+
const ids = new Set(selected.map((ref) => ref.id));
|
|
195
|
+
const entries = selected.map((ref) => current(doc, ref.id, ref.revision, { kind: "user" }, true));
|
|
196
|
+
if (entries.some((entry) => entry.scope === "project" || entry.tags.includes("pinned"))) throw new Error("Retention cannot erase project guidance or pinned memory");
|
|
197
|
+
if (doc.entries.some((entry) => entry.status !== "deleted" && entry.derivedFrom.some((source) => ids.has(source.id)))) throw new Error("Retention cannot erase a live memory provenance source");
|
|
198
|
+
for (const entry of entries) redact(entry);
|
|
199
|
+
return entries.length;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Inspector includes proposals, but never another member's private records. */
|
|
203
|
+
async list(query, authority) {
|
|
204
|
+
validateQuery(query, authority);
|
|
205
|
+
const doc = await this.read(query);
|
|
206
|
+
return doc.entries.filter((entry) => visible(entry, query, authority) && entry.status !== "deleted").sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id)).map(clone);
|
|
207
|
+
}
|
|
208
|
+
/** Bounded lexical/path selection. Revisions and exclusions can be shown in the inspector. */
|
|
209
|
+
async retrieve(query, authority) {
|
|
210
|
+
validateQuery(query, authority);
|
|
211
|
+
return this.snapshot((db) => {
|
|
212
|
+
const doc = this.database.read(db, query);
|
|
213
|
+
const eligible = doc.entries.filter((entry) => visible(entry, query, authority) && !exclusion(entry, query) && (!entry.paths.length || query.paths?.some((path) => entry.paths.some((prefix) => pathMatches(path, prefix)))));
|
|
214
|
+
const matched = this.database.search(db, query, eligible.map((entry) => entry.id));
|
|
215
|
+
const graph = this.database.graph(db, eligible);
|
|
216
|
+
const related = relatedMemories(graph, new Set(matched.keys()));
|
|
217
|
+
const selection = { entries: [], manifest: { included: [], omitted: [] } };
|
|
218
|
+
const maxEntries = bound(query.maxEntries, 8, 1, 32);
|
|
219
|
+
const maxChars = bound(query.maxChars, 8e3, 128, 32e3);
|
|
220
|
+
const words = terms(query.query ?? "");
|
|
221
|
+
const candidates = doc.entries.filter((entry) => visible(entry, query, authority) && entry.status !== "deleted").map((entry) => ({ entry, score: score(entry, query, words) + Math.min(6, matched.get(entry.id) ?? 0) + (related.has(entry.id) ? 1 : 0) })).sort((a, b) => b.score - a.score || b.entry.updatedAt - a.entry.updatedAt || a.entry.id.localeCompare(b.entry.id));
|
|
222
|
+
let size = 0;
|
|
223
|
+
for (const { entry, score: relevance } of candidates) {
|
|
224
|
+
const ref = { id: entry.id, revision: entry.revision };
|
|
225
|
+
let reason = exclusion(entry, query);
|
|
226
|
+
if (!reason && entry.paths.length && !query.paths?.some((path) => entry.paths.some((prefix) => pathMatches(path, prefix)))) reason = "outside task paths";
|
|
227
|
+
if (!reason && selection.entries.length >= maxEntries) reason = "entry budget";
|
|
228
|
+
if (!reason && size + entry.title.length + entry.body.length > maxChars) reason = "context budget";
|
|
229
|
+
if (reason) {
|
|
230
|
+
selection.manifest.omitted.push({ ...ref, reason });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
selection.entries.push(clone(entry));
|
|
234
|
+
const rated = entry.feedback?.some((feedback) => feedback.revision === entry.revision && feedback.rating === "useful");
|
|
235
|
+
selection.manifest.included.push({ ...ref, reason: rated ? "applicable context with useful user feedback" : related.has(entry.id) && !matched.has(entry.id) ? "connected to matching memory through recorded evidence" : relevance > 0 ? "matching task text, tags or paths" : "applicable shared context" });
|
|
236
|
+
size += entry.title.length + entry.body.length;
|
|
237
|
+
}
|
|
238
|
+
return selection;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Resolve a recorded selection without adding newly-created or newer-revision content. */
|
|
242
|
+
async resolveSelection(query, manifest, authority) {
|
|
243
|
+
validateQuery(query, authority);
|
|
244
|
+
const doc = await this.read(query);
|
|
245
|
+
const result = { entries: [], manifest: { included: [], omitted: clone(manifest.omitted) } };
|
|
246
|
+
let size = 0;
|
|
247
|
+
for (const ref of manifest.included) {
|
|
248
|
+
const entry = doc.entries.find((candidate) => candidate.id === ref.id);
|
|
249
|
+
let reason;
|
|
250
|
+
if (!entry || entry.status === "deleted") reason = "recorded memory was deleted";
|
|
251
|
+
else if (!visible(entry, query, authority)) reason = "recorded memory is outside current scope";
|
|
252
|
+
else if (entry.revision !== ref.revision) reason = "recorded memory revision is stale";
|
|
253
|
+
else reason = exclusion(entry, query);
|
|
254
|
+
if (!reason && entry.paths.length && !query.paths?.some((path) => entry.paths.some((prefix) => pathMatches(path, prefix)))) reason = "outside task paths";
|
|
255
|
+
if (!reason && (result.entries.length >= bound(query.maxEntries, 8, 1, 32) || size + entry.title.length + entry.body.length > bound(query.maxChars, 8e3, 128, 32e3))) reason = "recorded context exceeds current budget";
|
|
256
|
+
if (reason) {
|
|
257
|
+
result.manifest.omitted.push({ id: ref.id, revision: ref.revision, reason });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
result.entries.push(clone(entry));
|
|
261
|
+
result.manifest.included.push(clone(ref));
|
|
262
|
+
size += entry.title.length + entry.body.length;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/** Inspector graph uses only visible memories; proposals stay explicitly labelled by callers. */
|
|
267
|
+
async graph(query, authority) {
|
|
268
|
+
validateQuery(query, authority);
|
|
269
|
+
return this.snapshot((db) => this.database.graph(
|
|
270
|
+
db,
|
|
271
|
+
this.database.read(db, query).entries.filter((entry) => visible(entry, query, authority) && entry.status !== "deleted")
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
async snapshot(fn) {
|
|
275
|
+
await queues.get(this.file);
|
|
276
|
+
return this.database.withDatabase((db) => {
|
|
277
|
+
db.exec("BEGIN");
|
|
278
|
+
try {
|
|
279
|
+
const result = fn(db);
|
|
280
|
+
db.exec("COMMIT");
|
|
281
|
+
return result;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
db.exec("ROLLBACK");
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
read(query) {
|
|
289
|
+
return this.snapshot((db) => this.database.read(db, query));
|
|
290
|
+
}
|
|
291
|
+
change(fn) {
|
|
292
|
+
const next = (queues.get(this.file) ?? Promise.resolve()).then(() => this.database.withDatabase((db) => this.database.change(db, fn)));
|
|
293
|
+
const settled = next.catch(() => void 0);
|
|
294
|
+
queues.set(this.file, settled);
|
|
295
|
+
void settled.then(() => {
|
|
296
|
+
if (queues.get(this.file) === settled) queues.delete(this.file);
|
|
297
|
+
});
|
|
298
|
+
return next;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const memoryStore = new MemoryStore();
|
|
302
|
+
function redact(entry) {
|
|
303
|
+
entry.title = "";
|
|
304
|
+
entry.body = "";
|
|
305
|
+
entry.tags = [];
|
|
306
|
+
entry.paths = [];
|
|
307
|
+
entry.sourceRefs = [];
|
|
308
|
+
delete entry.verification;
|
|
309
|
+
delete entry.acceptedBy;
|
|
310
|
+
delete entry.feedback;
|
|
311
|
+
entry.status = "deleted";
|
|
312
|
+
entry.deletedAt = Date.now();
|
|
313
|
+
touch(entry);
|
|
314
|
+
}
|
|
315
|
+
function validateInput(raw) {
|
|
316
|
+
if (!raw || !["private", "team", "project"].includes(raw.scope)) throw new Error("Invalid memory scope");
|
|
317
|
+
if (typeof raw.repo !== "string" || !isAbsolute(raw.repo) || normalize(raw.repo) !== raw.repo) throw new Error("Memory repository must be a normalized absolute path");
|
|
318
|
+
const input = {
|
|
319
|
+
scope: raw.scope,
|
|
320
|
+
repo: raw.repo,
|
|
321
|
+
title: text(raw.title, 200, "title"),
|
|
322
|
+
body: text(raw.body, MAX_BODY, "body"),
|
|
323
|
+
tags: strings(raw.tags ?? [], 32, 80, "tags"),
|
|
324
|
+
paths: strings(raw.paths ?? [], 64, 400, "paths").map(relativePath),
|
|
325
|
+
sourceRefs: strings(raw.sourceRefs ?? [], 128, 400, "source references"),
|
|
326
|
+
derivedFrom: uniqueRefs(raw.derivedFrom ?? []),
|
|
327
|
+
applicability: validateApplicability(raw.applicability ?? { kind: "repository" })
|
|
328
|
+
};
|
|
329
|
+
for (const key of ["projectId", "runId", "memberId"]) if (raw[key] !== void 0) input[key] = text(raw[key], 240, key);
|
|
330
|
+
if (raw.operationId !== void 0) input.operationId = text(raw.operationId, 500, "operationId");
|
|
331
|
+
if (input.scope === "project" && !input.projectId) throw new Error("Project memory needs a project ID");
|
|
332
|
+
if (input.scope !== "project" && !input.runId) throw new Error("Private/team memory needs a run ID");
|
|
333
|
+
if (input.scope === "private" && !input.memberId) throw new Error("Private memory needs a member ID");
|
|
334
|
+
if (raw.expiresAt !== void 0) {
|
|
335
|
+
if (!Number.isSafeInteger(raw.expiresAt) || raw.expiresAt < 0) throw new Error("Invalid memory expiry");
|
|
336
|
+
input.expiresAt = raw.expiresAt;
|
|
337
|
+
}
|
|
338
|
+
return input;
|
|
339
|
+
}
|
|
340
|
+
function authorize(entry, authority, write) {
|
|
341
|
+
if (authority.repo && authority.repo !== entry.repo) throw new Error("Memory repository scope mismatch");
|
|
342
|
+
if (authority.projectId && authority.projectId !== entry.projectId) throw new Error("Memory project scope mismatch");
|
|
343
|
+
if (authority.kind === "user") {
|
|
344
|
+
if (authority.runId && authority.runId !== entry.runId) throw new Error("Memory run scope mismatch");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (authority.kind === "import") {
|
|
348
|
+
if (entry.scope !== "project") throw new Error("Historical observations must be project proposals");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (entry.projectId !== authority.projectId) throw new Error("Memory project scope mismatch");
|
|
352
|
+
if (entry.scope !== "project" && entry.runId !== authority.runId) throw new Error("Memory run scope mismatch");
|
|
353
|
+
if (entry.scope === "private" && (!authority.memberId || entry.memberId !== authority.memberId)) throw new Error("Private memory belongs to another member");
|
|
354
|
+
if (write && entry.scope === "project" && entry.runId !== authority.runId) throw new Error("A run cannot edit another run's project proposal or a user-authored rule");
|
|
355
|
+
}
|
|
356
|
+
function origin(authority, scope) {
|
|
357
|
+
if (authority.kind === "run") return { kind: "run", runId: authority.runId, memberId: authority.memberId, trust: authority.sourceTrust, lockedDown: authority.lockedDown, private: scope === "private" };
|
|
358
|
+
return { kind: authority.kind, trust: authority.kind === "user" ? "trusted" : "unknown", lockedDown: false, private: scope === "private" };
|
|
359
|
+
}
|
|
360
|
+
function sourceEntries(doc, refs, input, authority) {
|
|
361
|
+
return refs.map((ref) => {
|
|
362
|
+
const entry = doc.entries.find((candidate) => candidate.id === ref.id);
|
|
363
|
+
if (!entry) throw new Error("Memory source does not exist");
|
|
364
|
+
authorize(entry, authority, false);
|
|
365
|
+
if (entry.status !== "deleted" && entry.revision < ref.revision) throw new MemoryConflictError(clone(entry));
|
|
366
|
+
if (entry.repo !== input.repo || entry.projectId !== input.projectId) throw new Error("Derived memory cannot cross repository/project boundaries");
|
|
367
|
+
return entry;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function preventPrivatePublication(scope, origins, authority) {
|
|
371
|
+
if (scope !== "private" && authority.kind !== "user" && origins.some((source) => source.private)) throw new Error("Publishing private memory requires an explicit user action");
|
|
372
|
+
}
|
|
373
|
+
function approval(entry, authority, verification) {
|
|
374
|
+
if (authority.kind === "import") throw new Error("Imported observations require verification or user approval");
|
|
375
|
+
if (authority.kind === "user") {
|
|
376
|
+
entry.status = "accepted";
|
|
377
|
+
entry.acceptedBy = "user";
|
|
378
|
+
delete entry.verification;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (entry.scope === "private") {
|
|
382
|
+
entry.status = "accepted";
|
|
383
|
+
entry.acceptedBy = "owner";
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (entry.scope === "team") {
|
|
387
|
+
if (!authority.canAcceptTeam) throw new Error("Only the assigned coordinator can accept team contracts");
|
|
388
|
+
entry.status = "accepted";
|
|
389
|
+
entry.acceptedBy = "team";
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (authority.lockedDown || authority.sourceTrust !== "trusted" || entry.origins.some((source) => source.lockedDown || source.trust !== "trusted" || source.private)) {
|
|
393
|
+
throw new Error("Untrusted, imported or private project knowledge requires explicit user approval of this revision");
|
|
394
|
+
}
|
|
395
|
+
if (!verification?.passed || !/^[0-9a-f]{40,64}$/i.test(verification.commit) || !verification.evidenceRefs?.length || verification.evidenceRefs.some((ref) => typeof ref !== "string" || !ref.trim())) {
|
|
396
|
+
throw new Error("Project promotion requires passing runtime verification with commit and evidence references");
|
|
397
|
+
}
|
|
398
|
+
if (entry.applicability.kind === "repository" && !verification.merged) throw new Error("Unmerged knowledge must stay team-scoped or explicitly branch-specific");
|
|
399
|
+
if (entry.applicability.kind === "branch") {
|
|
400
|
+
if (!entry.applicability.commit || entry.applicability.branch !== verification.branch || entry.applicability.commit !== verification.commit) throw new Error("Branch knowledge must match the verified branch and commit");
|
|
401
|
+
}
|
|
402
|
+
entry.status = "accepted";
|
|
403
|
+
entry.acceptedBy = "verification";
|
|
404
|
+
entry.verification = clone(verification);
|
|
405
|
+
}
|
|
406
|
+
function validateQuery(query, authority) {
|
|
407
|
+
if (!query || typeof query.repo !== "string" || !isAbsolute(query.repo) || normalize(query.repo) !== query.repo) throw new Error("Memory query needs an absolute repository");
|
|
408
|
+
if (authority.repo && authority.repo !== query.repo) throw new Error("Memory repository scope mismatch");
|
|
409
|
+
if (authority.projectId && authority.projectId !== query.projectId) throw new Error("Memory project scope mismatch");
|
|
410
|
+
if (authority.kind === "run") {
|
|
411
|
+
if (query.projectId !== authority.projectId || query.runId && query.runId !== authority.runId || query.memberId && query.memberId !== authority.memberId) throw new Error("Memory query scope mismatch");
|
|
412
|
+
}
|
|
413
|
+
if (query.paths) strings(query.paths, 64, 400, "paths").forEach(relativePath);
|
|
414
|
+
}
|
|
415
|
+
function visible(entry, query, authority) {
|
|
416
|
+
if (entry.repo !== query.repo || entry.projectId !== query.projectId || query.scope && entry.scope !== query.scope) return false;
|
|
417
|
+
if (entry.scope === "private" && (entry.runId !== query.runId || entry.memberId !== query.memberId)) return false;
|
|
418
|
+
if (entry.scope === "team" && entry.runId !== query.runId) return false;
|
|
419
|
+
try {
|
|
420
|
+
authorize(entry, authority, false);
|
|
421
|
+
return true;
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function exclusion(entry, query) {
|
|
427
|
+
if (entry.status !== "accepted") return "unaccepted proposal";
|
|
428
|
+
const feedback = entry.feedback?.filter((item) => item.revision === entry.revision) ?? [];
|
|
429
|
+
if (feedback.some((item) => item.rating === "stale" || item.rating === "incorrect")) return "user marked this revision stale or incorrect";
|
|
430
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) return "expired";
|
|
431
|
+
if (entry.applicability.kind === "branch") {
|
|
432
|
+
if (entry.applicability.branch !== query.branch) return "different branch";
|
|
433
|
+
const commit = entry.applicability.commit;
|
|
434
|
+
if (commit && commit !== query.commit && !query.allowedCommits?.includes(commit)) return "unverified commit applicability";
|
|
435
|
+
}
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
function score(entry, query, words) {
|
|
439
|
+
const available = terms(`${entry.title} ${entry.body} ${entry.tags.join(" ")}`);
|
|
440
|
+
let value = 0;
|
|
441
|
+
for (const word of words) if (available.has(word)) value++;
|
|
442
|
+
for (const tag of query.tags ?? []) if (entry.tags.includes(tag)) value += 3;
|
|
443
|
+
for (const path of query.paths ?? []) if (entry.paths.some((prefix) => pathMatches(path, prefix))) value += 5;
|
|
444
|
+
const feedback = entry.feedback?.filter((item) => item.revision === entry.revision) ?? [];
|
|
445
|
+
value += Math.max(-3, Math.min(3, feedback.reduce((sum, item) => sum + (item.rating === "useful" ? 1 : item.rating === "irrelevant" ? -1 : 0), 0)));
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
function pathMatches(path, prefix) {
|
|
449
|
+
return path === "*" || prefix === "*" || path === prefix || path.startsWith(prefix + "/") || prefix.startsWith(path + "/");
|
|
450
|
+
}
|
|
451
|
+
function terms(value) {
|
|
452
|
+
return new Set(value.toLowerCase().match(/[a-z0-9_]{3,}/g) ?? []);
|
|
453
|
+
}
|
|
454
|
+
function clone(value) {
|
|
455
|
+
return structuredClone(value);
|
|
456
|
+
}
|
|
457
|
+
function touch(entry) {
|
|
458
|
+
entry.revision++;
|
|
459
|
+
entry.updatedAt = Date.now();
|
|
460
|
+
}
|
|
461
|
+
function bound(value, fallback, min, max) {
|
|
462
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, Math.floor(value))) : fallback;
|
|
463
|
+
}
|
|
464
|
+
function text(value, max, name) {
|
|
465
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || value.includes("\0")) throw new Error(`Invalid memory ${name} (1\u2013${max} characters)`);
|
|
466
|
+
return value.trim();
|
|
467
|
+
}
|
|
468
|
+
function strings(value, max, length, name) {
|
|
469
|
+
if (!Array.isArray(value) || value.length > max) throw new Error(`Invalid memory ${name}`);
|
|
470
|
+
return [...new Set(value.map((item) => text(item, length, name)))];
|
|
471
|
+
}
|
|
472
|
+
function relativePath(value) {
|
|
473
|
+
const path = value.replace(/\\/g, "/").replace(/\/$/, "");
|
|
474
|
+
if (!path || path.startsWith("/") || /^[a-z]:/i.test(path) || path.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("Memory paths must stay inside the repository");
|
|
475
|
+
return path;
|
|
476
|
+
}
|
|
477
|
+
function validateApplicability(value) {
|
|
478
|
+
if (value?.kind === "repository") return { kind: "repository" };
|
|
479
|
+
if (value?.kind !== "branch") throw new Error("Invalid memory applicability");
|
|
480
|
+
const branch = text(value.branch, 240, "branch");
|
|
481
|
+
if (value.commit !== void 0 && !/^[0-9a-f]{40,64}$/i.test(value.commit)) throw new Error("Memory applicability needs a full commit SHA");
|
|
482
|
+
return { kind: "branch", branch, ...value.commit ? { commit: value.commit } : {} };
|
|
483
|
+
}
|
|
484
|
+
function uniqueRefs(value) {
|
|
485
|
+
if (!Array.isArray(value) || value.length > 1024) throw new Error("Too many memory derivation references");
|
|
486
|
+
const refs = /* @__PURE__ */ new Map();
|
|
487
|
+
for (const ref of value) {
|
|
488
|
+
if (!ref || !Number.isSafeInteger(ref.revision) || ref.revision < 1) throw new Error("Invalid memory revision reference");
|
|
489
|
+
const id = text(ref.id, 240, "reference");
|
|
490
|
+
refs.set(`${id}@${ref.revision}`, { id, revision: ref.revision });
|
|
491
|
+
}
|
|
492
|
+
return [...refs.values()].sort((a, b) => a.id.localeCompare(b.id) || a.revision - b.revision);
|
|
493
|
+
}
|
|
494
|
+
function unionOrigins(origins) {
|
|
495
|
+
return [...new Map(origins.map((source) => [JSON.stringify(source), source])).entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, source]) => source);
|
|
496
|
+
}
|
|
497
|
+
function current(doc, id, revision, authority, write = false) {
|
|
498
|
+
const entry = doc.entries.find((candidate) => candidate.id === id);
|
|
499
|
+
if (!entry || entry.status === "deleted") throw new Error("Memory entry not found");
|
|
500
|
+
authorize(entry, authority, write);
|
|
501
|
+
if (entry.revision !== revision) throw new MemoryConflictError(clone(entry));
|
|
502
|
+
return entry;
|
|
503
|
+
}
|
|
504
|
+
function hashOperation(input, authority) {
|
|
505
|
+
return createHash("sha256").update(JSON.stringify({ input, origin: origin(authority, input.scope) })).digest("hex");
|
|
506
|
+
}
|
|
507
|
+
function canonicalJson(value) {
|
|
508
|
+
const sorted = (item) => Array.isArray(item) ? item.map(sorted) : item && typeof item === "object" ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)).map(([key, value2]) => [key, sorted(value2)])) : item;
|
|
509
|
+
return JSON.stringify(sorted(value));
|
|
510
|
+
}
|
|
511
|
+
function validateDocument(raw) {
|
|
512
|
+
try {
|
|
513
|
+
if (Buffer.byteLength(raw) > MAX_FILE) throw new Error("file is too large");
|
|
514
|
+
const document = JSON.parse(raw);
|
|
515
|
+
if (!document || typeof document !== "object") throw new Error("invalid document");
|
|
516
|
+
const doc = document;
|
|
517
|
+
if (doc.schemaVersion !== 1 || !Array.isArray(doc.entries) || doc.entries.filter((entry) => entry?.status !== "deleted" && entry?.scope === "project").length > MAX_ENTRIES) throw new Error("unsupported schema");
|
|
518
|
+
const ids = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const entry of doc.entries) {
|
|
520
|
+
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || ids.has(entry.id) || !Number.isSafeInteger(entry.revision) || entry.revision < 1 || !["proposed", "accepted", "deleted"].includes(entry.status)) throw new Error("invalid entry");
|
|
521
|
+
ids.add(entry.id);
|
|
522
|
+
validateInput(entry.status === "deleted" ? { ...entry, title: "deleted", body: "deleted" } : entry);
|
|
523
|
+
if (!Array.isArray(entry.origins) || !entry.origins.length || entry.origins.some((source) => !source || !["user", "run", "import"].includes(source.kind) || !["trusted", "untrusted", "unknown"].includes(source.trust) || typeof source.lockedDown !== "boolean" || typeof source.private !== "boolean")) throw new Error("invalid provenance");
|
|
524
|
+
if (!Array.isArray(entry.paths) || !Array.isArray(entry.tags) || !Array.isArray(entry.derivedFrom) || !Array.isArray(entry.sourceRefs) || !Number.isFinite(entry.createdAt) || !Number.isFinite(entry.updatedAt)) throw new Error("incomplete entry");
|
|
525
|
+
if (entry.acceptanceOperations !== void 0 && (!Array.isArray(entry.acceptanceOperations) || entry.acceptanceOperations.length > 128 || entry.acceptanceOperations.some((operation) => !operation || typeof operation.id !== "string" || !operation.id || operation.id.length > 500 || !Number.isSafeInteger(operation.expectedRevision) || operation.expectedRevision < 1 || operation.resultRevision !== operation.expectedRevision + 1 || typeof operation.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(operation.fingerprint)))) throw new Error("invalid acceptance operation");
|
|
526
|
+
}
|
|
527
|
+
return doc;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new Error(`Memory store cannot be read; no records were changed: ${error.message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function relatedMemories(graph, matches) {
|
|
533
|
+
const nodes = new Map(graph.entities.map((entity) => [entity.id, entity]));
|
|
534
|
+
let frontier = new Set(graph.entities.filter((entity) => entity.kind === "memory" && matches.has(entity.key)).map((entity) => entity.id));
|
|
535
|
+
const visited = new Set(frontier);
|
|
536
|
+
for (let hop = 0; hop < 2; hop++) {
|
|
537
|
+
const next = /* @__PURE__ */ new Set();
|
|
538
|
+
for (const edge of graph.relationships) {
|
|
539
|
+
if (frontier.has(edge.from) && !visited.has(edge.to)) next.add(edge.to);
|
|
540
|
+
if (frontier.has(edge.to) && !visited.has(edge.from)) next.add(edge.from);
|
|
541
|
+
}
|
|
542
|
+
for (const id of next) visited.add(id);
|
|
543
|
+
frontier = next;
|
|
544
|
+
}
|
|
545
|
+
return new Set([...visited].flatMap((id) => nodes.get(id)?.kind === "memory" ? [nodes.get(id).key] : []));
|
|
546
|
+
}
|
|
547
|
+
export {
|
|
548
|
+
MemoryConflictError,
|
|
549
|
+
MemoryStore,
|
|
550
|
+
memoryStore,
|
|
551
|
+
validateDocument
|
|
552
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { assertWorktree, git, resolveCommit } from "./git.js";
|
|
2
|
+
import { inTemporaryWorktree } from "./readiness.js";
|
|
3
|
+
import { runCheck } from "./checks.js";
|
|
4
|
+
import { runVerification } from "./verification.js";
|
|
5
|
+
import { readOnlyGitMetadata } from "./agent-container.js";
|
|
6
|
+
async function validateMergeTarget(repo, input) {
|
|
7
|
+
const [source, target] = await Promise.all([resolveCommit(repo, `refs/heads/${input.branch}`), resolveCommit(repo, `refs/heads/${input.into}`)]);
|
|
8
|
+
return inTemporaryWorktree(repo, target, async (path) => {
|
|
9
|
+
const checkout = await assertWorktree(repo, { path });
|
|
10
|
+
const container = input.container ? { ...input.container, readOnly: [.../* @__PURE__ */ new Set([...input.container.readOnly ?? [], ...readOnlyGitMetadata(path)])] } : void 0;
|
|
11
|
+
const merged = await git(["-c", "core.hooksPath=/dev/null", "-c", "user.name=code-flow validation", "-c", "user.email=validation@code-flow.invalid", "merge", "--no-commit", "--no-ff", source], checkout, { signal: input.signal });
|
|
12
|
+
const result = { branch: input.branch, into: input.into, source, target, conflicts: [], at: Date.now() };
|
|
13
|
+
if (merged.code) {
|
|
14
|
+
result.conflicts = (await git(["diff", "--name-only", "--diff-filter=U", "-z"], checkout)).stdout.split("\0").filter(Boolean);
|
|
15
|
+
if (!result.conflicts.length) throw new Error(merged.stderr.trim() || merged.stdout.trim() || "Cannot construct the merge target");
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
const tree = await git(["write-tree"], checkout);
|
|
19
|
+
if (tree.code) throw new Error(tree.stderr.trim() || "Cannot record the merge tree");
|
|
20
|
+
result.tree = tree.stdout.trim();
|
|
21
|
+
const assertCandidate = async (stage) => {
|
|
22
|
+
if (input.signal?.aborted) throw new Error("Merge validation was cancelled");
|
|
23
|
+
await assertWorktree(repo, { path });
|
|
24
|
+
const options = { signal: input.signal, timeoutMs: 1e4 };
|
|
25
|
+
const [currentHead, currentTree, unstaged, flags] = await Promise.all([
|
|
26
|
+
git(["rev-parse", "--verify", "HEAD^{commit}"], checkout, options),
|
|
27
|
+
git(["write-tree"], checkout, options),
|
|
28
|
+
git(["diff", "--no-ext-diff", "--no-textconv", "--ignore-submodules=none", "--quiet", "--exit-code", "--"], checkout, options),
|
|
29
|
+
git(["ls-files", "-v", "-z"], checkout, options)
|
|
30
|
+
]);
|
|
31
|
+
if (input.signal?.aborted) throw new Error("Merge validation was cancelled");
|
|
32
|
+
const hidden = flags.stdout.split("\0").some((file) => /^[a-zS] /.test(file));
|
|
33
|
+
if (currentHead.code || currentTree.code || unstaged.code || flags.code || hidden || currentHead.stdout.trim() !== target || currentTree.stdout.trim() !== result.tree) throw new Error(`Merge validation ${stage} changed tracked files, the staged tree or the checkout commit, or hid tracked files from comparison. The original merge candidate was not verified.`);
|
|
34
|
+
};
|
|
35
|
+
await assertCandidate("merge construction");
|
|
36
|
+
if (input.setup) {
|
|
37
|
+
await assertCandidate("before setup");
|
|
38
|
+
const setup = await runCheck(path, input.setup, { signal: input.signal, container, timeoutMs: 12e4 });
|
|
39
|
+
await assertCandidate("setup");
|
|
40
|
+
if (!setup.ok) throw new Error(`Merge validation setup failed: ${setup.output.slice(-2e3)}`);
|
|
41
|
+
}
|
|
42
|
+
result.report = await runVerification(path, input.profile, {
|
|
43
|
+
signal: input.signal,
|
|
44
|
+
container,
|
|
45
|
+
execute: async (check, options) => {
|
|
46
|
+
await assertCandidate(`before gate ${check.id}`);
|
|
47
|
+
const checked = await runCheck(path, check.command, options);
|
|
48
|
+
await assertCandidate(`gate ${check.id}`);
|
|
49
|
+
return checked;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
await assertCandidate("completion");
|
|
53
|
+
return result;
|
|
54
|
+
}, input.signal);
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
validateMergeTarget
|
|
58
|
+
};
|