glimmervoid 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/glimmervoid.js +144 -57
- package/dist/chunks/{adapters-Dho1VSY5.js → adapters-BU74M4QZ.js} +1 -1
- package/dist/chunks/{adapters-zPDx1Xox.js → adapters-CoC18rdt.js} +56 -31
- package/dist/chunks/agent-api-cli-V_BkYhBI.js +142 -0
- package/dist/chunks/agent-api-core-B3YI8SYs.js +192 -0
- package/dist/chunks/{agent-setup-cli-B5Atxr3F.js → agent-setup-cli-D4956BfN.js} +11 -28
- package/dist/chunks/backoff-DjVpfHk7.js +14 -0
- package/dist/chunks/child-process-safe-B3zcPdFM.js +2 -0
- package/dist/chunks/{child-process-safe-DRGsp0TE.js → child-process-safe-DDELk9XC.js} +3 -2
- package/dist/chunks/coerce-NSst8oRP.js +32 -0
- package/dist/chunks/{contracts-DGkKNh9W.js → config-CZ4H51D0.js} +374 -1684
- package/dist/chunks/{config-store-mm7MgzoL.js → config-store-BVtdHkDK.js} +594 -329
- package/dist/chunks/config-store-Cp62jN1O.js +2 -0
- package/dist/chunks/contracts-DXujp-Zn.js +2131 -0
- package/dist/chunks/git-version-CrLW4a6r.js +58 -0
- package/dist/chunks/{grok-DuU8xVLZ.js → grok-C4lzPFEg.js} +24 -21
- package/dist/chunks/{hook-relay-core-BHfYFnYV.js → hook-relay-core-DA_bz2KA.js} +10 -5
- package/dist/chunks/{hook-tools-pSJia7ks.js → hook-tools-UX2fmt81.js} +3 -5
- package/dist/chunks/{ingest-editor-core-BuI1gMF_.js → ingest-editor-core-vR6Wv6bP.js} +24 -13
- package/dist/chunks/json-core-dfa_7Fak.js +23 -0
- package/dist/chunks/json-file-oEH9vt-0.js +352 -0
- package/dist/chunks/knowledge-graph-cli-Bjy9_kv5.js +1354 -0
- package/dist/chunks/{loopback-post-C7C22eYi.js → loopback-post-DepQfdcM.js} +40 -26
- package/dist/chunks/{main-CuIZQPhZ.js → main-DSbpYn2Y.js} +27880 -19139
- package/dist/chunks/{node-pty-preflight-CK1knGIR.js → node-pty-preflight-C8QIlrtU.js} +4 -6
- package/dist/chunks/{pair-cli-w-gCAF-1.js → pair-cli-CKn-wUw9.js} +4 -3
- package/dist/chunks/paths-CeJl2Vcg.js +55 -0
- package/dist/chunks/{remote-config-C2ytQwrs.js → remote-config-DP44Ybt9.js} +37 -41
- package/dist/chunks/{rtk-hook-core-Bl6gYHPN.js → rtk-hook-core-BmEz2aq9.js} +3 -5
- package/dist/chunks/rtk-resolver-CIVLE_xk.js +2 -0
- package/dist/chunks/{rtk-resolver-CLo-gZzw.js → rtk-resolver-jJxy_Mrt.js} +2 -2
- package/dist/chunks/{runtime-paths-Ci_ksSkj.js → runtime-paths-BPoxENmK.js} +2 -6
- package/dist/chunks/runtime-paths-DU5jBEEO.js +2 -0
- package/dist/chunks/{sandbox-deps-Dd5-kZnf.js → sandbox-deps-DuaNFq7n.js} +16 -13
- package/dist/chunks/sandbox-proxy-core-hnQgKR0T.js +27 -0
- package/dist/chunks/{session-Cez8aIWg.js → session-B8fJb_Or.js} +25 -2
- package/dist/chunks/text-DoirglWV.js +21 -0
- package/dist/chunks/timer-deps-u2DAQLok.js +15 -0
- package/dist/chunks/{visions-cli-BXQFIBCX.js → visions-cli-BhFdym5n.js} +1 -1
- package/dist/chunks/{visions-setup-CvVnOe1r.js → visions-setup-CfgNHiG3.js} +20 -357
- package/dist/client/assets/index-C_XKsDFx.css +1 -0
- package/dist/client/assets/index-CoqWjYxP.js +254 -0
- package/dist/client/fonts/CommitMono-OFL.txt +92 -0
- package/dist/client/fonts/commit-mono-400-italic.woff2 +0 -0
- package/dist/client/fonts/commit-mono-400-regular.woff2 +0 -0
- package/dist/client/fonts/commit-mono-700-italic.woff2 +0 -0
- package/dist/client/fonts/commit-mono-700-regular.woff2 +0 -0
- package/dist/client/index.html +7 -24
- package/dist/client/octicons-LICENSE.txt +21 -0
- package/dist/scripts/postinstall-path-check.js +1 -1
- package/dist/server/index.js +3 -3
- package/dist/session/command-hook-relay.js +1 -1
- package/dist/session/hook-relay.js +3 -3
- package/dist/session/hook-tool-relay.js +2 -2
- package/dist/session/sandbox-open-launcher.js +79 -0
- package/dist/session/statusline-relay.js +8 -18
- package/dist/session/visions-relay.js +20 -11
- package/package.json +4 -1
- package/dist/chunks/agent-api-cli-D0MmtEdz.js +0 -71
- package/dist/chunks/agent-api-core-Dz0r21y9.js +0 -103
- package/dist/chunks/child-process-safe-DvQjXsgz.js +0 -2
- package/dist/chunks/config-store-CC8T3okC.js +0 -2
- package/dist/chunks/ingest-core-DWI1Mbu4.js +0 -342
- package/dist/chunks/rtk-resolver-CMQAidlI.js +0 -2
- package/dist/chunks/runtime-paths-JLrZ3wyi.js +0 -2
- package/dist/client/assets/index-B1MFijjb.css +0 -1
- package/dist/client/assets/index-CU1iPPdn.js +0 -249
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
import { r as errorMessage } from "./text-DoirglWV.js";
|
|
2
|
+
import { s as resolvePackageBin } from "./runtime-paths-BPoxENmK.js";
|
|
3
|
+
import { r as execFileSync } from "./child-process-safe-DDELk9XC.js";
|
|
4
|
+
import { n as glimmervoidHomeDir, t as decideConfigPath } from "./config-path-core-DFt-TemS.js";
|
|
5
|
+
import { r as BrowserConfig } from "./config-CZ4H51D0.js";
|
|
6
|
+
import fs, { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
7
|
+
import path, { dirname, join, posix, resolve, win32 } from "node:path";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
|
+
//#region knowledge-graph/coherence-delta.ts
|
|
13
|
+
var journalRecordKinds = [
|
|
14
|
+
"decision",
|
|
15
|
+
"blocked",
|
|
16
|
+
"conjecture"
|
|
17
|
+
];
|
|
18
|
+
var openWorkStates = /* @__PURE__ */ new Set([
|
|
19
|
+
"open",
|
|
20
|
+
"active",
|
|
21
|
+
"blocked"
|
|
22
|
+
]);
|
|
23
|
+
var closedTaskStatuses$1 = /* @__PURE__ */ new Set(["done", "dropped"]);
|
|
24
|
+
var openTaskStatuses = /* @__PURE__ */ new Set([
|
|
25
|
+
"todo",
|
|
26
|
+
"doing",
|
|
27
|
+
"waiting"
|
|
28
|
+
]);
|
|
29
|
+
function inferRecordKind(recordId) {
|
|
30
|
+
if (/^wrk-[0-9a-f]{16}$/.test(recordId)) return "work";
|
|
31
|
+
if (/^d-[0-9a-f]{8,}$/.test(recordId)) return "decision";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function findStatusDivergence(trackingStatus, workState) {
|
|
35
|
+
if (trackingStatus === void 0) return null;
|
|
36
|
+
if (closedTaskStatuses$1.has(trackingStatus) && openWorkStates.has(workState)) return `kg says ${trackingStatus}, ledger says ${workState}`;
|
|
37
|
+
if (openTaskStatuses.has(trackingStatus) && workState === "completed") return `ledger says completed, kg still ${trackingStatus}`;
|
|
38
|
+
if (trackingStatus !== "dropped" && workState === "cancelled") return `ledger says cancelled, kg still ${trackingStatus}`;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function isAfter(timestamp, since) {
|
|
42
|
+
return since !== void 0 && timestamp !== null && Date.parse(timestamp) > Date.parse(since);
|
|
43
|
+
}
|
|
44
|
+
function hasDecisionMovedSince(decision, since) {
|
|
45
|
+
return isAfter(decision.at, since) || isAfter(decision.retractedAt, since);
|
|
46
|
+
}
|
|
47
|
+
function reportWorkRecord(tracked, snapshot, since) {
|
|
48
|
+
const workOrder = snapshot.workOrders.find((candidate) => candidate.id === tracked.recordId);
|
|
49
|
+
if (!workOrder) return {
|
|
50
|
+
tracked,
|
|
51
|
+
label: tracked.recordId,
|
|
52
|
+
state: "missing",
|
|
53
|
+
findings: [{
|
|
54
|
+
kind: "missing",
|
|
55
|
+
detail: "not in this repo's work ledger"
|
|
56
|
+
}],
|
|
57
|
+
decisions: []
|
|
58
|
+
};
|
|
59
|
+
const findings = [];
|
|
60
|
+
if (snapshot.unverifiedCompletedWorkIds.includes(workOrder.id)) findings.push({
|
|
61
|
+
kind: "unverified",
|
|
62
|
+
detail: "completed with no verification on record"
|
|
63
|
+
});
|
|
64
|
+
const divergence = tracked.trackingKind === "task" ? findStatusDivergence(tracked.trackingStatus, workOrder.state) : null;
|
|
65
|
+
if (divergence) findings.push({
|
|
66
|
+
kind: "status-diverges",
|
|
67
|
+
detail: divergence
|
|
68
|
+
});
|
|
69
|
+
if (isAfter(workOrder.lastEventAt, since)) findings.push({
|
|
70
|
+
kind: "moved-since",
|
|
71
|
+
detail: `last event ${workOrder.lastEventAt}`
|
|
72
|
+
});
|
|
73
|
+
const decisions = snapshot.decisions.filter((decision) => decision.workId === workOrder.id && (since === void 0 || hasDecisionMovedSince(decision, since)));
|
|
74
|
+
return {
|
|
75
|
+
tracked,
|
|
76
|
+
label: workOrder.objective,
|
|
77
|
+
state: `${workOrder.state}/${workOrder.readiness}`,
|
|
78
|
+
findings,
|
|
79
|
+
decisions
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function reportDecisionRecord(tracked, snapshot, since) {
|
|
83
|
+
const decision = snapshot.decisions.find((candidate) => candidate.id === tracked.recordId);
|
|
84
|
+
if (!decision) return {
|
|
85
|
+
tracked,
|
|
86
|
+
label: tracked.recordId,
|
|
87
|
+
state: "missing",
|
|
88
|
+
findings: [{
|
|
89
|
+
kind: "missing",
|
|
90
|
+
detail: "not in this repo's decision journal"
|
|
91
|
+
}],
|
|
92
|
+
decisions: []
|
|
93
|
+
};
|
|
94
|
+
const findings = decision.standing === "retracted" ? [{
|
|
95
|
+
kind: "retracted",
|
|
96
|
+
detail: "withdrawn in the journal; kg notes relying on it need a look"
|
|
97
|
+
}] : [];
|
|
98
|
+
if (isAfter(decision.retractedAt, since)) findings.push({
|
|
99
|
+
kind: "moved-since",
|
|
100
|
+
detail: `retracted ${decision.retractedAt}`
|
|
101
|
+
});
|
|
102
|
+
if (isAfter(decision.at, since)) findings.push({
|
|
103
|
+
kind: "moved-since",
|
|
104
|
+
detail: `decided ${decision.at}`
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
tracked,
|
|
108
|
+
label: decision.chose,
|
|
109
|
+
state: `${decision.kind}/${decision.standing}`,
|
|
110
|
+
findings,
|
|
111
|
+
decisions: []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function buildCoherenceDelta(trackedRecords, snapshotsByRepo, since) {
|
|
115
|
+
return [...new Set(trackedRecords.map((tracked) => tracked.repo))].toSorted().map((repo) => {
|
|
116
|
+
const repoRecords = trackedRecords.filter((tracked) => tracked.repo === repo);
|
|
117
|
+
const snapshot = snapshotsByRepo.get(repo) ?? {
|
|
118
|
+
isAvailable: false,
|
|
119
|
+
reason: "not read"
|
|
120
|
+
};
|
|
121
|
+
if (!snapshot.isAvailable) return {
|
|
122
|
+
repo,
|
|
123
|
+
isAvailable: false,
|
|
124
|
+
reason: snapshot.reason,
|
|
125
|
+
records: repoRecords
|
|
126
|
+
};
|
|
127
|
+
const records = repoRecords.map((tracked) => tracked.record === "work" ? reportWorkRecord(tracked, snapshot, since) : reportDecisionRecord(tracked, snapshot, since));
|
|
128
|
+
return {
|
|
129
|
+
repo,
|
|
130
|
+
isAvailable: true,
|
|
131
|
+
heading: snapshot.heading,
|
|
132
|
+
headingReasons: snapshot.headingReasons,
|
|
133
|
+
records
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region knowledge-graph/graph-schema.ts
|
|
139
|
+
var UNSAFE_TEXT_CHARACTERS = /[\p{Cc}\p{Zl}\p{Zp}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;
|
|
140
|
+
var INVISIBLE_TEXT_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\s]/gu;
|
|
141
|
+
function hasVisibleText(text) {
|
|
142
|
+
return text.replace(INVISIBLE_TEXT_CHARACTERS, "") !== "";
|
|
143
|
+
}
|
|
144
|
+
function containsUnsafeTextCharacter(text) {
|
|
145
|
+
return text.match(UNSAFE_TEXT_CHARACTERS) !== null;
|
|
146
|
+
}
|
|
147
|
+
function stripUnsafeTextCharacters(text) {
|
|
148
|
+
return text.replace(UNSAFE_TEXT_CHARACTERS, "");
|
|
149
|
+
}
|
|
150
|
+
function splitIntoSafeLines(text) {
|
|
151
|
+
return text.split(/\r\n|[\r\n\u2028\u2029]/).map((line) => line.split(" ").map(stripUnsafeTextCharacters).join(" "));
|
|
152
|
+
}
|
|
153
|
+
function listKnownPropertyKeys(schema, kind) {
|
|
154
|
+
const properties = schema.kinds[kind]?.properties;
|
|
155
|
+
if (properties === void 0) return null;
|
|
156
|
+
if (!("shape" in properties) || typeof properties.shape !== "object" || properties.shape === null) return null;
|
|
157
|
+
return Object.keys(properties.shape);
|
|
158
|
+
}
|
|
159
|
+
function describeUnknownKind(schema, kind) {
|
|
160
|
+
return `unknown kind "${kind}"; known: ${Object.keys(schema.kinds).join(", ")}`;
|
|
161
|
+
}
|
|
162
|
+
function describeUnknownEdgeType(schema, edgeType) {
|
|
163
|
+
return `unknown edge type "${edgeType}"; known: ${Object.keys(schema.edges).join(", ")}`;
|
|
164
|
+
}
|
|
165
|
+
function isKindAllowed(allowedKinds, kind) {
|
|
166
|
+
return allowedKinds === "any" || allowedKinds.includes(kind);
|
|
167
|
+
}
|
|
168
|
+
function describeAllowedKinds(allowedKinds) {
|
|
169
|
+
return allowedKinds === "any" ? "any kind" : allowedKinds.join(" | ");
|
|
170
|
+
}
|
|
171
|
+
function findEdgeViolation(schema, candidate) {
|
|
172
|
+
const definition = schema.edges[candidate.edgeType];
|
|
173
|
+
if (!definition) return describeUnknownEdgeType(schema, candidate.edgeType);
|
|
174
|
+
if (!isKindAllowed(definition.fromKinds, candidate.fromKind)) return `${candidate.edgeType} cannot start at a ${candidate.fromKind} (${candidate.fromId}); allowed: ${describeAllowedKinds(definition.fromKinds)}`;
|
|
175
|
+
if (!isKindAllowed(definition.toKinds, candidate.toKind)) return `${candidate.edgeType} cannot point at a ${candidate.toKind} (${candidate.toId}); allowed: ${describeAllowedKinds(definition.toKinds)}`;
|
|
176
|
+
if (candidate.fromId === candidate.toId) return `${candidate.fromId} cannot ${candidate.edgeType} itself`;
|
|
177
|
+
if (definition.maxOutgoingPerNode !== void 0 && candidate.existingOutgoingCount >= definition.maxOutgoingPerNode) return `${candidate.fromId} already has ${candidate.existingOutgoingCount} ${candidate.edgeType} edge(s); limit is ${definition.maxOutgoingPerNode}`;
|
|
178
|
+
if (definition.isAcyclic && candidate.targetReachesSource) return `${candidate.fromId} ${candidate.edgeType} ${candidate.toId} would create a cycle`;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function parseNodeProperties(schema, kind, rawProperties) {
|
|
182
|
+
const kindDefinition = schema.kinds[kind];
|
|
183
|
+
if (!kindDefinition) return { error: describeUnknownKind(schema, kind) };
|
|
184
|
+
const parsed = kindDefinition.properties.safeParse(rawProperties);
|
|
185
|
+
if (!parsed.success) return { error: `invalid ${kind} properties:\n${z.prettifyError(parsed.error)}` };
|
|
186
|
+
return { properties: parsed.data };
|
|
187
|
+
}
|
|
188
|
+
function findCycleNodeIds(edges) {
|
|
189
|
+
const targetsBySource = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const edge of edges) targetsBySource.set(edge.fromId, [...targetsBySource.get(edge.fromId) ?? [], edge.toId]);
|
|
191
|
+
const finishedIds = /* @__PURE__ */ new Set();
|
|
192
|
+
const onPathIds = /* @__PURE__ */ new Set();
|
|
193
|
+
const cycleNodeIds = /* @__PURE__ */ new Set();
|
|
194
|
+
const visit = (nodeId) => {
|
|
195
|
+
if (finishedIds.has(nodeId)) return;
|
|
196
|
+
if (onPathIds.has(nodeId)) {
|
|
197
|
+
cycleNodeIds.add(nodeId);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
onPathIds.add(nodeId);
|
|
201
|
+
for (const targetId of targetsBySource.get(nodeId) ?? []) visit(targetId);
|
|
202
|
+
onPathIds.delete(nodeId);
|
|
203
|
+
finishedIds.add(nodeId);
|
|
204
|
+
};
|
|
205
|
+
for (const sourceId of targetsBySource.keys()) visit(sourceId);
|
|
206
|
+
return [...cycleNodeIds];
|
|
207
|
+
}
|
|
208
|
+
function findIntegrityViolations(schema, nodes, edges) {
|
|
209
|
+
const violations = [];
|
|
210
|
+
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
211
|
+
for (const node of nodes) {
|
|
212
|
+
const parsed = parseNodeProperties(schema, node.kind, node.properties);
|
|
213
|
+
if ("error" in parsed) violations.push(`${node.id}: ${parsed.error}`);
|
|
214
|
+
}
|
|
215
|
+
const outgoingCountByTypeAndSource = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const edge of edges) {
|
|
217
|
+
const fromNode = nodesById.get(edge.fromId);
|
|
218
|
+
const toNode = nodesById.get(edge.toId);
|
|
219
|
+
if (!fromNode || !toNode) {
|
|
220
|
+
violations.push(`${edge.fromId} ${edge.edgeType} ${edge.toId}: dangling edge`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const countKey = `${edge.edgeType}\u0000${edge.fromId}`;
|
|
224
|
+
const existingOutgoingCount = outgoingCountByTypeAndSource.get(countKey) ?? 0;
|
|
225
|
+
outgoingCountByTypeAndSource.set(countKey, existingOutgoingCount + 1);
|
|
226
|
+
const violation = findEdgeViolation(schema, {
|
|
227
|
+
edgeType: edge.edgeType,
|
|
228
|
+
fromId: edge.fromId,
|
|
229
|
+
fromKind: fromNode.kind,
|
|
230
|
+
toId: edge.toId,
|
|
231
|
+
toKind: toNode.kind,
|
|
232
|
+
existingOutgoingCount,
|
|
233
|
+
targetReachesSource: false
|
|
234
|
+
});
|
|
235
|
+
if (violation) violations.push(violation);
|
|
236
|
+
}
|
|
237
|
+
for (const [edgeType, definition] of Object.entries(schema.edges)) {
|
|
238
|
+
if (!definition.isAcyclic) continue;
|
|
239
|
+
const cycleNodeIds = findCycleNodeIds(edges.filter((edge) => edge.edgeType === edgeType));
|
|
240
|
+
if (cycleNodeIds.length > 0) violations.push(`${edgeType} cycle through ${cycleNodeIds.join(", ")}`);
|
|
241
|
+
}
|
|
242
|
+
return violations;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region knowledge-graph/coherence-source.ts
|
|
246
|
+
var LedgerText = z.string().transform(stripUnsafeTextCharacters);
|
|
247
|
+
var CoherenceRefusal = z.looseObject({ error: LedgerText });
|
|
248
|
+
var WorkInspectOutput = z.looseObject({ work: z.array(z.looseObject({
|
|
249
|
+
work: LedgerText,
|
|
250
|
+
state: LedgerText,
|
|
251
|
+
readiness: LedgerText,
|
|
252
|
+
opened: z.looseObject({ objective: LedgerText }),
|
|
253
|
+
last: z.looseObject({ at: LedgerText }).nullable()
|
|
254
|
+
})) });
|
|
255
|
+
var OrientHeading = z.looseObject({
|
|
256
|
+
action: LedgerText,
|
|
257
|
+
reasons: z.array(LedgerText)
|
|
258
|
+
});
|
|
259
|
+
var OrientOutput = OrientHeading.extend({ consequences: z.looseObject({ unverifiedCompletedWork: z.array(LedgerText) }) });
|
|
260
|
+
var TrackableJournalKind = z.enum(journalRecordKinds);
|
|
261
|
+
var JournalRow = z.looseObject({
|
|
262
|
+
id: LedgerText,
|
|
263
|
+
kind: LedgerText,
|
|
264
|
+
at: LedgerText,
|
|
265
|
+
chose: LedgerText.optional(),
|
|
266
|
+
because: LedgerText.optional(),
|
|
267
|
+
work: LedgerText.optional(),
|
|
268
|
+
supersedes: LedgerText.optional()
|
|
269
|
+
});
|
|
270
|
+
var JOURNAL_OPEN_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
271
|
+
function readRegularFile(filePath, displayName) {
|
|
272
|
+
if (!lstatSync(filePath).isFile()) throw new Error(`${displayName} is not a regular file`);
|
|
273
|
+
const fileDescriptor = openSync(filePath, JOURNAL_OPEN_FLAGS);
|
|
274
|
+
try {
|
|
275
|
+
if (!fstatSync(fileDescriptor).isFile()) throw new Error(`${displayName} is not a regular file`);
|
|
276
|
+
return readFileSync(fileDescriptor, "utf8");
|
|
277
|
+
} finally {
|
|
278
|
+
closeSync(fileDescriptor);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function readCompleteJournalLines(filePath, displayName) {
|
|
282
|
+
return readRegularFile(filePath, displayName).split("\n").slice(0, -1).filter((line) => line.trim() !== "");
|
|
283
|
+
}
|
|
284
|
+
function findEarliestRetractionAtById(rows) {
|
|
285
|
+
const retractedAtById = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const row of rows) {
|
|
287
|
+
if (row.kind !== "retraction" || !row.supersedes) continue;
|
|
288
|
+
const knownAt = retractedAtById.get(row.supersedes);
|
|
289
|
+
if (knownAt !== void 0 && Date.parse(knownAt) <= Date.parse(row.at)) continue;
|
|
290
|
+
retractedAtById.set(row.supersedes, row.at);
|
|
291
|
+
}
|
|
292
|
+
return retractedAtById;
|
|
293
|
+
}
|
|
294
|
+
function findSettlementById(rows, retractedAtById) {
|
|
295
|
+
const settlementById = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
if (!row.supersedes || retractedAtById.has(row.id)) continue;
|
|
298
|
+
if (row.kind === "resolution") settlementById.set(row.supersedes, "resolved");
|
|
299
|
+
if (row.kind === "dismissal" && settlementById.get(row.supersedes) !== "resolved") settlementById.set(row.supersedes, "dismissed");
|
|
300
|
+
}
|
|
301
|
+
return settlementById;
|
|
302
|
+
}
|
|
303
|
+
function listJournalFiles(journalDirectory, relativeDirectory) {
|
|
304
|
+
return readdirSync(join(journalDirectory, relativeDirectory), { withFileTypes: true }).flatMap((entry) => {
|
|
305
|
+
const relativePath = join(relativeDirectory, entry.name);
|
|
306
|
+
if (entry.name.endsWith(".jsonl")) return [relativePath];
|
|
307
|
+
if (entry.isSymbolicLink()) throw new Error(`${relativePath} is a symbolic link`);
|
|
308
|
+
if (entry.isDirectory()) return listJournalFiles(journalDirectory, relativePath);
|
|
309
|
+
return [];
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function readDecisionJournal(repo) {
|
|
313
|
+
const journalDirectory = join(repo, ".coherence", "decisions");
|
|
314
|
+
const journalDirectoryStats = lstatSync(journalDirectory, { throwIfNoEntry: false });
|
|
315
|
+
if (journalDirectoryStats === void 0) return [];
|
|
316
|
+
if (!journalDirectoryStats.isDirectory()) throw new Error(".coherence/decisions is not a directory");
|
|
317
|
+
const rows = listJournalFiles(journalDirectory, "").flatMap((fileName) => readCompleteJournalLines(join(journalDirectory, fileName), fileName).map((line, index) => {
|
|
318
|
+
const parsed = JournalRow.safeParse(JSON.parse(line));
|
|
319
|
+
if (!parsed.success) throw new Error(`unreadable journal row ${fileName}:${index + 1}`);
|
|
320
|
+
return parsed.data;
|
|
321
|
+
}));
|
|
322
|
+
const retractedAtById = findEarliestRetractionAtById(rows);
|
|
323
|
+
const settlementById = findSettlementById(rows, retractedAtById);
|
|
324
|
+
const standingOf = (row, isConjecture) => {
|
|
325
|
+
if (retractedAtById.has(row.id)) return "retracted";
|
|
326
|
+
if (!isConjecture) return "standing";
|
|
327
|
+
return settlementById.get(row.id) ?? "open";
|
|
328
|
+
};
|
|
329
|
+
return rows.flatMap((row) => {
|
|
330
|
+
const kind = TrackableJournalKind.safeParse(row.kind);
|
|
331
|
+
if (!kind.success) return [];
|
|
332
|
+
return [{
|
|
333
|
+
id: row.id,
|
|
334
|
+
kind: kind.data,
|
|
335
|
+
standing: standingOf(row, kind.data === "conjecture"),
|
|
336
|
+
chose: row.chose ?? "",
|
|
337
|
+
because: row.because ?? "",
|
|
338
|
+
at: row.at,
|
|
339
|
+
workId: row.work ?? null,
|
|
340
|
+
retractedAt: retractedAtById.get(row.id) ?? null
|
|
341
|
+
}];
|
|
342
|
+
}).toSorted((left, right) => left.at.localeCompare(right.at));
|
|
343
|
+
}
|
|
344
|
+
function describeShapeDrift(commandName, error) {
|
|
345
|
+
const firstIssue = error.issues[0];
|
|
346
|
+
if (firstIssue === void 0) return `unexpected ${commandName} output`;
|
|
347
|
+
return stripUnsafeTextCharacters(`unexpected ${commandName} output at ${firstIssue.path.length === 0 ? "top level" : firstIssue.path.join(".")}: ${firstIssue.message}`);
|
|
348
|
+
}
|
|
349
|
+
function describeOrientFailure(orientJson, error) {
|
|
350
|
+
const heading = OrientHeading.safeParse(orientJson);
|
|
351
|
+
if (heading.success && heading.data.action === "refuse") return `orient refused: ${heading.data.reasons.join("; ")}`;
|
|
352
|
+
return describeShapeDrift("coherence orient", error);
|
|
353
|
+
}
|
|
354
|
+
function describeReadFailure(error) {
|
|
355
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return "coherence CLI not found";
|
|
356
|
+
return stripUnsafeTextCharacters(error instanceof Error ? error.message.split("\n")[0] ?? "unknown error" : String(error));
|
|
357
|
+
}
|
|
358
|
+
function readRepoSnapshot(repo, runCoherence) {
|
|
359
|
+
const runCoherenceJson = (commandArguments) => JSON.parse(runCoherence(repo, commandArguments));
|
|
360
|
+
if (!existsSync(join(repo, ".coherence"))) return {
|
|
361
|
+
isAvailable: false,
|
|
362
|
+
reason: "no .coherence ledger in this repo"
|
|
363
|
+
};
|
|
364
|
+
try {
|
|
365
|
+
const workInspectJson = runCoherenceJson([
|
|
366
|
+
"work",
|
|
367
|
+
"inspect",
|
|
368
|
+
"--json"
|
|
369
|
+
]);
|
|
370
|
+
const workRefusal = CoherenceRefusal.safeParse(workInspectJson);
|
|
371
|
+
if (workRefusal.success) return {
|
|
372
|
+
isAvailable: false,
|
|
373
|
+
reason: `work ledger refused: ${workRefusal.data.error}`
|
|
374
|
+
};
|
|
375
|
+
const workInspect = WorkInspectOutput.safeParse(workInspectJson);
|
|
376
|
+
if (!workInspect.success) return {
|
|
377
|
+
isAvailable: false,
|
|
378
|
+
reason: describeShapeDrift("coherence work inspect", workInspect.error)
|
|
379
|
+
};
|
|
380
|
+
const orientJson = runCoherenceJson(["orient", "--json"]);
|
|
381
|
+
const orient = OrientOutput.safeParse(orientJson);
|
|
382
|
+
if (!orient.success) return {
|
|
383
|
+
isAvailable: false,
|
|
384
|
+
reason: describeOrientFailure(orientJson, orient.error)
|
|
385
|
+
};
|
|
386
|
+
const workOrders = workInspect.data.work.map((entry) => ({
|
|
387
|
+
id: entry.work,
|
|
388
|
+
objective: entry.opened.objective,
|
|
389
|
+
state: entry.state,
|
|
390
|
+
readiness: entry.readiness,
|
|
391
|
+
lastEventAt: entry.last?.at ?? null
|
|
392
|
+
}));
|
|
393
|
+
return {
|
|
394
|
+
isAvailable: true,
|
|
395
|
+
heading: orient.data.action,
|
|
396
|
+
headingReasons: orient.data.reasons,
|
|
397
|
+
workOrders,
|
|
398
|
+
unverifiedCompletedWorkIds: orient.data.consequences.unverifiedCompletedWork,
|
|
399
|
+
decisions: readDecisionJournal(repo)
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return {
|
|
403
|
+
isAvailable: false,
|
|
404
|
+
reason: describeReadFailure(error)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region knowledge-graph/graph-store.ts
|
|
410
|
+
var NodeRow = z.object({
|
|
411
|
+
id: z.string(),
|
|
412
|
+
kind: z.string(),
|
|
413
|
+
title: z.string(),
|
|
414
|
+
body: z.string(),
|
|
415
|
+
properties: z.string(),
|
|
416
|
+
created_at: z.string(),
|
|
417
|
+
updated_at: z.string()
|
|
418
|
+
});
|
|
419
|
+
var EdgeRow = z.object({
|
|
420
|
+
edge_type: z.string(),
|
|
421
|
+
from_id: z.string(),
|
|
422
|
+
to_id: z.string(),
|
|
423
|
+
created_at: z.string()
|
|
424
|
+
});
|
|
425
|
+
var MetaRow = z.object({ value: z.string() });
|
|
426
|
+
var ColumnNameRow = z.object({ name: z.string() });
|
|
427
|
+
var CountRow = z.object({ edge_count: z.number() });
|
|
428
|
+
var NumberRow = z.object({ last_number: z.number() });
|
|
429
|
+
var NeighbourRow = z.object({ id: z.string() });
|
|
430
|
+
var SqliteBusyError = z.looseObject({ errcode: z.literal(5) });
|
|
431
|
+
var tableDefinitions = `
|
|
432
|
+
CREATE TABLE IF NOT EXISTS graph_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
433
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
434
|
+
id TEXT PRIMARY KEY,
|
|
435
|
+
kind TEXT NOT NULL,
|
|
436
|
+
title TEXT NOT NULL CHECK (length(trim(title)) > 0),
|
|
437
|
+
body TEXT NOT NULL DEFAULT '',
|
|
438
|
+
properties TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(properties)),
|
|
439
|
+
created_at TEXT NOT NULL,
|
|
440
|
+
updated_at TEXT NOT NULL
|
|
441
|
+
);
|
|
442
|
+
CREATE INDEX IF NOT EXISTS nodes_by_kind ON nodes (kind);
|
|
443
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
444
|
+
edge_type TEXT NOT NULL,
|
|
445
|
+
from_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
446
|
+
to_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
447
|
+
created_at TEXT NOT NULL,
|
|
448
|
+
PRIMARY KEY (edge_type, from_id, to_id)
|
|
449
|
+
);
|
|
450
|
+
CREATE INDEX IF NOT EXISTS edges_by_target ON edges (to_id, edge_type);
|
|
451
|
+
CREATE TABLE IF NOT EXISTS id_counters (prefix TEXT PRIMARY KEY, last_number INTEGER NOT NULL);
|
|
452
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS node_search USING fts5 (id UNINDEXED, title, body);
|
|
453
|
+
`;
|
|
454
|
+
function toGraphNode(row) {
|
|
455
|
+
const parsedRow = NodeRow.parse(row);
|
|
456
|
+
return {
|
|
457
|
+
id: parsedRow.id,
|
|
458
|
+
kind: parsedRow.kind,
|
|
459
|
+
title: parsedRow.title,
|
|
460
|
+
body: parsedRow.body,
|
|
461
|
+
properties: z.record(z.string(), z.unknown()).parse(JSON.parse(parsedRow.properties)),
|
|
462
|
+
createdAt: parsedRow.created_at,
|
|
463
|
+
updatedAt: parsedRow.updated_at
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function toGraphEdge(row) {
|
|
467
|
+
const parsedRow = EdgeRow.parse(row);
|
|
468
|
+
return {
|
|
469
|
+
edgeType: parsedRow.edge_type,
|
|
470
|
+
fromId: parsedRow.from_id,
|
|
471
|
+
toId: parsedRow.to_id,
|
|
472
|
+
createdAt: parsedRow.created_at
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function toSearchExpression(text) {
|
|
476
|
+
return text.split(/\s+/).filter((token) => token.length > 0).map((token) => `"${token.replaceAll("\"", "\"\"")}"*`).join(" ");
|
|
477
|
+
}
|
|
478
|
+
function withoutEmptyValues(properties) {
|
|
479
|
+
return Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== void 0 && value !== ""));
|
|
480
|
+
}
|
|
481
|
+
var BUSY_TIMEOUT_MS = 5e3;
|
|
482
|
+
var JOURNAL_SWITCH_RETRY_MS = 10;
|
|
483
|
+
function normalizeTitle(title) {
|
|
484
|
+
const trimmedTitle = title.trim();
|
|
485
|
+
if (!hasVisibleText(trimmedTitle)) throw new Error("title must not be empty");
|
|
486
|
+
if (containsUnsafeTextCharacter(trimmedTitle)) throw new Error("title must be one line without control or bidi characters");
|
|
487
|
+
return trimmedTitle;
|
|
488
|
+
}
|
|
489
|
+
function readOwnership(database) {
|
|
490
|
+
if (!(database.prepare("SELECT 1 AS found FROM sqlite_master WHERE substr(name, 1, 7) <> 'sqlite_' LIMIT 1").get() !== void 0)) return { kind: "fresh" };
|
|
491
|
+
const metaColumnNames = database.prepare("SELECT name FROM pragma_table_info('graph_meta')").all().map((row) => ColumnNameRow.parse(row).name);
|
|
492
|
+
if (!metaColumnNames.includes("key") || !metaColumnNames.includes("value")) return { kind: "foreign" };
|
|
493
|
+
const storedName = MetaRow.safeParse(database.prepare("SELECT value FROM graph_meta WHERE key = 'schema_name'").get());
|
|
494
|
+
return storedName.success ? {
|
|
495
|
+
kind: "owned",
|
|
496
|
+
schemaName: storedName.data.value
|
|
497
|
+
} : { kind: "foreign" };
|
|
498
|
+
}
|
|
499
|
+
function refuseUnlessClaimable(ownership, databasePath, schema) {
|
|
500
|
+
if (ownership.kind === "foreign") throw new Error(`${databasePath} is not a knowledge graph database; refusing to claim it`);
|
|
501
|
+
if (ownership.kind === "owned" && ownership.schemaName !== schema.name) throw new Error(`${databasePath} belongs to the "${ownership.schemaName}" graph, not "${schema.name}"`);
|
|
502
|
+
}
|
|
503
|
+
function runInTransaction(database, work, beginStatement = "BEGIN IMMEDIATE") {
|
|
504
|
+
if (database.isTransaction) return work();
|
|
505
|
+
database.exec(beginStatement);
|
|
506
|
+
try {
|
|
507
|
+
const value = work();
|
|
508
|
+
database.exec("COMMIT");
|
|
509
|
+
return value;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
database.exec("ROLLBACK");
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function tryEnableWriteAheadLog(database, deadlineMs) {
|
|
516
|
+
try {
|
|
517
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
518
|
+
return true;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (SqliteBusyError.safeParse(error).success && Date.now() < deadlineMs) return false;
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function enableWriteAheadLog(database) {
|
|
525
|
+
const deadlineMs = Date.now() + BUSY_TIMEOUT_MS;
|
|
526
|
+
const retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
527
|
+
while (!tryEnableWriteAheadLog(database, deadlineMs)) Atomics.wait(retryPause, 0, 0, JOURNAL_SWITCH_RETRY_MS);
|
|
528
|
+
}
|
|
529
|
+
function claimDatabase(database, databasePath, schema) {
|
|
530
|
+
refuseUnlessClaimable(readOwnership(database), databasePath, schema);
|
|
531
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
532
|
+
runInTransaction(database, () => {
|
|
533
|
+
const ownership = readOwnership(database);
|
|
534
|
+
refuseUnlessClaimable(ownership, databasePath, schema);
|
|
535
|
+
database.exec(tableDefinitions);
|
|
536
|
+
if (ownership.kind !== "fresh") return;
|
|
537
|
+
database.prepare("INSERT INTO graph_meta (key, value) VALUES ('schema_name', ?), ('schema_version', ?)").run(schema.name, String(schema.version));
|
|
538
|
+
});
|
|
539
|
+
enableWriteAheadLog(database);
|
|
540
|
+
}
|
|
541
|
+
function openGraphStore(databasePath, schema, currentTimestamp = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
542
|
+
const database = new DatabaseSync(databasePath, { timeout: BUSY_TIMEOUT_MS });
|
|
543
|
+
try {
|
|
544
|
+
claimDatabase(database, databasePath, schema);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
database.close();
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
const transaction = (work) => runInTransaction(database, work);
|
|
550
|
+
const getNode = (id) => {
|
|
551
|
+
const row = database.prepare("SELECT * FROM nodes WHERE id = ?").get(id);
|
|
552
|
+
return row === void 0 ? null : toGraphNode(row);
|
|
553
|
+
};
|
|
554
|
+
const requireNode = (id) => {
|
|
555
|
+
const node = getNode(id);
|
|
556
|
+
if (!node) throw new Error(`no node with id ${id}`);
|
|
557
|
+
return node;
|
|
558
|
+
};
|
|
559
|
+
const writeSearchEntry = (node) => {
|
|
560
|
+
database.prepare("DELETE FROM node_search WHERE id = ?").run(node.id);
|
|
561
|
+
database.prepare("INSERT INTO node_search (id, title, body) VALUES (?, ?, ?)").run(node.id, node.title, node.body);
|
|
562
|
+
};
|
|
563
|
+
const claimNextId = (idPrefix) => {
|
|
564
|
+
const row = database.prepare("INSERT INTO id_counters (prefix, last_number) VALUES (?, 1) ON CONFLICT (prefix) DO UPDATE SET last_number = last_number + 1 RETURNING last_number").get(idPrefix);
|
|
565
|
+
return `${idPrefix}-${NumberRow.parse(row).last_number}`;
|
|
566
|
+
};
|
|
567
|
+
const parsePropertiesOrThrow = (kind, rawProperties) => {
|
|
568
|
+
const parsed = parseNodeProperties(schema, kind, rawProperties);
|
|
569
|
+
if ("error" in parsed) throw new Error(parsed.error);
|
|
570
|
+
return parsed.properties;
|
|
571
|
+
};
|
|
572
|
+
const addNode = (kind, title, rawProperties = {}, body = "") => {
|
|
573
|
+
const properties = parsePropertiesOrThrow(kind, withoutEmptyValues(rawProperties));
|
|
574
|
+
const normalizedTitle = normalizeTitle(title);
|
|
575
|
+
const idPrefix = schema.kinds[kind]?.idPrefix ?? kind;
|
|
576
|
+
return transaction(() => {
|
|
577
|
+
const timestamp = currentTimestamp();
|
|
578
|
+
const id = claimNextId(idPrefix);
|
|
579
|
+
database.prepare("INSERT INTO nodes (id, kind, title, body, properties, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, kind, normalizedTitle, body, JSON.stringify(properties), timestamp, timestamp);
|
|
580
|
+
const node = requireNode(id);
|
|
581
|
+
writeSearchEntry(node);
|
|
582
|
+
return node;
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
const updateNode = (id, changes) => {
|
|
586
|
+
const title = changes.title === void 0 ? void 0 : normalizeTitle(changes.title);
|
|
587
|
+
return transaction(() => {
|
|
588
|
+
const existing = requireNode(id);
|
|
589
|
+
const mergedProperties = withoutEmptyValues({
|
|
590
|
+
...existing.properties,
|
|
591
|
+
...changes.properties
|
|
592
|
+
});
|
|
593
|
+
const properties = parsePropertiesOrThrow(existing.kind, mergedProperties);
|
|
594
|
+
database.prepare("UPDATE nodes SET title = ?, body = ?, properties = ?, updated_at = ? WHERE id = ?").run(title ?? existing.title, changes.body ?? existing.body, JSON.stringify(properties), currentTimestamp(), id);
|
|
595
|
+
const node = requireNode(id);
|
|
596
|
+
writeSearchEntry(node);
|
|
597
|
+
return node;
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
const removeNode = (id) => {
|
|
601
|
+
requireNode(id);
|
|
602
|
+
transaction(() => {
|
|
603
|
+
database.prepare("DELETE FROM nodes WHERE id = ?").run(id);
|
|
604
|
+
database.prepare("DELETE FROM node_search WHERE id = ?").run(id);
|
|
605
|
+
});
|
|
606
|
+
};
|
|
607
|
+
const isReachable = (edgeType, startId, targetId) => {
|
|
608
|
+
return database.prepare(`
|
|
609
|
+
WITH RECURSIVE reachable (id) AS (
|
|
610
|
+
SELECT ?
|
|
611
|
+
UNION
|
|
612
|
+
SELECT edges.to_id FROM edges JOIN reachable ON edges.from_id = reachable.id WHERE edges.edge_type = ?
|
|
613
|
+
)
|
|
614
|
+
SELECT 1 AS found FROM reachable WHERE id = ? LIMIT 1`).get(startId, edgeType, targetId) !== void 0;
|
|
615
|
+
};
|
|
616
|
+
const findEdge = (fromId, edgeType, toId) => {
|
|
617
|
+
const row = database.prepare("SELECT * FROM edges WHERE edge_type = ? AND from_id = ? AND to_id = ?").get(edgeType, fromId, toId);
|
|
618
|
+
return row === void 0 ? null : toGraphEdge(row);
|
|
619
|
+
};
|
|
620
|
+
const link = (fromId, edgeType, toId) => {
|
|
621
|
+
const fromNode = requireNode(fromId);
|
|
622
|
+
const toNode = requireNode(toId);
|
|
623
|
+
return transaction(() => {
|
|
624
|
+
const existingEdge = findEdge(fromId, edgeType, toId);
|
|
625
|
+
if (existingEdge) return existingEdge;
|
|
626
|
+
const countRow = database.prepare("SELECT count(*) AS edge_count FROM edges WHERE edge_type = ? AND from_id = ?").get(edgeType, fromId);
|
|
627
|
+
const violation = findEdgeViolation(schema, {
|
|
628
|
+
edgeType,
|
|
629
|
+
fromId,
|
|
630
|
+
fromKind: fromNode.kind,
|
|
631
|
+
toId,
|
|
632
|
+
toKind: toNode.kind,
|
|
633
|
+
existingOutgoingCount: CountRow.parse(countRow).edge_count,
|
|
634
|
+
targetReachesSource: isReachable(edgeType, toId, fromId)
|
|
635
|
+
});
|
|
636
|
+
if (violation) throw new Error(violation);
|
|
637
|
+
const createdAt = currentTimestamp();
|
|
638
|
+
database.prepare("INSERT INTO edges (edge_type, from_id, to_id, created_at) VALUES (?, ?, ?, ?)").run(edgeType, fromId, toId, createdAt);
|
|
639
|
+
return {
|
|
640
|
+
edgeType,
|
|
641
|
+
fromId,
|
|
642
|
+
toId,
|
|
643
|
+
createdAt
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
const unlink = (fromId, edgeType, toId) => Number(database.prepare("DELETE FROM edges WHERE edge_type = ? AND from_id = ? AND to_id = ?").run(edgeType, fromId, toId).changes) > 0;
|
|
648
|
+
const refuseUnknownFilters = (kind, propertyFilters) => {
|
|
649
|
+
if (kind !== void 0 && !schema.kinds[kind]) throw new Error(describeUnknownKind(schema, kind));
|
|
650
|
+
const filterKeys = Object.keys(propertyFilters);
|
|
651
|
+
if (filterKeys.length === 0) return;
|
|
652
|
+
const knownKeysPerKind = (kind === void 0 ? Object.keys(schema.kinds) : [kind]).map((kindInScope) => listKnownPropertyKeys(schema, kindInScope));
|
|
653
|
+
if (knownKeysPerKind.includes(null)) return;
|
|
654
|
+
const knownKeys = new Set(knownKeysPerKind.flatMap((keys) => keys ?? []));
|
|
655
|
+
const unknownKey = filterKeys.find((key) => !knownKeys.has(key));
|
|
656
|
+
if (unknownKey !== void 0) throw new Error(`unknown field "${unknownKey}"${kind === void 0 ? "" : ` for ${kind}`}; known: ${[...knownKeys].join(", ")}`);
|
|
657
|
+
};
|
|
658
|
+
const listNodes = (kind, propertyFilters = {}) => {
|
|
659
|
+
refuseUnknownFilters(kind, propertyFilters);
|
|
660
|
+
const rows = kind === void 0 ? database.prepare("SELECT * FROM nodes ORDER BY created_at, id").all() : database.prepare("SELECT * FROM nodes WHERE kind = ? ORDER BY created_at, id").all(kind);
|
|
661
|
+
const filterEntries = Object.entries(propertyFilters);
|
|
662
|
+
return rows.map(toGraphNode).filter((node) => filterEntries.every(([key, expected]) => String(node.properties[key] ?? "") === expected));
|
|
663
|
+
};
|
|
664
|
+
const listEdges = (edgeType) => {
|
|
665
|
+
return (edgeType === void 0 ? database.prepare("SELECT * FROM edges ORDER BY created_at").all() : database.prepare("SELECT * FROM edges WHERE edge_type = ? ORDER BY created_at").all(edgeType)).map(toGraphEdge);
|
|
666
|
+
};
|
|
667
|
+
const outgoingEdges = (id) => database.prepare("SELECT * FROM edges WHERE from_id = ? ORDER BY edge_type, to_id").all(id).map(toGraphEdge);
|
|
668
|
+
const incomingEdges = (id) => database.prepare("SELECT * FROM edges WHERE to_id = ? ORDER BY edge_type, from_id").all(id).map(toGraphEdge);
|
|
669
|
+
const walkWithinSnapshot = (startId, edgeType, direction) => {
|
|
670
|
+
requireNode(startId);
|
|
671
|
+
if (!schema.edges[edgeType]) throw new Error(describeUnknownEdgeType(schema, edgeType));
|
|
672
|
+
const neighbourQuery = direction === "outgoing" ? database.prepare("SELECT to_id AS id FROM edges WHERE edge_type = ? AND from_id = ?") : database.prepare("SELECT from_id AS id FROM edges WHERE edge_type = ? AND to_id = ?");
|
|
673
|
+
const visitedIds = /* @__PURE__ */ new Set([startId]);
|
|
674
|
+
const steps = [];
|
|
675
|
+
let frontierIds = [startId];
|
|
676
|
+
for (let depth = 1; frontierIds.length > 0; depth += 1) {
|
|
677
|
+
const nextFrontierIds = [];
|
|
678
|
+
for (const nodeId of frontierIds) for (const row of neighbourQuery.all(edgeType, nodeId)) {
|
|
679
|
+
const neighbourId = NeighbourRow.parse(row).id;
|
|
680
|
+
if (visitedIds.has(neighbourId)) continue;
|
|
681
|
+
visitedIds.add(neighbourId);
|
|
682
|
+
nextFrontierIds.push(neighbourId);
|
|
683
|
+
}
|
|
684
|
+
nextFrontierIds.sort();
|
|
685
|
+
for (const nodeId of nextFrontierIds) steps.push({
|
|
686
|
+
node: requireNode(nodeId),
|
|
687
|
+
depth
|
|
688
|
+
});
|
|
689
|
+
frontierIds = nextFrontierIds;
|
|
690
|
+
}
|
|
691
|
+
return steps;
|
|
692
|
+
};
|
|
693
|
+
const walk = (startId, edgeType, direction) => runInTransaction(database, () => walkWithinSnapshot(startId, edgeType, direction), "BEGIN DEFERRED");
|
|
694
|
+
const search = (text, limit = 20) => {
|
|
695
|
+
const expression = toSearchExpression(text);
|
|
696
|
+
if (expression === "") return [];
|
|
697
|
+
return database.prepare("SELECT nodes.* FROM node_search JOIN nodes ON nodes.id = node_search.id WHERE node_search MATCH ? ORDER BY rank LIMIT ?").all(expression, limit).map(toGraphNode);
|
|
698
|
+
};
|
|
699
|
+
const checkIntegrity = () => findIntegrityViolations(schema, listNodes(), listEdges());
|
|
700
|
+
return {
|
|
701
|
+
schema,
|
|
702
|
+
addNode,
|
|
703
|
+
updateNode,
|
|
704
|
+
removeNode,
|
|
705
|
+
getNode,
|
|
706
|
+
requireNode,
|
|
707
|
+
link,
|
|
708
|
+
unlink,
|
|
709
|
+
listNodes,
|
|
710
|
+
listEdges,
|
|
711
|
+
outgoingEdges,
|
|
712
|
+
incomingEdges,
|
|
713
|
+
walk,
|
|
714
|
+
search,
|
|
715
|
+
checkIntegrity,
|
|
716
|
+
transaction,
|
|
717
|
+
close: () => database.close()
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region knowledge-graph/personal-schema.ts
|
|
722
|
+
var TaskStatus = z.enum([
|
|
723
|
+
"todo",
|
|
724
|
+
"doing",
|
|
725
|
+
"waiting",
|
|
726
|
+
"done",
|
|
727
|
+
"dropped"
|
|
728
|
+
]);
|
|
729
|
+
var ProjectStatus = z.enum([
|
|
730
|
+
"active",
|
|
731
|
+
"paused",
|
|
732
|
+
"done",
|
|
733
|
+
"archived"
|
|
734
|
+
]);
|
|
735
|
+
var TaskPriority = z.enum([
|
|
736
|
+
"p0",
|
|
737
|
+
"p1",
|
|
738
|
+
"p2",
|
|
739
|
+
"p3"
|
|
740
|
+
]);
|
|
741
|
+
var AbsoluteRepoPath = z.string().refine((repoPath) => posix.isAbsolute(repoPath) || win32.isAbsolute(repoPath), { message: "repo must be an absolute path" });
|
|
742
|
+
var personalSchema = {
|
|
743
|
+
name: "personal",
|
|
744
|
+
version: 1,
|
|
745
|
+
kinds: {
|
|
746
|
+
project: {
|
|
747
|
+
idPrefix: "P",
|
|
748
|
+
description: "A body of work with an outcome (a work project, a side project)",
|
|
749
|
+
properties: z.strictObject({
|
|
750
|
+
status: ProjectStatus.default("active"),
|
|
751
|
+
area: z.enum(["work", "personal"]).default("work")
|
|
752
|
+
})
|
|
753
|
+
},
|
|
754
|
+
task: {
|
|
755
|
+
idPrefix: "T",
|
|
756
|
+
description: "A concrete next action",
|
|
757
|
+
properties: z.strictObject({
|
|
758
|
+
status: TaskStatus.default("todo"),
|
|
759
|
+
priority: TaskPriority.default("p2"),
|
|
760
|
+
due: z.iso.date().optional()
|
|
761
|
+
})
|
|
762
|
+
},
|
|
763
|
+
note: {
|
|
764
|
+
idPrefix: "N",
|
|
765
|
+
description: "Knowledge you want to keep: a fact, a how-to, an insight, a log entry",
|
|
766
|
+
properties: z.strictObject({ noteType: z.enum([
|
|
767
|
+
"fact",
|
|
768
|
+
"howto",
|
|
769
|
+
"insight",
|
|
770
|
+
"log"
|
|
771
|
+
]).default("insight") })
|
|
772
|
+
},
|
|
773
|
+
reference: {
|
|
774
|
+
idPrefix: "R",
|
|
775
|
+
description: "An external source: article, paper, book, repo, doc, video, talk",
|
|
776
|
+
properties: z.strictObject({
|
|
777
|
+
medium: z.enum([
|
|
778
|
+
"article",
|
|
779
|
+
"paper",
|
|
780
|
+
"book",
|
|
781
|
+
"repo",
|
|
782
|
+
"doc",
|
|
783
|
+
"video",
|
|
784
|
+
"talk"
|
|
785
|
+
]).default("article"),
|
|
786
|
+
url: z.url().optional(),
|
|
787
|
+
author: z.string().min(1).optional()
|
|
788
|
+
})
|
|
789
|
+
},
|
|
790
|
+
question: {
|
|
791
|
+
idPrefix: "Q",
|
|
792
|
+
description: "An open research question",
|
|
793
|
+
properties: z.strictObject({ status: z.enum([
|
|
794
|
+
"open",
|
|
795
|
+
"answered",
|
|
796
|
+
"parked"
|
|
797
|
+
]).default("open") })
|
|
798
|
+
},
|
|
799
|
+
coherence_record: {
|
|
800
|
+
idPrefix: "C",
|
|
801
|
+
description: "A pointer to a ticket or decision in a repo's Coherence ledger; the ledger stays the source of truth",
|
|
802
|
+
properties: z.strictObject({
|
|
803
|
+
repo: AbsoluteRepoPath,
|
|
804
|
+
record: z.enum(["work", "decision"]),
|
|
805
|
+
recordId: z.string()
|
|
806
|
+
}).refine((pointer) => inferRecordKind(pointer.recordId) === pointer.record, {
|
|
807
|
+
message: "recordId does not match record kind",
|
|
808
|
+
path: ["recordId"]
|
|
809
|
+
})
|
|
810
|
+
},
|
|
811
|
+
topic: {
|
|
812
|
+
idPrefix: "TP",
|
|
813
|
+
description: "A subject area that cuts across projects",
|
|
814
|
+
properties: z.strictObject({})
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
edges: {
|
|
818
|
+
part_of: {
|
|
819
|
+
description: "Belongs to a project",
|
|
820
|
+
fromKinds: [
|
|
821
|
+
"task",
|
|
822
|
+
"note",
|
|
823
|
+
"reference",
|
|
824
|
+
"question"
|
|
825
|
+
],
|
|
826
|
+
toKinds: ["project"],
|
|
827
|
+
maxOutgoingPerNode: 1
|
|
828
|
+
},
|
|
829
|
+
subtask_of: {
|
|
830
|
+
description: "Is a step of a larger task",
|
|
831
|
+
fromKinds: ["task"],
|
|
832
|
+
toKinds: ["task"],
|
|
833
|
+
maxOutgoingPerNode: 1,
|
|
834
|
+
isAcyclic: true
|
|
835
|
+
},
|
|
836
|
+
blocks: {
|
|
837
|
+
description: "Must finish before the target task can start",
|
|
838
|
+
fromKinds: ["task"],
|
|
839
|
+
toKinds: ["task"],
|
|
840
|
+
isAcyclic: true
|
|
841
|
+
},
|
|
842
|
+
cites: {
|
|
843
|
+
description: "Draws on an external source",
|
|
844
|
+
fromKinds: [
|
|
845
|
+
"note",
|
|
846
|
+
"question",
|
|
847
|
+
"task",
|
|
848
|
+
"project"
|
|
849
|
+
],
|
|
850
|
+
toKinds: ["reference"]
|
|
851
|
+
},
|
|
852
|
+
answers: {
|
|
853
|
+
description: "Resolves a research question",
|
|
854
|
+
fromKinds: ["note"],
|
|
855
|
+
toKinds: ["question"]
|
|
856
|
+
},
|
|
857
|
+
supersedes: {
|
|
858
|
+
description: "Replaces an older note",
|
|
859
|
+
fromKinds: ["note"],
|
|
860
|
+
toKinds: ["note"],
|
|
861
|
+
maxOutgoingPerNode: 1,
|
|
862
|
+
isAcyclic: true
|
|
863
|
+
},
|
|
864
|
+
tracked_by: {
|
|
865
|
+
description: "Is carried out or settled by a record in a Coherence ledger",
|
|
866
|
+
fromKinds: [
|
|
867
|
+
"project",
|
|
868
|
+
"task",
|
|
869
|
+
"note",
|
|
870
|
+
"question"
|
|
871
|
+
],
|
|
872
|
+
toKinds: ["coherence_record"]
|
|
873
|
+
},
|
|
874
|
+
about: {
|
|
875
|
+
description: "Is about a topic",
|
|
876
|
+
fromKinds: "any",
|
|
877
|
+
toKinds: ["topic"]
|
|
878
|
+
},
|
|
879
|
+
relates_to: {
|
|
880
|
+
description: "Loosely related; prefer a specific edge when one fits",
|
|
881
|
+
fromKinds: "any",
|
|
882
|
+
toKinds: "any"
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var closedTaskStatuses = /* @__PURE__ */ new Set(["done", "dropped"]);
|
|
887
|
+
var actionableTaskStatuses = /* @__PURE__ */ new Set(["todo", "doing"]);
|
|
888
|
+
var priorityRank = {
|
|
889
|
+
p0: 0,
|
|
890
|
+
p1: 1,
|
|
891
|
+
p2: 2,
|
|
892
|
+
p3: 3
|
|
893
|
+
};
|
|
894
|
+
function isTaskClosed(status) {
|
|
895
|
+
return typeof status === "string" && closedTaskStatuses.has(status);
|
|
896
|
+
}
|
|
897
|
+
function rankNextActions(candidates) {
|
|
898
|
+
const isActionable = (candidate) => actionableTaskStatuses.has(candidate.status) && candidate.openBlockerIds.length === 0 && (candidate.projectStatus === void 0 || candidate.projectStatus === "active");
|
|
899
|
+
const compareCandidates = (left, right) => {
|
|
900
|
+
const statusOrder = Number(right.status === "doing") - Number(left.status === "doing");
|
|
901
|
+
if (statusOrder !== 0) return statusOrder;
|
|
902
|
+
const priorityOrder = (priorityRank[left.priority] ?? 9) - (priorityRank[right.priority] ?? 9);
|
|
903
|
+
if (priorityOrder !== 0) return priorityOrder;
|
|
904
|
+
return (left.due ?? "9999-12-31").localeCompare(right.due ?? "9999-12-31");
|
|
905
|
+
};
|
|
906
|
+
return candidates.filter(isActionable).toSorted(compareCandidates);
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region knowledge-graph/graph-views.ts
|
|
910
|
+
function readTaskHierarchy(store) {
|
|
911
|
+
const projectIdByNode = new Map(store.listEdges("part_of").map((edge) => [edge.fromId, edge.toId]));
|
|
912
|
+
const parentIdByTask = new Map(store.listEdges("subtask_of").map((edge) => [edge.fromId, edge.toId]));
|
|
913
|
+
const lineageOf = (nodeId) => {
|
|
914
|
+
const lineage = /* @__PURE__ */ new Set();
|
|
915
|
+
let currentId = nodeId;
|
|
916
|
+
while (currentId !== void 0 && !lineage.has(currentId)) {
|
|
917
|
+
lineage.add(currentId);
|
|
918
|
+
currentId = parentIdByTask.get(currentId);
|
|
919
|
+
}
|
|
920
|
+
return [...lineage];
|
|
921
|
+
};
|
|
922
|
+
const projectIdOf = (nodeId) => lineageOf(nodeId).map((lineageId) => projectIdByNode.get(lineageId)).find((projectId) => projectId !== void 0);
|
|
923
|
+
return {
|
|
924
|
+
lineageOf,
|
|
925
|
+
projectIdOf
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function collectNextActions(store) {
|
|
929
|
+
const tasks = store.listNodes("task");
|
|
930
|
+
const nodesById = new Map(store.listNodes().map((node) => [node.id, node]));
|
|
931
|
+
const openBlockerIdsByTask = /* @__PURE__ */ new Map();
|
|
932
|
+
for (const edge of store.listEdges("blocks")) {
|
|
933
|
+
if (isTaskClosed(nodesById.get(edge.fromId)?.properties.status)) continue;
|
|
934
|
+
openBlockerIdsByTask.set(edge.toId, [...openBlockerIdsByTask.get(edge.toId) ?? [], edge.fromId]);
|
|
935
|
+
}
|
|
936
|
+
const hierarchy = readTaskHierarchy(store);
|
|
937
|
+
return rankNextActions(tasks.map((task) => {
|
|
938
|
+
const projectId = hierarchy.projectIdOf(task.id);
|
|
939
|
+
const projectStatus = projectId === void 0 ? void 0 : nodesById.get(projectId)?.properties.status;
|
|
940
|
+
return {
|
|
941
|
+
id: task.id,
|
|
942
|
+
title: task.title,
|
|
943
|
+
status: String(task.properties.status),
|
|
944
|
+
priority: String(task.properties.priority),
|
|
945
|
+
due: typeof task.properties.due === "string" ? task.properties.due : void 0,
|
|
946
|
+
projectStatus: typeof projectStatus === "string" ? projectStatus : void 0,
|
|
947
|
+
openBlockerIds: openBlockerIdsByTask.get(task.id) ?? []
|
|
948
|
+
};
|
|
949
|
+
}));
|
|
950
|
+
}
|
|
951
|
+
function formatPropertyList(properties) {
|
|
952
|
+
return Object.entries(properties).map(([key, value]) => `${key}=${stripUnsafeTextCharacters(String(value))}`).join(" ");
|
|
953
|
+
}
|
|
954
|
+
function formatNodeLine(node) {
|
|
955
|
+
const propertyList = formatPropertyList(node.properties);
|
|
956
|
+
return `${node.id.padEnd(6)} ${node.kind.padEnd(9)} ${node.title}${propertyList === "" ? "" : ` [${propertyList}]`}`;
|
|
957
|
+
}
|
|
958
|
+
function quoteBody(body) {
|
|
959
|
+
if (body === "") return "";
|
|
960
|
+
return splitIntoSafeLines(body).map((line) => line === "" ? ">" : `> ${line}`).join("\n");
|
|
961
|
+
}
|
|
962
|
+
function renderMarkdown(schema, nodes, edges) {
|
|
963
|
+
const titleById = new Map(nodes.map((node) => [node.id, node.title]));
|
|
964
|
+
const describeEdge = (edge, perspectiveId) => {
|
|
965
|
+
const isOutgoing = edge.fromId === perspectiveId;
|
|
966
|
+
const otherId = isOutgoing ? edge.toId : edge.fromId;
|
|
967
|
+
return `- ${isOutgoing ? edge.edgeType : `${edge.edgeType} (from)`}: ${otherId} ${titleById.get(otherId) ?? ""}`.trimEnd();
|
|
968
|
+
};
|
|
969
|
+
const sections = Object.keys(schema.kinds).map((kind) => {
|
|
970
|
+
const kindNodes = nodes.filter((node) => node.kind === kind);
|
|
971
|
+
if (kindNodes.length === 0) return "";
|
|
972
|
+
const nodeBlocks = kindNodes.map((node) => {
|
|
973
|
+
const relatedEdges = edges.filter((edge) => edge.fromId === node.id || edge.toId === node.id);
|
|
974
|
+
return [
|
|
975
|
+
`### ${node.id} ${node.title}`,
|
|
976
|
+
formatPropertyList(node.properties),
|
|
977
|
+
...relatedEdges.map((edge) => describeEdge(edge, node.id)),
|
|
978
|
+
quoteBody(node.body)
|
|
979
|
+
].filter((line) => line !== "").join("\n");
|
|
980
|
+
});
|
|
981
|
+
return [`## ${kind}`, ...nodeBlocks].join("\n\n");
|
|
982
|
+
});
|
|
983
|
+
return `${[`# ${schema.name} graph`, ...sections.filter((section) => section !== "")].join("\n\n")}\n`;
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region knowledge-graph/cli.ts
|
|
987
|
+
var schemasByName = { personal: personalSchema };
|
|
988
|
+
var writeCommandNames = /* @__PURE__ */ new Set([
|
|
989
|
+
"add",
|
|
990
|
+
"set",
|
|
991
|
+
"rm",
|
|
992
|
+
"link",
|
|
993
|
+
"unlink",
|
|
994
|
+
"track"
|
|
995
|
+
]);
|
|
996
|
+
var SinceTimestamp = z.iso.datetime({ offset: true });
|
|
997
|
+
var usage = `glimmervoid kg: a typed, local-first knowledge and task graph (experimental)
|
|
998
|
+
|
|
999
|
+
glimmervoid kg schema kinds, their fields, and allowed edges
|
|
1000
|
+
glimmervoid kg add <kind> <title> [key=value...] create a node (--body text)
|
|
1001
|
+
glimmervoid kg set <id> [key=value...] change fields; title= and body= also work; key= clears
|
|
1002
|
+
glimmervoid kg rm <id> delete a node and its edges
|
|
1003
|
+
glimmervoid kg link <from> <edge> <to> add a typed edge, e.g. kg link T-1 blocks T-2
|
|
1004
|
+
glimmervoid kg unlink <from> <edge> <to>
|
|
1005
|
+
glimmervoid kg show <id> a node with every edge in and out
|
|
1006
|
+
glimmervoid kg ls [kind] [key=value...] list nodes, filtered by fields
|
|
1007
|
+
glimmervoid kg find <text> full-text search over titles and bodies
|
|
1008
|
+
glimmervoid kg next unblocked todo/doing tasks in active projects, ranked
|
|
1009
|
+
glimmervoid kg chain <id> [edge] [--incoming] walk an edge transitively; bare "kg chain T-3" lists everything blocking T-3
|
|
1010
|
+
glimmervoid kg check re-validate every node and edge against the schema
|
|
1011
|
+
glimmervoid kg export the whole graph as markdown
|
|
1012
|
+
glimmervoid kg track <id> <repo> <recordId> point a kg node at a Coherence ticket (wrk-...) or journal record (d-...)
|
|
1013
|
+
glimmervoid kg delta [id] [--since <iso time>] what the tracked Coherence records did, and where kg and the ledger disagree
|
|
1014
|
+
|
|
1015
|
+
--graph <name> which schema (default personal) --db <path> database file (default <glimmervoid home>/knowledge-graph/<graph>.sqlite)
|
|
1016
|
+
--json machine-readable output`;
|
|
1017
|
+
function splitAssignments(tokens) {
|
|
1018
|
+
const assignments = {};
|
|
1019
|
+
for (const token of tokens) {
|
|
1020
|
+
const separatorIndex = token.indexOf("=");
|
|
1021
|
+
if (separatorIndex <= 0) throw new Error(`expected key=value, got "${token}"`);
|
|
1022
|
+
assignments[token.slice(0, separatorIndex)] = token.slice(separatorIndex + 1);
|
|
1023
|
+
}
|
|
1024
|
+
return assignments;
|
|
1025
|
+
}
|
|
1026
|
+
function requirePositional(positionals, index, name) {
|
|
1027
|
+
const value = positionals[index];
|
|
1028
|
+
if (value === void 0) throw new Error(`missing <${name}>\n\n${usage}`);
|
|
1029
|
+
return value;
|
|
1030
|
+
}
|
|
1031
|
+
function readEdgeArguments(positionals) {
|
|
1032
|
+
return {
|
|
1033
|
+
fromId: requirePositional(positionals, 1, "from"),
|
|
1034
|
+
edgeType: requirePositional(positionals, 2, "edge"),
|
|
1035
|
+
toId: requirePositional(positionals, 3, "to")
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function collectTrackedRecords(store, scopeNodeId) {
|
|
1039
|
+
const nodesById = new Map(store.listNodes().map((node) => [node.id, node]));
|
|
1040
|
+
const hierarchy = readTaskHierarchy(store);
|
|
1041
|
+
const isInScope = (nodeId) => scopeNodeId === void 0 || hierarchy.lineageOf(nodeId).includes(scopeNodeId) || hierarchy.projectIdOf(nodeId) === scopeNodeId;
|
|
1042
|
+
return store.listEdges("tracked_by").filter((edge) => isInScope(edge.fromId)).flatMap((edge) => {
|
|
1043
|
+
const trackingNode = nodesById.get(edge.fromId);
|
|
1044
|
+
const pointer = nodesById.get(edge.toId);
|
|
1045
|
+
if (!trackingNode || !pointer) return [];
|
|
1046
|
+
const recordId = String(pointer.properties.recordId);
|
|
1047
|
+
const record = inferRecordKind(recordId);
|
|
1048
|
+
if (record === null) return [];
|
|
1049
|
+
const trackingStatus = trackingNode.properties.status;
|
|
1050
|
+
return [{
|
|
1051
|
+
trackingNodeId: trackingNode.id,
|
|
1052
|
+
trackingKind: trackingNode.kind,
|
|
1053
|
+
trackingStatus: typeof trackingStatus === "string" ? trackingStatus : void 0,
|
|
1054
|
+
recordNodeId: pointer.id,
|
|
1055
|
+
repo: String(pointer.properties.repo),
|
|
1056
|
+
record,
|
|
1057
|
+
recordId
|
|
1058
|
+
}];
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
function renderRepoReport(report) {
|
|
1062
|
+
const repoLabel = stripUnsafeTextCharacters(report.repo);
|
|
1063
|
+
if (!report.isAvailable) {
|
|
1064
|
+
const recordLines = report.records.map((tracked) => ` ${tracked.trackingNodeId} -> ${tracked.recordNodeId} ${tracked.recordId} (not checked)`);
|
|
1065
|
+
return [`${repoLabel} unavailable: ${report.reason}`, ...recordLines].join("\n");
|
|
1066
|
+
}
|
|
1067
|
+
const recordBlocks = report.records.map((recordReport) => [
|
|
1068
|
+
` ${recordReport.tracked.trackingNodeId} -> ${recordReport.tracked.recordNodeId} ${recordReport.tracked.record.padEnd(8)} ${recordReport.state.padEnd(16)} ${recordReport.label}`,
|
|
1069
|
+
...recordReport.findings.map((finding) => ` ! ${finding.kind}: ${finding.detail}`),
|
|
1070
|
+
...recordReport.decisions.map((decision) => ` ${decision.kind} ${decision.id} (${decision.standing}) ${decision.chose}`)
|
|
1071
|
+
].join("\n"));
|
|
1072
|
+
return [`${repoLabel} heading: ${report.heading}${report.headingReasons.length === 0 ? "" : ` (${report.headingReasons.join("; ")})`}`, ...recordBlocks].join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
function print(context, jsonValue, text) {
|
|
1075
|
+
context.writeOutput(`${context.isJson ? JSON.stringify(jsonValue, null, 2) : text}\n`);
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
function describeSchema(schema) {
|
|
1079
|
+
const kindLines = Object.entries(schema.kinds).map(([kind, definition]) => {
|
|
1080
|
+
const shape = (listKnownPropertyKeys(schema, kind) ?? []).join(", ");
|
|
1081
|
+
return ` ${kind} (${definition.idPrefix}-n): ${definition.description}${shape === "" ? "" : `\n fields: ${shape}`}`;
|
|
1082
|
+
});
|
|
1083
|
+
const edgeLines = Object.entries(schema.edges).map(([edgeType, definition]) => {
|
|
1084
|
+
const limits = [definition.maxOutgoingPerNode === void 0 ? "" : `max ${definition.maxOutgoingPerNode}`, definition.isAcyclic ? "acyclic" : ""].filter((limit) => limit !== "").join(", ");
|
|
1085
|
+
return ` ${edgeType}: ${describeAllowedKinds(definition.fromKinds)} -> ${describeAllowedKinds(definition.toKinds)}${limits === "" ? "" : ` (${limits})`} ${definition.description}`;
|
|
1086
|
+
});
|
|
1087
|
+
return [
|
|
1088
|
+
`graph "${schema.name}" v${schema.version}`,
|
|
1089
|
+
"kinds:",
|
|
1090
|
+
...kindLines,
|
|
1091
|
+
"edges:",
|
|
1092
|
+
...edgeLines
|
|
1093
|
+
].join("\n");
|
|
1094
|
+
}
|
|
1095
|
+
var commands = {
|
|
1096
|
+
schema: (context) => print(context, context.store.schema, describeSchema(context.store.schema)),
|
|
1097
|
+
add: (context) => {
|
|
1098
|
+
const kind = requirePositional(context.positionals, 1, "kind");
|
|
1099
|
+
const title = requirePositional(context.positionals, 2, "title");
|
|
1100
|
+
const node = context.store.addNode(kind, title, splitAssignments(context.positionals.slice(3)), context.body ?? "");
|
|
1101
|
+
print(context, node, formatNodeLine(node));
|
|
1102
|
+
},
|
|
1103
|
+
set: (context) => {
|
|
1104
|
+
const id = requirePositional(context.positionals, 1, "id");
|
|
1105
|
+
const { title, body, ...properties } = splitAssignments(context.positionals.slice(2));
|
|
1106
|
+
const node = context.store.updateNode(id, {
|
|
1107
|
+
title,
|
|
1108
|
+
body: context.body ?? body,
|
|
1109
|
+
properties
|
|
1110
|
+
});
|
|
1111
|
+
print(context, node, formatNodeLine(node));
|
|
1112
|
+
},
|
|
1113
|
+
rm: (context) => {
|
|
1114
|
+
const id = requirePositional(context.positionals, 1, "id");
|
|
1115
|
+
context.store.removeNode(id);
|
|
1116
|
+
print(context, { removed: id }, `removed ${id}`);
|
|
1117
|
+
},
|
|
1118
|
+
link: (context) => {
|
|
1119
|
+
const { fromId, edgeType, toId } = readEdgeArguments(context.positionals);
|
|
1120
|
+
const edge = context.store.link(fromId, edgeType, toId);
|
|
1121
|
+
print(context, edge, `${edge.fromId} ${edge.edgeType} ${edge.toId}`);
|
|
1122
|
+
},
|
|
1123
|
+
unlink: (context) => {
|
|
1124
|
+
const { fromId, edgeType, toId } = readEdgeArguments(context.positionals);
|
|
1125
|
+
const wasRemoved = context.store.unlink(fromId, edgeType, toId);
|
|
1126
|
+
print(context, { removed: wasRemoved }, wasRemoved ? "unlinked" : "no such edge");
|
|
1127
|
+
},
|
|
1128
|
+
show: (context) => {
|
|
1129
|
+
const node = context.store.requireNode(requirePositional(context.positionals, 1, "id"));
|
|
1130
|
+
const outgoing = context.store.outgoingEdges(node.id);
|
|
1131
|
+
const incoming = context.store.incomingEdges(node.id);
|
|
1132
|
+
const titleOf = (id) => context.store.getNode(id)?.title ?? "?";
|
|
1133
|
+
const lines = [
|
|
1134
|
+
formatNodeLine(node),
|
|
1135
|
+
...outgoing.map((edge) => ` -> ${edge.edgeType} ${edge.toId} ${titleOf(edge.toId)}`),
|
|
1136
|
+
...incoming.map((edge) => ` <- ${edge.edgeType} ${edge.fromId} ${titleOf(edge.fromId)}`),
|
|
1137
|
+
node.body === "" ? "" : `\n${splitIntoSafeLines(node.body).join("\n")}`
|
|
1138
|
+
];
|
|
1139
|
+
print(context, {
|
|
1140
|
+
node,
|
|
1141
|
+
outgoing,
|
|
1142
|
+
incoming
|
|
1143
|
+
}, lines.filter((line) => line !== "").join("\n"));
|
|
1144
|
+
},
|
|
1145
|
+
ls: (context) => {
|
|
1146
|
+
const kind = context.positionals[1]?.includes("=") ? void 0 : context.positionals[1];
|
|
1147
|
+
const filterTokens = context.positionals.slice(kind === void 0 ? 1 : 2);
|
|
1148
|
+
const nodes = context.store.listNodes(kind, splitAssignments(filterTokens));
|
|
1149
|
+
print(context, nodes, nodes.map(formatNodeLine).join("\n") || "(none)");
|
|
1150
|
+
},
|
|
1151
|
+
find: (context) => {
|
|
1152
|
+
const nodes = context.store.search(context.positionals.slice(1).join(" "));
|
|
1153
|
+
print(context, nodes, nodes.map(formatNodeLine).join("\n") || "(no matches)");
|
|
1154
|
+
},
|
|
1155
|
+
next: (context) => {
|
|
1156
|
+
const nextActions = collectNextActions(context.store);
|
|
1157
|
+
print(context, nextActions, nextActions.map((action) => `${action.id.padEnd(6)} ${action.priority} ${action.status.padEnd(5)} ${action.title}${action.due === void 0 ? "" : ` due ${action.due}`}`).join("\n") || "(nothing actionable)");
|
|
1158
|
+
},
|
|
1159
|
+
chain: (context) => {
|
|
1160
|
+
const id = requirePositional(context.positionals, 1, "id");
|
|
1161
|
+
const edgeType = context.positionals[2] ?? "blocks";
|
|
1162
|
+
const steps = context.store.walk(id, edgeType, context.isIncoming || context.positionals[2] === void 0 ? "incoming" : "outgoing");
|
|
1163
|
+
print(context, steps, steps.map((step) => `${" ".repeat(step.depth - 1)}${formatNodeLine(step.node)}`).join("\n") || "(empty)");
|
|
1164
|
+
},
|
|
1165
|
+
check: (context) => {
|
|
1166
|
+
const violations = context.store.checkIntegrity();
|
|
1167
|
+
print(context, violations, violations.join("\n") || "ok: every node and edge matches the schema");
|
|
1168
|
+
return violations.length > 0 ? 1 : 0;
|
|
1169
|
+
},
|
|
1170
|
+
track: (context) => {
|
|
1171
|
+
const trackingNode = context.store.requireNode(requirePositional(context.positionals, 1, "id"));
|
|
1172
|
+
const repo = realpathSync(resolve(requirePositional(context.positionals, 2, "repo")));
|
|
1173
|
+
const recordId = requirePositional(context.positionals, 3, "recordId");
|
|
1174
|
+
const record = inferRecordKind(recordId);
|
|
1175
|
+
if (record === null) throw new Error(`"${recordId}" is neither a ticket id (wrk-...) nor a decision id (d-...)`);
|
|
1176
|
+
const snapshot = readRepoSnapshot(repo, context.runCoherence);
|
|
1177
|
+
if (!snapshot.isAvailable) throw new Error(`cannot verify ${recordId}: ${snapshot.reason}`);
|
|
1178
|
+
const label = record === "work" ? snapshot.workOrders.find((workOrder) => workOrder.id === recordId)?.objective : snapshot.decisions.find((decision) => decision.id === recordId)?.chose;
|
|
1179
|
+
if (label === void 0) throw new Error(`${recordId} is not in the Coherence ledger at ${repo}`);
|
|
1180
|
+
const { pointer, edge } = context.store.transaction(() => {
|
|
1181
|
+
const trackedPointer = context.store.listNodes("coherence_record", {
|
|
1182
|
+
repo,
|
|
1183
|
+
recordId
|
|
1184
|
+
})[0] ?? context.store.addNode("coherence_record", label, {
|
|
1185
|
+
repo,
|
|
1186
|
+
record,
|
|
1187
|
+
recordId
|
|
1188
|
+
});
|
|
1189
|
+
return {
|
|
1190
|
+
pointer: trackedPointer,
|
|
1191
|
+
edge: context.store.link(trackingNode.id, "tracked_by", trackedPointer.id)
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
print(context, {
|
|
1195
|
+
pointer,
|
|
1196
|
+
edge
|
|
1197
|
+
}, `${trackingNode.id} tracked_by ${pointer.id} (${record} ${recordId}: ${label})`);
|
|
1198
|
+
},
|
|
1199
|
+
delta: (context) => {
|
|
1200
|
+
const scopeNodeId = context.positionals[1];
|
|
1201
|
+
if (scopeNodeId !== void 0) context.store.requireNode(scopeNodeId);
|
|
1202
|
+
const trackedRecords = collectTrackedRecords(context.store, scopeNodeId);
|
|
1203
|
+
const repos = [...new Set(trackedRecords.map((tracked) => tracked.repo))];
|
|
1204
|
+
const reports = buildCoherenceDelta(trackedRecords, new Map(repos.map((repo) => [repo, readRepoSnapshot(repo, context.runCoherence)])), context.since);
|
|
1205
|
+
print(context, reports, reports.map(renderRepoReport).join("\n\n") || "(nothing tracked; use glimmervoid kg track)");
|
|
1206
|
+
},
|
|
1207
|
+
export: (context) => {
|
|
1208
|
+
context.writeOutput(renderMarkdown(context.store.schema, context.store.listNodes(), context.store.listEdges()));
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
function resolveDatabasePath(requestedPath, commandName, defaultPath) {
|
|
1212
|
+
if (requestedPath === void 0) return defaultPath;
|
|
1213
|
+
if (requestedPath.trim() === "") throw new Error("--db needs a database file path");
|
|
1214
|
+
if (writeCommandNames.has(commandName) || existsSync(requestedPath)) return requestedPath;
|
|
1215
|
+
throw new Error(`no knowledge graph database at ${requestedPath}; check --db, or create it with a write such as kg add`);
|
|
1216
|
+
}
|
|
1217
|
+
function runCommandLine(commandArguments, dependencies) {
|
|
1218
|
+
const { values, positionals } = parseArgs({
|
|
1219
|
+
args: commandArguments,
|
|
1220
|
+
allowPositionals: true,
|
|
1221
|
+
options: {
|
|
1222
|
+
graph: {
|
|
1223
|
+
type: "string",
|
|
1224
|
+
default: "personal"
|
|
1225
|
+
},
|
|
1226
|
+
db: { type: "string" },
|
|
1227
|
+
body: { type: "string" },
|
|
1228
|
+
since: { type: "string" },
|
|
1229
|
+
incoming: {
|
|
1230
|
+
type: "boolean",
|
|
1231
|
+
default: false
|
|
1232
|
+
},
|
|
1233
|
+
json: {
|
|
1234
|
+
type: "boolean",
|
|
1235
|
+
default: false
|
|
1236
|
+
},
|
|
1237
|
+
help: {
|
|
1238
|
+
type: "boolean",
|
|
1239
|
+
short: "h",
|
|
1240
|
+
default: false
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
const commandName = positionals[0];
|
|
1245
|
+
const command = commandName === void 0 ? void 0 : commands[commandName];
|
|
1246
|
+
if (values.help || commandName === void 0 || command === void 0) {
|
|
1247
|
+
dependencies.writeOutput(`${usage}\n`);
|
|
1248
|
+
return values.help ? 0 : 1;
|
|
1249
|
+
}
|
|
1250
|
+
const schema = schemasByName[values.graph];
|
|
1251
|
+
if (!schema) throw new Error(`unknown graph "${values.graph}"; known: ${Object.keys(schemasByName).join(", ")}`);
|
|
1252
|
+
if (values.since !== void 0 && !SinceTimestamp.safeParse(values.since).success) throw new Error(`--since needs an ISO 8601 timestamp with Z or an offset, e.g. 2026-10-09T09:00:00Z; got "${values.since}"`);
|
|
1253
|
+
const databasePath = resolveDatabasePath(values.db, commandName, join(dependencies.defaultDatabaseDirectory, `${schema.name}.sqlite`));
|
|
1254
|
+
mkdirSync(dirname(databasePath), { recursive: true });
|
|
1255
|
+
const store = openGraphStore(databasePath, schema);
|
|
1256
|
+
try {
|
|
1257
|
+
return command({
|
|
1258
|
+
store,
|
|
1259
|
+
positionals,
|
|
1260
|
+
body: values.body,
|
|
1261
|
+
since: values.since,
|
|
1262
|
+
isIncoming: values.incoming,
|
|
1263
|
+
isJson: values.json,
|
|
1264
|
+
runCoherence: dependencies.runCoherence,
|
|
1265
|
+
writeOutput: dependencies.writeOutput
|
|
1266
|
+
}) ?? 0;
|
|
1267
|
+
} finally {
|
|
1268
|
+
store.close();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function runKnowledgeGraphCli(commandArguments, dependencies) {
|
|
1272
|
+
try {
|
|
1273
|
+
return runCommandLine(commandArguments, dependencies);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
dependencies.writeError(`kg: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1276
|
+
return 1;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region server/knowledge-graph-cli.ts
|
|
1281
|
+
var COHERENCE_TIMEOUT_MS = 3e4;
|
|
1282
|
+
var COHERENCE_MAX_BUFFER_BYTES = 16777216;
|
|
1283
|
+
var CoherenceRefusalExit = z.looseObject({
|
|
1284
|
+
status: z.literal(2),
|
|
1285
|
+
stdout: z.string().min(1)
|
|
1286
|
+
});
|
|
1287
|
+
var KnowledgeGraphGate = z.looseObject({ knowledgeGraph: BrowserConfig.shape.knowledgeGraph });
|
|
1288
|
+
var KNOWLEDGE_GRAPH_DISABLED_MESSAGE = "glimmervoid kg is experimental and off. Turn on Settings > Lanes > Knowledge graph, or set knowledgeGraph.enabled in config.json.";
|
|
1289
|
+
function decideKnowledgeGraphGate(configPath) {
|
|
1290
|
+
if (!configPath) return {
|
|
1291
|
+
isEnabled: false,
|
|
1292
|
+
loadError: null
|
|
1293
|
+
};
|
|
1294
|
+
try {
|
|
1295
|
+
const parsed = KnowledgeGraphGate.safeParse(JSON.parse(fs.readFileSync(configPath, "utf8")));
|
|
1296
|
+
return {
|
|
1297
|
+
isEnabled: parsed.success && parsed.data.knowledgeGraph?.enabled === true,
|
|
1298
|
+
loadError: null
|
|
1299
|
+
};
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
return {
|
|
1302
|
+
isEnabled: false,
|
|
1303
|
+
loadError: `Could not load ${configPath}: ${errorMessage(error)}`
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function runBundledCoherence(repo, commandArguments) {
|
|
1308
|
+
const coherenceCliPath = resolvePackageBin("@danilocampos/coherence", "coherence");
|
|
1309
|
+
if (!coherenceCliPath) throw new Error("Could not resolve the coherence CLI");
|
|
1310
|
+
try {
|
|
1311
|
+
return execFileSync(process.execPath, [coherenceCliPath, ...commandArguments], {
|
|
1312
|
+
cwd: repo,
|
|
1313
|
+
env: {
|
|
1314
|
+
...process.env,
|
|
1315
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
1316
|
+
},
|
|
1317
|
+
encoding: "utf8",
|
|
1318
|
+
timeout: COHERENCE_TIMEOUT_MS,
|
|
1319
|
+
maxBuffer: COHERENCE_MAX_BUFFER_BYTES,
|
|
1320
|
+
stdio: [
|
|
1321
|
+
"ignore",
|
|
1322
|
+
"pipe",
|
|
1323
|
+
"pipe"
|
|
1324
|
+
]
|
|
1325
|
+
});
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
const refusalExit = CoherenceRefusalExit.safeParse(error);
|
|
1328
|
+
if (!refusalExit.success) throw error;
|
|
1329
|
+
return refusalExit.data.stdout;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function runKnowledgeGraphCommand(commandArguments) {
|
|
1333
|
+
const homeDirectory = glimmervoidHomeDir(os.homedir(), process.env);
|
|
1334
|
+
const gate = decideKnowledgeGraphGate(decideConfigPath({
|
|
1335
|
+
env: process.env,
|
|
1336
|
+
homeDir: homeDirectory
|
|
1337
|
+
}, (candidate) => fs.existsSync(candidate)).path);
|
|
1338
|
+
if (gate.loadError) {
|
|
1339
|
+
console.error(`kg: ${gate.loadError}`);
|
|
1340
|
+
return 1;
|
|
1341
|
+
}
|
|
1342
|
+
if (!gate.isEnabled) {
|
|
1343
|
+
console.error(KNOWLEDGE_GRAPH_DISABLED_MESSAGE);
|
|
1344
|
+
return 1;
|
|
1345
|
+
}
|
|
1346
|
+
return runKnowledgeGraphCli(commandArguments, {
|
|
1347
|
+
defaultDatabaseDirectory: path.join(homeDirectory, "knowledge-graph"),
|
|
1348
|
+
runCoherence: runBundledCoherence,
|
|
1349
|
+
writeOutput: (text) => process.stdout.write(text),
|
|
1350
|
+
writeError: (text) => process.stderr.write(text)
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
//#endregion
|
|
1354
|
+
export { runKnowledgeGraphCommand };
|