@a-company/paradigm 3.9.0 → 3.11.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/dist/{accept-orchestration-DIGPJVUR.js → accept-orchestration-Z35I5AYN.js} +5 -5
- package/dist/{assessment-loader-T4GPBHLB.js → assessment-loader-C5EOUM47.js} +0 -1
- package/dist/{chunk-Y4XZWCHK.js → chunk-24AAVLME.js} +8 -8
- package/dist/{chunk-4N6AYEEA.js → chunk-3TWXFFZ3.js} +1 -1
- package/dist/{chunk-5S5CF3ER.js → chunk-4ZO3ZOPM.js} +19 -2141
- package/dist/{chunk-6RNYVBSG.js → chunk-CP6IZGUN.js} +4 -4
- package/dist/{chunk-M2XMTJHQ.js → chunk-DS5QY37M.js} +201 -287
- package/dist/chunk-F6EJKLF4.js +4971 -0
- package/dist/chunk-MW5DMGBB.js +255 -0
- package/dist/{chunk-KFHK6EBI.js → chunk-OSYMVGWX.js} +59 -3
- package/dist/{chunk-GY5KO3YZ.js → chunk-RDPXBMHK.js} +1 -1
- package/dist/{chunk-ADOBV4PH.js → chunk-UVI3OH3G.js} +6 -2127
- package/dist/{diff-J6C5IHPV.js → diff-PZAYCIAE.js} +5 -5
- package/dist/{dist-OLFOTUHS.js → dist-6SX5ZKKF.js} +2 -2
- package/dist/{dist-OMY7U6NR.js → dist-YB7T54QE.js} +1 -2
- package/dist/{doctor-TQYRF7KK.js → doctor-3YQ55536.js} +1 -1
- package/dist/drift-FH2UY64B.js +251 -0
- package/dist/{flow-7JUH6D4H.js → flow-MCKPJGRJ.js} +1 -1
- package/dist/{habits-ZJBAL4HD.js → habits-NC2TRMRV.js} +2 -2
- package/dist/{hooks-DLZEYHI3.js → hooks-JXYHVGIN.js} +1 -1
- package/dist/index.js +77 -51
- package/dist/mcp.js +5502 -9984
- package/dist/{orchestrate-FAV64G2R.js → orchestrate-BGRFBGBH.js} +5 -5
- package/dist/{plugin-update-checker-TWBWUSAG.js → plugin-update-checker-S3W4BUJO.js} +0 -1
- package/dist/portal-check-2HI4FFD6.js +42 -0
- package/dist/portal-compliance-KQCTAQTJ.js +18 -0
- package/dist/{providers-NQ67LO2Z.js → providers-IONB4YRJ.js} +1 -1
- package/dist/reindex-ZM6J53UP.js +11 -0
- package/dist/{sentinel-KDIGZWKT.js → sentinel-BGCISNIK.js} +1 -1
- package/dist/{server-NN7WDAZJ.js → server-3K3TTJH3.js} +1 -1
- package/dist/{shift-KJWSJLWN.js → shift-6I6N6RNK.js} +36 -8
- package/dist/{spawn-EO7B2UM3.js → spawn-WGFJ5RQZ.js} +5 -5
- package/dist/{task-loader-GUX4KS6N.js → task-loader-7M2FCBX6.js} +0 -1
- package/dist/{team-6CCNANKE.js → team-AFOKQ7YQ.js} +6 -6
- package/dist/{triage-B5W6GZLT.js → triage-MKKIWBSW.js} +2 -2
- package/dist/workspace-VBTW7OYL.js +271 -0
- package/package.json +2 -1
- package/dist/chunk-HPC3JAUP.js +0 -42
- /package/dist/{chunk-CCG6KYBT.js → chunk-5N5LR2KS.js} +0 -0
|
@@ -0,0 +1,4971 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../paradigm-mcp/src/tools/reindex.ts
|
|
4
|
+
import * as fs8 from "fs";
|
|
5
|
+
import * as path9 from "path";
|
|
6
|
+
import * as yaml7 from "js-yaml";
|
|
7
|
+
|
|
8
|
+
// ../paradigm-mcp/node_modules/.pnpm/@a-company+premise-core@0.2.0_typescript@5.9.3/node_modules/@a-company/premise-core/dist/index.js
|
|
9
|
+
import * as yaml3 from "js-yaml";
|
|
10
|
+
import { z as z2 } from "zod";
|
|
11
|
+
import * as path3 from "path";
|
|
12
|
+
|
|
13
|
+
// ../paradigm-mcp/node_modules/.pnpm/@a-company+purpose-core@3.5.0_typescript@5.9.3/node_modules/@a-company/purpose-core/dist/index.js
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as yaml from "js-yaml";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { glob } from "glob";
|
|
19
|
+
var PurposeItemSchema = z.object({
|
|
20
|
+
description: z.string(),
|
|
21
|
+
endpoints: z.array(z.string()).optional(),
|
|
22
|
+
tests: z.array(z.string()).optional(),
|
|
23
|
+
rules: z.record(z.unknown()).optional(),
|
|
24
|
+
aspects: z.array(z.string()).optional(),
|
|
25
|
+
// Symbol reference arrays
|
|
26
|
+
flows: z.array(z.string()).optional(),
|
|
27
|
+
gates: z.array(z.string()).optional(),
|
|
28
|
+
signals: z.array(z.string()).optional(),
|
|
29
|
+
states: z.array(z.string()).optional(),
|
|
30
|
+
components: z.array(z.string()).optional(),
|
|
31
|
+
// Extra fields preserved
|
|
32
|
+
tags: z.array(z.string()).optional(),
|
|
33
|
+
location: z.string().optional(),
|
|
34
|
+
locations: z.array(z.string()).optional(),
|
|
35
|
+
uses: z.array(z.string()).optional(),
|
|
36
|
+
"used-by": z.array(z.string()).optional(),
|
|
37
|
+
"used-for": z.array(z.string()).optional(),
|
|
38
|
+
exports: z.array(z.string()).optional(),
|
|
39
|
+
status: z.string().optional(),
|
|
40
|
+
properties: z.record(z.unknown()).optional(),
|
|
41
|
+
handles: z.array(z.string()).optional()
|
|
42
|
+
}).passthrough();
|
|
43
|
+
var PurposeItemArraySchema = PurposeItemSchema.extend({
|
|
44
|
+
id: z.string()
|
|
45
|
+
});
|
|
46
|
+
var SignalDefinitionObjectSchema = z.object({
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
category: z.string().optional(),
|
|
49
|
+
severity: z.enum(["info", "warn", "error"]).optional(),
|
|
50
|
+
emitters: z.array(z.string()).optional(),
|
|
51
|
+
related: z.array(z.string()).optional(),
|
|
52
|
+
data: z.record(z.unknown()).optional()
|
|
53
|
+
});
|
|
54
|
+
var SignalDefinitionSchema = z.union([
|
|
55
|
+
SignalDefinitionObjectSchema,
|
|
56
|
+
z.string().transform((desc) => ({ description: desc }))
|
|
57
|
+
]);
|
|
58
|
+
var RelationshipObjectSchema = z.object({
|
|
59
|
+
from: z.string(),
|
|
60
|
+
to: z.string(),
|
|
61
|
+
type: z.string(),
|
|
62
|
+
description: z.string().optional()
|
|
63
|
+
});
|
|
64
|
+
var RelationshipSchema = z.union([RelationshipObjectSchema, z.string()]);
|
|
65
|
+
var FlowStepObjectSchema = z.object({
|
|
66
|
+
component: z.string(),
|
|
67
|
+
action: z.string(),
|
|
68
|
+
description: z.string().optional()
|
|
69
|
+
});
|
|
70
|
+
var FlowStepSchema = z.union([FlowStepObjectSchema, z.string()]);
|
|
71
|
+
var FlowWithStepsSchema = z.object({
|
|
72
|
+
name: z.string(),
|
|
73
|
+
description: z.string().optional(),
|
|
74
|
+
steps: z.array(FlowStepSchema)
|
|
75
|
+
});
|
|
76
|
+
var FlowDefinitionSchema = z.object({
|
|
77
|
+
description: z.string().optional(),
|
|
78
|
+
gates: z.array(z.string()).optional(),
|
|
79
|
+
signals: z.array(z.string()).optional(),
|
|
80
|
+
components: z.array(z.string()).optional(),
|
|
81
|
+
steps: z.array(FlowStepSchema).optional()
|
|
82
|
+
});
|
|
83
|
+
var GateDefinitionSchema = z.object({
|
|
84
|
+
description: z.string().optional(),
|
|
85
|
+
requires: z.array(z.string()).optional(),
|
|
86
|
+
keys: z.array(z.string()).optional(),
|
|
87
|
+
signals: z.array(z.string()).optional()
|
|
88
|
+
});
|
|
89
|
+
var StateDefinitionSchema = z.object({
|
|
90
|
+
description: z.string().optional(),
|
|
91
|
+
default: z.unknown().optional(),
|
|
92
|
+
type: z.string().optional()
|
|
93
|
+
});
|
|
94
|
+
var AspectDefinitionSchema = z.object({
|
|
95
|
+
description: z.string().optional(),
|
|
96
|
+
tags: z.array(z.string()).optional(),
|
|
97
|
+
anchors: z.array(z.string()).optional(),
|
|
98
|
+
"applies-to": z.array(z.string()).optional(),
|
|
99
|
+
enforcement: z.string().optional()
|
|
100
|
+
});
|
|
101
|
+
var ReferenceSchema = z.object({
|
|
102
|
+
target: z.string(),
|
|
103
|
+
type: z.string(),
|
|
104
|
+
path: z.string()
|
|
105
|
+
});
|
|
106
|
+
var PurposeFileSchema = z.object({
|
|
107
|
+
version: z.string().optional(),
|
|
108
|
+
description: z.string().optional(),
|
|
109
|
+
apiSpec: z.string().optional(),
|
|
110
|
+
context: z.array(z.string()).optional(),
|
|
111
|
+
rules: z.record(z.unknown()).optional(),
|
|
112
|
+
// Support both array format [{ id, description }] and record format { id: { description } }
|
|
113
|
+
features: z.union([
|
|
114
|
+
z.array(PurposeItemArraySchema),
|
|
115
|
+
z.record(PurposeItemSchema)
|
|
116
|
+
]).optional(),
|
|
117
|
+
components: z.union([
|
|
118
|
+
z.array(PurposeItemArraySchema),
|
|
119
|
+
z.record(PurposeItemSchema)
|
|
120
|
+
]).optional(),
|
|
121
|
+
gates: z.record(GateDefinitionSchema).optional(),
|
|
122
|
+
states: z.record(StateDefinitionSchema).optional(),
|
|
123
|
+
signals: z.record(SignalDefinitionSchema).optional(),
|
|
124
|
+
aspects: z.record(AspectDefinitionSchema).optional(),
|
|
125
|
+
relationships: z.array(RelationshipSchema).optional(),
|
|
126
|
+
// Support both array format and record format for flows
|
|
127
|
+
flows: z.union([
|
|
128
|
+
z.array(FlowWithStepsSchema),
|
|
129
|
+
z.record(FlowDefinitionSchema)
|
|
130
|
+
]).optional(),
|
|
131
|
+
references: z.array(ReferenceSchema).optional()
|
|
132
|
+
});
|
|
133
|
+
function parsePurposeFile(filePath) {
|
|
134
|
+
const result = parsePurposeFileDetailed(filePath);
|
|
135
|
+
return { data: result.data, errors: result.errors };
|
|
136
|
+
}
|
|
137
|
+
function parsePurposeFileDetailed(filePath) {
|
|
138
|
+
const errors = [];
|
|
139
|
+
const detailedErrors = [];
|
|
140
|
+
let rawContent;
|
|
141
|
+
try {
|
|
142
|
+
rawContent = fs.readFileSync(filePath, "utf8");
|
|
143
|
+
} catch (e) {
|
|
144
|
+
const error = `Cannot read file: ${e.message}`;
|
|
145
|
+
errors.push(error);
|
|
146
|
+
detailedErrors.push({ message: error, type: "file" });
|
|
147
|
+
return { data: null, errors, detailedErrors, rawContent: void 0, isYamlValid: false };
|
|
148
|
+
}
|
|
149
|
+
const processedContent = rawContent.replace(/^([#~!$^][\w-]+):/gm, '"$1":').replace(/^(\s*-\s+)([!#][\w-]+)$/gm, '$1"$2"');
|
|
150
|
+
let data = null;
|
|
151
|
+
try {
|
|
152
|
+
data = yaml.load(processedContent);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const yamlError = e;
|
|
155
|
+
const line = yamlError.mark?.line ? yamlError.mark.line + 1 : void 0;
|
|
156
|
+
const message = `YAML syntax error: ${yamlError.reason || e.message}`;
|
|
157
|
+
errors.push(`${message}${line ? ` (line ${line})` : ""}`);
|
|
158
|
+
detailedErrors.push({
|
|
159
|
+
message,
|
|
160
|
+
line,
|
|
161
|
+
type: "yaml"
|
|
162
|
+
});
|
|
163
|
+
return { data: null, errors, detailedErrors, rawContent, isYamlValid: false };
|
|
164
|
+
}
|
|
165
|
+
if (data === null || data === void 0) {
|
|
166
|
+
return {
|
|
167
|
+
data: {},
|
|
168
|
+
errors: [],
|
|
169
|
+
detailedErrors: [],
|
|
170
|
+
rawContent,
|
|
171
|
+
isYamlValid: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (typeof data === "object" && data !== null) {
|
|
175
|
+
const obj = data;
|
|
176
|
+
const prefixMap = {
|
|
177
|
+
"#": "components",
|
|
178
|
+
"$": "flows",
|
|
179
|
+
"^": "gates",
|
|
180
|
+
"!": "signals",
|
|
181
|
+
"~": "aspects"
|
|
182
|
+
};
|
|
183
|
+
for (const key of Object.keys(obj)) {
|
|
184
|
+
const prefix = key[0];
|
|
185
|
+
const target = prefixMap[prefix];
|
|
186
|
+
if (!target || key.length < 2) continue;
|
|
187
|
+
const id = key.slice(1);
|
|
188
|
+
const value = obj[key];
|
|
189
|
+
if (typeof value !== "object" || value === null) continue;
|
|
190
|
+
const dict = obj[target] || {};
|
|
191
|
+
if (!(target in obj)) obj[target] = dict;
|
|
192
|
+
if (!(id in dict)) dict[id] = value;
|
|
193
|
+
delete obj[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const parseResult = PurposeFileSchema.safeParse(data);
|
|
197
|
+
if (!parseResult.success) {
|
|
198
|
+
for (const issue of parseResult.error.issues) {
|
|
199
|
+
const path22 = issue.path.join(".");
|
|
200
|
+
const message = issue.message;
|
|
201
|
+
errors.push(`Schema error at ${path22 || "/"}: ${message}`);
|
|
202
|
+
detailedErrors.push({
|
|
203
|
+
message,
|
|
204
|
+
path: path22 || "/",
|
|
205
|
+
type: "schema"
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return { data, errors, detailedErrors, rawContent, isYamlValid: true };
|
|
209
|
+
}
|
|
210
|
+
return { data: parseResult.data, errors: [], detailedErrors: [], rawContent, isYamlValid: true };
|
|
211
|
+
}
|
|
212
|
+
function serializePurposeFile(data) {
|
|
213
|
+
return yaml.dump(data, {
|
|
214
|
+
indent: 2,
|
|
215
|
+
lineWidth: -1,
|
|
216
|
+
noRefs: true,
|
|
217
|
+
sortKeys: false
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function normalizeItemsToEntries(items) {
|
|
221
|
+
if (!items) return [];
|
|
222
|
+
if (Array.isArray(items)) {
|
|
223
|
+
return items.map((item) => [item.id, item]);
|
|
224
|
+
} else {
|
|
225
|
+
return Object.entries(items);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function findPurposeFiles(rootDir) {
|
|
229
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
230
|
+
const files = await glob("**/.purpose", {
|
|
231
|
+
cwd: absoluteRoot,
|
|
232
|
+
absolute: true,
|
|
233
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
|
|
234
|
+
});
|
|
235
|
+
return files.sort((a, b) => {
|
|
236
|
+
const depthA = a.split(path.sep).length;
|
|
237
|
+
const depthB = b.split(path.sep).length;
|
|
238
|
+
return depthA - depthB;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function getAllPurposeFiles(rootDir) {
|
|
242
|
+
const files = await findPurposeFiles(rootDir);
|
|
243
|
+
const parsed = [];
|
|
244
|
+
for (const filePath of files) {
|
|
245
|
+
const { data, errors } = parsePurposeFile(filePath);
|
|
246
|
+
if (data) {
|
|
247
|
+
parsed.push({ filePath, data });
|
|
248
|
+
if (errors.length > 0) {
|
|
249
|
+
console.warn(`Warnings parsing ${filePath}:`, errors);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
function extractFeatures(parsedFiles) {
|
|
256
|
+
const features = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const { filePath, data } of parsedFiles) {
|
|
258
|
+
const entries = normalizeItemsToEntries(data.features);
|
|
259
|
+
for (const [id, item] of entries) {
|
|
260
|
+
features.set(id, { item, filePath });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return features;
|
|
264
|
+
}
|
|
265
|
+
function extractComponents(parsedFiles) {
|
|
266
|
+
const components = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const { filePath, data } of parsedFiles) {
|
|
268
|
+
const entries = normalizeItemsToEntries(data.components);
|
|
269
|
+
for (const [id, item] of entries) {
|
|
270
|
+
components.set(id, { item, filePath });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return components;
|
|
274
|
+
}
|
|
275
|
+
function extractGates(parsedFiles) {
|
|
276
|
+
const gates = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const { filePath, data } of parsedFiles) {
|
|
278
|
+
if (data.gates) {
|
|
279
|
+
for (const [id, item] of Object.entries(data.gates)) {
|
|
280
|
+
gates.set(id, { item, filePath });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return gates;
|
|
285
|
+
}
|
|
286
|
+
function extractStates(parsedFiles) {
|
|
287
|
+
const states = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const { filePath, data } of parsedFiles) {
|
|
289
|
+
if (data.states) {
|
|
290
|
+
for (const [id, item] of Object.entries(data.states)) {
|
|
291
|
+
states.set(id, { item, filePath });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return states;
|
|
296
|
+
}
|
|
297
|
+
function extractFlows(parsedFiles) {
|
|
298
|
+
const flows = /* @__PURE__ */ new Map();
|
|
299
|
+
for (const { filePath, data } of parsedFiles) {
|
|
300
|
+
if (data.flows) {
|
|
301
|
+
if (Array.isArray(data.flows)) {
|
|
302
|
+
for (const flow of data.flows) {
|
|
303
|
+
flows.set(flow.name, {
|
|
304
|
+
item: {
|
|
305
|
+
id: flow.name,
|
|
306
|
+
description: flow.description,
|
|
307
|
+
steps: flow.steps
|
|
308
|
+
},
|
|
309
|
+
filePath
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
for (const [id, flowDef] of Object.entries(data.flows)) {
|
|
314
|
+
flows.set(id, {
|
|
315
|
+
item: {
|
|
316
|
+
id,
|
|
317
|
+
description: flowDef.description,
|
|
318
|
+
gates: flowDef.gates,
|
|
319
|
+
signals: flowDef.signals,
|
|
320
|
+
components: flowDef.components,
|
|
321
|
+
steps: flowDef.steps
|
|
322
|
+
},
|
|
323
|
+
filePath
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return flows;
|
|
330
|
+
}
|
|
331
|
+
function extractSignals(parsedFiles) {
|
|
332
|
+
const signals = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const { filePath, data } of parsedFiles) {
|
|
334
|
+
if (data.signals) {
|
|
335
|
+
for (const [id, item] of Object.entries(data.signals)) {
|
|
336
|
+
signals.set(id, { item, filePath });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return signals;
|
|
341
|
+
}
|
|
342
|
+
function extractAspects(parsedFiles) {
|
|
343
|
+
const aspects = /* @__PURE__ */ new Map();
|
|
344
|
+
for (const { filePath, data } of parsedFiles) {
|
|
345
|
+
if (data.aspects) {
|
|
346
|
+
for (const [id, item] of Object.entries(data.aspects)) {
|
|
347
|
+
aspects.set(id, { item, filePath });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return aspects;
|
|
352
|
+
}
|
|
353
|
+
function extractSymbolReferences(parsedFiles) {
|
|
354
|
+
const refs = [];
|
|
355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const { filePath, data } of parsedFiles) {
|
|
357
|
+
const featureEntries = normalizeItemsToEntries(data.features);
|
|
358
|
+
for (const [id, item] of featureEntries) {
|
|
359
|
+
extractRefsFromItem(`#${id}`, item, filePath, refs, seen);
|
|
360
|
+
}
|
|
361
|
+
const componentEntries = normalizeItemsToEntries(data.components);
|
|
362
|
+
for (const [id, item] of componentEntries) {
|
|
363
|
+
extractRefsFromItem(`#${id}`, item, filePath, refs, seen);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return refs;
|
|
367
|
+
}
|
|
368
|
+
function extractRefsFromItem(sourceSymbol, item, filePath, refs, seen) {
|
|
369
|
+
if (item.flows) {
|
|
370
|
+
for (const flow of item.flows) {
|
|
371
|
+
const symbol = flow.startsWith("$") ? flow : `$${flow}`;
|
|
372
|
+
if (!seen.has(symbol)) {
|
|
373
|
+
seen.add(symbol);
|
|
374
|
+
refs.push({ symbol, type: "flow", sourceSymbol, filePath });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (item.gates) {
|
|
379
|
+
for (const gate of item.gates) {
|
|
380
|
+
const symbol = gate.startsWith("^") ? gate : `^${gate}`;
|
|
381
|
+
if (!seen.has(symbol)) {
|
|
382
|
+
seen.add(symbol);
|
|
383
|
+
refs.push({ symbol, type: "gate", sourceSymbol, filePath });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (item.signals) {
|
|
388
|
+
for (const signal of item.signals) {
|
|
389
|
+
const symbol = signal.startsWith("!") ? signal : `!${signal}`;
|
|
390
|
+
if (!seen.has(symbol)) {
|
|
391
|
+
seen.add(symbol);
|
|
392
|
+
refs.push({ symbol, type: "signal", sourceSymbol, filePath });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (item.states) {
|
|
397
|
+
for (const state of item.states) {
|
|
398
|
+
const symbol = state.startsWith("#") ? state : state.startsWith("%") ? `#${state.slice(1)}` : `#${state}`;
|
|
399
|
+
if (!seen.has(symbol)) {
|
|
400
|
+
seen.add(symbol);
|
|
401
|
+
refs.push({ symbol, type: "component", sourceSymbol, filePath });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (item.components) {
|
|
406
|
+
for (const comp of item.components) {
|
|
407
|
+
const symbol = comp.startsWith("#") ? comp : `#${comp}`;
|
|
408
|
+
if (!seen.has(symbol)) {
|
|
409
|
+
seen.add(symbol);
|
|
410
|
+
refs.push({ symbol, type: "component", sourceSymbol, filePath });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (item.aspects) {
|
|
415
|
+
for (const aspect of item.aspects) {
|
|
416
|
+
const symbol = aspect.startsWith("~") ? aspect : `~${aspect}`;
|
|
417
|
+
if (!seen.has(symbol)) {
|
|
418
|
+
seen.add(symbol);
|
|
419
|
+
refs.push({ symbol, type: "aspect", sourceSymbol, filePath });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (item.description) {
|
|
424
|
+
const descRefs = extractSymbolsFromText(item.description);
|
|
425
|
+
for (const { symbol, type } of descRefs) {
|
|
426
|
+
if (!seen.has(symbol)) {
|
|
427
|
+
seen.add(symbol);
|
|
428
|
+
refs.push({ symbol, type, sourceSymbol, filePath });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
var SYMBOL_BLOCKLIST = /* @__PURE__ */ new Set([
|
|
434
|
+
"$lib",
|
|
435
|
+
"$env",
|
|
436
|
+
"$app",
|
|
437
|
+
"$service-worker",
|
|
438
|
+
"$virtual",
|
|
439
|
+
"$schema",
|
|
440
|
+
"$ref",
|
|
441
|
+
"$id",
|
|
442
|
+
"$type"
|
|
443
|
+
]);
|
|
444
|
+
function extractSymbolsFromText(text) {
|
|
445
|
+
const results = [];
|
|
446
|
+
const pattern = /([$^!#~%])([a-zA-Z][a-zA-Z0-9._-]*)/g;
|
|
447
|
+
let match;
|
|
448
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
449
|
+
const prefix = match[1];
|
|
450
|
+
const id = match[2];
|
|
451
|
+
let symbol;
|
|
452
|
+
let type;
|
|
453
|
+
switch (prefix) {
|
|
454
|
+
case "#":
|
|
455
|
+
type = "component";
|
|
456
|
+
symbol = `#${id}`;
|
|
457
|
+
break;
|
|
458
|
+
case "$":
|
|
459
|
+
type = "flow";
|
|
460
|
+
symbol = `$${id}`;
|
|
461
|
+
break;
|
|
462
|
+
case "^":
|
|
463
|
+
type = "gate";
|
|
464
|
+
symbol = `^${id}`;
|
|
465
|
+
break;
|
|
466
|
+
case "!":
|
|
467
|
+
type = "signal";
|
|
468
|
+
symbol = `!${id}`;
|
|
469
|
+
break;
|
|
470
|
+
case "~":
|
|
471
|
+
type = "aspect";
|
|
472
|
+
symbol = `~${id}`;
|
|
473
|
+
break;
|
|
474
|
+
// Legacy: %state → #component
|
|
475
|
+
case "%":
|
|
476
|
+
type = "component";
|
|
477
|
+
symbol = `#${id}`;
|
|
478
|
+
break;
|
|
479
|
+
default:
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (SYMBOL_BLOCKLIST.has(symbol)) continue;
|
|
483
|
+
results.push({ symbol, type });
|
|
484
|
+
}
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
487
|
+
function normalizeToEntries(items) {
|
|
488
|
+
if (!items) return [];
|
|
489
|
+
if (Array.isArray(items)) {
|
|
490
|
+
return items.map((item) => [item.id, item]);
|
|
491
|
+
} else {
|
|
492
|
+
return Object.entries(items);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function getItemIds(items) {
|
|
496
|
+
if (!items) return [];
|
|
497
|
+
if (Array.isArray(items)) {
|
|
498
|
+
return items.map((item) => item.id);
|
|
499
|
+
} else {
|
|
500
|
+
return Object.keys(items);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function validatePurposeFile(data, filePath) {
|
|
504
|
+
const issues = [];
|
|
505
|
+
const prefix = filePath ? `${filePath}: ` : "";
|
|
506
|
+
const featureEntries = normalizeToEntries(data.features);
|
|
507
|
+
for (const [id, feature] of featureEntries) {
|
|
508
|
+
validatePurposeItem(id, feature, "feature", prefix, issues);
|
|
509
|
+
}
|
|
510
|
+
const componentEntries = normalizeToEntries(data.components);
|
|
511
|
+
for (const [id, component] of componentEntries) {
|
|
512
|
+
validatePurposeItem(id, component, "component", prefix, issues);
|
|
513
|
+
}
|
|
514
|
+
if (data.relationships) {
|
|
515
|
+
const allIds = /* @__PURE__ */ new Set([
|
|
516
|
+
...getItemIds(data.features),
|
|
517
|
+
...getItemIds(data.components)
|
|
518
|
+
]);
|
|
519
|
+
for (const rel of data.relationships) {
|
|
520
|
+
if (typeof rel === "string" || !rel || !rel.from || !rel.to) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const fromId = rel.from.replace(/^[@#$%~^!?]/, "");
|
|
524
|
+
if (!allIds.has(fromId) && !rel.from.includes(".")) {
|
|
525
|
+
issues.push({
|
|
526
|
+
type: "warning",
|
|
527
|
+
message: `${prefix}Relationship references unknown source: "${rel.from}"`,
|
|
528
|
+
path: "relationships"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const toId = rel.to.replace(/^[@#$%~^!?]/, "");
|
|
532
|
+
if (!allIds.has(toId) && !rel.to.includes(".")) {
|
|
533
|
+
issues.push({
|
|
534
|
+
type: "warning",
|
|
535
|
+
message: `${prefix}Relationship references unknown target: "${rel.to}"`,
|
|
536
|
+
path: "relationships"
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (data.flows) {
|
|
542
|
+
const componentIds = new Set(getItemIds(data.components));
|
|
543
|
+
if (Array.isArray(data.flows)) {
|
|
544
|
+
for (const flow of data.flows) {
|
|
545
|
+
if (!flow || typeof flow !== "object") continue;
|
|
546
|
+
if (!flow.name) {
|
|
547
|
+
issues.push({
|
|
548
|
+
type: "error",
|
|
549
|
+
message: `${prefix}Flow missing required "name" field`,
|
|
550
|
+
path: "flows"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (flow.steps && Array.isArray(flow.steps)) {
|
|
554
|
+
for (const step of flow.steps) {
|
|
555
|
+
if (typeof step === "string" || !step || !step.component) continue;
|
|
556
|
+
const componentId = step.component.replace(/^#/, "");
|
|
557
|
+
if (!componentIds.has(componentId)) {
|
|
558
|
+
issues.push({
|
|
559
|
+
type: "warning",
|
|
560
|
+
message: `${prefix}Flow "${flow.name}" references unknown component: "${step.component}"`,
|
|
561
|
+
path: `flows.${flow.name}`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
for (const [flowId, flowDef] of Object.entries(data.flows)) {
|
|
569
|
+
if (!flowDef || typeof flowDef !== "object") continue;
|
|
570
|
+
if (flowDef.steps && Array.isArray(flowDef.steps)) {
|
|
571
|
+
for (const step of flowDef.steps) {
|
|
572
|
+
if (typeof step === "string" || !step || !step.component) continue;
|
|
573
|
+
const componentId = step.component.replace(/^#/, "");
|
|
574
|
+
if (!componentIds.has(componentId)) {
|
|
575
|
+
issues.push({
|
|
576
|
+
type: "warning",
|
|
577
|
+
message: `${prefix}Flow "${flowId}" references unknown component: "${step.component}"`,
|
|
578
|
+
path: `flows.${flowId}`
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
valid: issues.filter((i) => i.type === "error").length === 0,
|
|
588
|
+
issues
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function validatePurposeItem(id, item, itemType, prefix, issues) {
|
|
592
|
+
const path22 = `${itemType}s.${id}`;
|
|
593
|
+
if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(id)) {
|
|
594
|
+
issues.push({
|
|
595
|
+
type: "warning",
|
|
596
|
+
message: `${prefix}${itemType} ID "${id}" should use alphanumeric characters and hyphens`,
|
|
597
|
+
path: path22
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (!item.description || item.description.trim() === "") {
|
|
601
|
+
issues.push({
|
|
602
|
+
type: "warning",
|
|
603
|
+
message: `${prefix}${itemType} "${id}" has no description`,
|
|
604
|
+
path: path22
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
if (item.endpoints) {
|
|
608
|
+
for (const endpoint of item.endpoints) {
|
|
609
|
+
if (!/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\//.test(endpoint)) {
|
|
610
|
+
issues.push({
|
|
611
|
+
type: "warning",
|
|
612
|
+
message: `${prefix}Endpoint "${endpoint}" in ${itemType} "${id}" may not be in standard format (e.g., "GET /api/users")`,
|
|
613
|
+
path: `${path22}.endpoints`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ../paradigm-mcp/node_modules/.pnpm/@a-company+portal-core@0.1.0/node_modules/@a-company/portal-core/dist/index.js
|
|
621
|
+
import * as fs2 from "fs";
|
|
622
|
+
import * as path2 from "path";
|
|
623
|
+
import * as yaml2 from "js-yaml";
|
|
624
|
+
import { glob as glob2 } from "glob";
|
|
625
|
+
var DEFAULT_DEV_SETTINGS = {
|
|
626
|
+
visualizerPort: 42195,
|
|
627
|
+
// Marathon: 42.195km
|
|
628
|
+
watcherPort: 42196,
|
|
629
|
+
// Marathon + 1
|
|
630
|
+
autoConnect: true
|
|
631
|
+
};
|
|
632
|
+
async function parseGateConfig(configPath) {
|
|
633
|
+
const absolutePath = path2.resolve(configPath);
|
|
634
|
+
const rootDir = path2.dirname(absolutePath);
|
|
635
|
+
if (!fs2.existsSync(absolutePath)) {
|
|
636
|
+
throw new Error(`Gate config not found: ${absolutePath}`);
|
|
637
|
+
}
|
|
638
|
+
const content = fs2.readFileSync(absolutePath, "utf8");
|
|
639
|
+
const config = yaml2.load(content);
|
|
640
|
+
if (!config.version) {
|
|
641
|
+
throw new Error('Gate config missing required "version" field');
|
|
642
|
+
}
|
|
643
|
+
const gates = [];
|
|
644
|
+
const configAny = config;
|
|
645
|
+
const gatesSource = config.gates || configAny.portals;
|
|
646
|
+
if (gatesSource) {
|
|
647
|
+
for (const [id, gateDef] of Object.entries(gatesSource)) {
|
|
648
|
+
gates.push(normalizeGate(id, gateDef));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (config.include) {
|
|
652
|
+
for (const pattern of config.include) {
|
|
653
|
+
const fullPattern = path2.join(rootDir, pattern);
|
|
654
|
+
const files = await glob2(fullPattern.replace(/\\/g, "/"));
|
|
655
|
+
for (const file of files) {
|
|
656
|
+
const additionalGates = await parseGateFile(file);
|
|
657
|
+
gates.push(...additionalGates);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const flows = [];
|
|
662
|
+
if (config.flows) {
|
|
663
|
+
for (const [id, flowDef] of Object.entries(config.flows)) {
|
|
664
|
+
flows.push(normalizeFlow(id, flowDef));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
version: config.version,
|
|
669
|
+
gates,
|
|
670
|
+
flows,
|
|
671
|
+
settings: {
|
|
672
|
+
dev: {
|
|
673
|
+
...DEFAULT_DEV_SETTINGS,
|
|
674
|
+
...config.settings?.dev
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
async function parseGateFile(filePath) {
|
|
680
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
681
|
+
const data = yaml2.load(content);
|
|
682
|
+
if (data.id) {
|
|
683
|
+
return [normalizeGate(data.id, data)];
|
|
684
|
+
}
|
|
685
|
+
if (data.gates) {
|
|
686
|
+
const gates = [];
|
|
687
|
+
for (const [id, gateDef] of Object.entries(data.gates)) {
|
|
688
|
+
gates.push(normalizeGate(id, gateDef));
|
|
689
|
+
}
|
|
690
|
+
return gates;
|
|
691
|
+
}
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
function normalizeGate(id, def) {
|
|
695
|
+
const locks = [];
|
|
696
|
+
if (def.locks) {
|
|
697
|
+
for (const lockDef of def.locks) {
|
|
698
|
+
locks.push(normalizeLock(lockDef));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const prizes = [];
|
|
702
|
+
if (def.prizes) {
|
|
703
|
+
for (const prizeDef of def.prizes) {
|
|
704
|
+
prizes.push(normalizePrize(prizeDef));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
id,
|
|
709
|
+
description: def.description,
|
|
710
|
+
locks,
|
|
711
|
+
prizes,
|
|
712
|
+
position: def.position
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function normalizeLock(def) {
|
|
716
|
+
const lockDef = def;
|
|
717
|
+
const keys = [];
|
|
718
|
+
if (lockDef.keys) {
|
|
719
|
+
for (const keyDef of lockDef.keys) {
|
|
720
|
+
if (typeof keyDef === "string") {
|
|
721
|
+
keys.push({ expression: keyDef });
|
|
722
|
+
} else if (keyDef.expression) {
|
|
723
|
+
const k = keyDef;
|
|
724
|
+
keys.push({
|
|
725
|
+
expression: k.expression,
|
|
726
|
+
description: k.description
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
id: lockDef.id,
|
|
733
|
+
description: lockDef.description,
|
|
734
|
+
keys,
|
|
735
|
+
mode: lockDef.mode || "all"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function normalizePrize(def) {
|
|
739
|
+
const prizeDef = def;
|
|
740
|
+
return {
|
|
741
|
+
id: prizeDef.id,
|
|
742
|
+
oneTime: prizeDef.oneTime ?? false,
|
|
743
|
+
metadata: prizeDef.metadata
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function normalizeFlow(id, def) {
|
|
747
|
+
return {
|
|
748
|
+
id,
|
|
749
|
+
description: def.description,
|
|
750
|
+
gates: def.gates || [],
|
|
751
|
+
forkable: def.forkable
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function findGateFiles(rootDir) {
|
|
755
|
+
const absoluteRoot = path2.resolve(rootDir);
|
|
756
|
+
const files = await glob2("**/portal.yaml", {
|
|
757
|
+
cwd: absoluteRoot,
|
|
758
|
+
absolute: true,
|
|
759
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
|
|
760
|
+
});
|
|
761
|
+
return files;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ../paradigm-mcp/node_modules/.pnpm/@a-company+premise-core@0.2.0_typescript@5.9.3/node_modules/@a-company/premise-core/dist/index.js
|
|
765
|
+
var PositionSchema = z2.object({
|
|
766
|
+
x: z2.number(),
|
|
767
|
+
y: z2.number()
|
|
768
|
+
});
|
|
769
|
+
var ViewportSchema = z2.object({
|
|
770
|
+
x: z2.number(),
|
|
771
|
+
y: z2.number(),
|
|
772
|
+
zoom: z2.number()
|
|
773
|
+
});
|
|
774
|
+
var PremiseSourceConfigSchema = z2.object({
|
|
775
|
+
path: z2.string(),
|
|
776
|
+
include: z2.array(z2.string()).optional(),
|
|
777
|
+
exclude: z2.array(z2.string()).optional()
|
|
778
|
+
});
|
|
779
|
+
var PremiseNodeSchema = z2.object({
|
|
780
|
+
id: z2.string(),
|
|
781
|
+
symbol: z2.string(),
|
|
782
|
+
type: z2.enum(["feature", "component", "flow", "state", "aspect", "gate", "signal", "idea"]),
|
|
783
|
+
content: z2.string().optional(),
|
|
784
|
+
position: PositionSchema,
|
|
785
|
+
tags: z2.array(z2.string()).optional(),
|
|
786
|
+
created: z2.string(),
|
|
787
|
+
modified: z2.string().optional()
|
|
788
|
+
});
|
|
789
|
+
var PremiseConnectionSchema = z2.object({
|
|
790
|
+
from: z2.string(),
|
|
791
|
+
to: z2.string(),
|
|
792
|
+
label: z2.string().optional(),
|
|
793
|
+
type: z2.string().optional()
|
|
794
|
+
});
|
|
795
|
+
var PremiseGroupSchema = z2.object({
|
|
796
|
+
id: z2.string(),
|
|
797
|
+
name: z2.string(),
|
|
798
|
+
nodes: z2.array(z2.string()),
|
|
799
|
+
color: z2.string().optional()
|
|
800
|
+
});
|
|
801
|
+
var PremiseLayoutSchema = z2.object({
|
|
802
|
+
viewport: ViewportSchema,
|
|
803
|
+
groups: z2.array(PremiseGroupSchema).optional()
|
|
804
|
+
});
|
|
805
|
+
var PremiseSnapshotStateSchema = z2.object({
|
|
806
|
+
nodes: z2.array(PremiseNodeSchema),
|
|
807
|
+
connections: z2.array(PremiseConnectionSchema),
|
|
808
|
+
layout: PremiseLayoutSchema
|
|
809
|
+
});
|
|
810
|
+
var PremiseSnapshotSchema = z2.object({
|
|
811
|
+
id: z2.string(),
|
|
812
|
+
name: z2.string(),
|
|
813
|
+
timestamp: z2.string(),
|
|
814
|
+
description: z2.string().optional(),
|
|
815
|
+
state: PremiseSnapshotStateSchema
|
|
816
|
+
});
|
|
817
|
+
var PremiseFileSchema = z2.object({
|
|
818
|
+
version: z2.string(),
|
|
819
|
+
metadata: z2.object({
|
|
820
|
+
name: z2.string(),
|
|
821
|
+
created: z2.string(),
|
|
822
|
+
modified: z2.string()
|
|
823
|
+
}),
|
|
824
|
+
sources: z2.object({
|
|
825
|
+
purpose: z2.array(PremiseSourceConfigSchema).optional(),
|
|
826
|
+
portal: z2.array(PremiseSourceConfigSchema).optional()
|
|
827
|
+
}),
|
|
828
|
+
nodes: z2.array(PremiseNodeSchema),
|
|
829
|
+
connections: z2.array(PremiseConnectionSchema),
|
|
830
|
+
layout: PremiseLayoutSchema,
|
|
831
|
+
snapshots: z2.array(PremiseSnapshotSchema).optional()
|
|
832
|
+
});
|
|
833
|
+
async function aggregateFromPremise(premiseFile, rootDir) {
|
|
834
|
+
const symbols = [];
|
|
835
|
+
const errors = [];
|
|
836
|
+
const purposeFiles = [];
|
|
837
|
+
const portalFiles = [];
|
|
838
|
+
if (premiseFile.sources.purpose) {
|
|
839
|
+
for (const source of premiseFile.sources.purpose) {
|
|
840
|
+
const sourcePath = path3.resolve(rootDir, source.path);
|
|
841
|
+
try {
|
|
842
|
+
const parsed = await getAllPurposeFiles(sourcePath);
|
|
843
|
+
purposeFiles.push(...parsed.map((p) => p.filePath));
|
|
844
|
+
const features = extractFeatures(parsed);
|
|
845
|
+
for (const [id, { item, filePath }] of features) {
|
|
846
|
+
symbols.push(createSymbolEntry({
|
|
847
|
+
id: `purpose-feature-${id}`,
|
|
848
|
+
symbol: `#${id}`,
|
|
849
|
+
type: "component",
|
|
850
|
+
source: "purpose",
|
|
851
|
+
filePath,
|
|
852
|
+
data: item,
|
|
853
|
+
description: item.description,
|
|
854
|
+
tags: ["feature"]
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
const components = extractComponents(parsed);
|
|
858
|
+
for (const [id, { item, filePath }] of components) {
|
|
859
|
+
symbols.push(createSymbolEntry({
|
|
860
|
+
id: `purpose-component-${id}`,
|
|
861
|
+
symbol: `#${id}`,
|
|
862
|
+
type: "component",
|
|
863
|
+
source: "purpose",
|
|
864
|
+
filePath,
|
|
865
|
+
data: item,
|
|
866
|
+
description: item.description
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
const gates = extractGates(parsed);
|
|
870
|
+
for (const [id, { item, filePath }] of gates) {
|
|
871
|
+
symbols.push(createSymbolEntry({
|
|
872
|
+
id: `purpose-gate-${id}`,
|
|
873
|
+
symbol: `^${id}`,
|
|
874
|
+
type: "gate",
|
|
875
|
+
source: "purpose",
|
|
876
|
+
filePath,
|
|
877
|
+
data: item,
|
|
878
|
+
description: item.description
|
|
879
|
+
}));
|
|
880
|
+
}
|
|
881
|
+
const states = extractStates(parsed);
|
|
882
|
+
for (const [id, { item, filePath }] of states) {
|
|
883
|
+
symbols.push(createSymbolEntry({
|
|
884
|
+
id: `purpose-state-${id}`,
|
|
885
|
+
symbol: `#${id}`,
|
|
886
|
+
type: "component",
|
|
887
|
+
source: "purpose",
|
|
888
|
+
filePath,
|
|
889
|
+
data: item,
|
|
890
|
+
description: item.description,
|
|
891
|
+
tags: ["state"]
|
|
892
|
+
}));
|
|
893
|
+
}
|
|
894
|
+
const flows = extractFlows(parsed);
|
|
895
|
+
for (const [id, { item, filePath }] of flows) {
|
|
896
|
+
symbols.push(createSymbolEntry({
|
|
897
|
+
id: `purpose-flow-${id}`,
|
|
898
|
+
symbol: `$${id}`,
|
|
899
|
+
type: "flow",
|
|
900
|
+
source: "purpose",
|
|
901
|
+
filePath,
|
|
902
|
+
data: item,
|
|
903
|
+
description: item.description
|
|
904
|
+
}));
|
|
905
|
+
}
|
|
906
|
+
const signals = extractSignals(parsed);
|
|
907
|
+
for (const [id, { item, filePath }] of signals) {
|
|
908
|
+
symbols.push(createSymbolEntry({
|
|
909
|
+
id: `purpose-signal-${id}`,
|
|
910
|
+
symbol: `!${id}`,
|
|
911
|
+
type: "signal",
|
|
912
|
+
source: "purpose",
|
|
913
|
+
filePath,
|
|
914
|
+
data: item,
|
|
915
|
+
description: item.description
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
const aspects = extractAspects(parsed);
|
|
919
|
+
for (const [id, { item, filePath }] of aspects) {
|
|
920
|
+
symbols.push(createSymbolEntry({
|
|
921
|
+
id: `purpose-aspect-${id}`,
|
|
922
|
+
symbol: `~${id}`,
|
|
923
|
+
type: "aspect",
|
|
924
|
+
source: "purpose",
|
|
925
|
+
filePath,
|
|
926
|
+
data: item,
|
|
927
|
+
description: item.description,
|
|
928
|
+
anchors: item.anchors?.map((a) => parseAnchorString(a)),
|
|
929
|
+
appliesTo: item["applies-to"]
|
|
930
|
+
}));
|
|
931
|
+
}
|
|
932
|
+
const symbolRefs = extractSymbolReferences(parsed);
|
|
933
|
+
const existingSymbols = new Set(symbols.map((s) => s.symbol));
|
|
934
|
+
for (const ref of symbolRefs) {
|
|
935
|
+
if (!existingSymbols.has(ref.symbol)) {
|
|
936
|
+
existingSymbols.add(ref.symbol);
|
|
937
|
+
symbols.push(createSymbolEntry({
|
|
938
|
+
id: `purpose-ref-${ref.type}-${ref.symbol.slice(1)}`,
|
|
939
|
+
symbol: ref.symbol,
|
|
940
|
+
type: ref.type,
|
|
941
|
+
source: "purpose",
|
|
942
|
+
filePath: ref.filePath,
|
|
943
|
+
data: { referencedFrom: ref.sourceSymbol },
|
|
944
|
+
description: `Referenced from ${ref.sourceSymbol}`
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
} catch (e) {
|
|
949
|
+
errors.push({
|
|
950
|
+
source: "purpose",
|
|
951
|
+
filePath: sourcePath,
|
|
952
|
+
message: e.message
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (premiseFile.sources.portal) {
|
|
958
|
+
for (const source of premiseFile.sources.portal) {
|
|
959
|
+
const sourcePath = path3.resolve(rootDir, source.path);
|
|
960
|
+
try {
|
|
961
|
+
let gateConfig;
|
|
962
|
+
if (sourcePath.endsWith(".yaml") || sourcePath.endsWith(".yml")) {
|
|
963
|
+
gateConfig = await parseGateConfig(sourcePath);
|
|
964
|
+
portalFiles.push(sourcePath);
|
|
965
|
+
} else {
|
|
966
|
+
const files = await findGateFiles(sourcePath);
|
|
967
|
+
portalFiles.push(...files);
|
|
968
|
+
if (files.length > 0) {
|
|
969
|
+
gateConfig = await parseGateConfig(files[0]);
|
|
970
|
+
for (let i = 1; i < files.length; i++) {
|
|
971
|
+
const additional = await parseGateConfig(files[i]);
|
|
972
|
+
gateConfig.gates.push(...additional.gates);
|
|
973
|
+
gateConfig.flows.push(...additional.flows);
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const gate of gateConfig.gates) {
|
|
980
|
+
symbols.push(createGateSymbol(gate, sourcePath));
|
|
981
|
+
for (const prize of gate.prizes) {
|
|
982
|
+
symbols.push(createSymbolEntry({
|
|
983
|
+
id: `gate-signal-${gate.id}-${prize.id}`,
|
|
984
|
+
symbol: `!${prize.id}`,
|
|
985
|
+
type: "signal",
|
|
986
|
+
source: "portal",
|
|
987
|
+
filePath: sourcePath,
|
|
988
|
+
data: prize,
|
|
989
|
+
description: `Signal from gate ${gate.id}`
|
|
990
|
+
}));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (const flow of gateConfig.flows) {
|
|
994
|
+
symbols.push(createFlowSymbol(flow, sourcePath));
|
|
995
|
+
}
|
|
996
|
+
} catch (e) {
|
|
997
|
+
errors.push({
|
|
998
|
+
source: "portal",
|
|
999
|
+
filePath: sourcePath,
|
|
1000
|
+
message: e.message
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const node of premiseFile.nodes) {
|
|
1006
|
+
const hasIdeaTag = node.tags?.includes("idea");
|
|
1007
|
+
if (!node.content && !hasIdeaTag) {
|
|
1008
|
+
const existing = symbols.find((s) => s.symbol === node.symbol);
|
|
1009
|
+
if (existing) {
|
|
1010
|
+
existing.position = node.position;
|
|
1011
|
+
existing.tags = node.tags;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
symbols.push(createSymbolEntry({
|
|
1016
|
+
id: node.id,
|
|
1017
|
+
symbol: node.symbol,
|
|
1018
|
+
type: node.type,
|
|
1019
|
+
source: "premise",
|
|
1020
|
+
filePath: ".premise",
|
|
1021
|
+
data: node,
|
|
1022
|
+
description: node.content,
|
|
1023
|
+
position: node.position,
|
|
1024
|
+
tags: node.tags,
|
|
1025
|
+
created: node.created,
|
|
1026
|
+
modified: node.modified
|
|
1027
|
+
}));
|
|
1028
|
+
}
|
|
1029
|
+
resolveReferences(symbols);
|
|
1030
|
+
return {
|
|
1031
|
+
symbols,
|
|
1032
|
+
purposeFiles,
|
|
1033
|
+
portalFiles,
|
|
1034
|
+
errors,
|
|
1035
|
+
timestamp: Date.now()
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function createSymbolEntry(partial) {
|
|
1039
|
+
return {
|
|
1040
|
+
...partial,
|
|
1041
|
+
data: partial.data ?? null,
|
|
1042
|
+
references: partial.references ?? [],
|
|
1043
|
+
referencedBy: partial.referencedBy ?? []
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function createGateSymbol(gate, filePath) {
|
|
1047
|
+
return createSymbolEntry({
|
|
1048
|
+
id: `gate-${gate.id}`,
|
|
1049
|
+
symbol: `^${gate.id}`,
|
|
1050
|
+
type: "gate",
|
|
1051
|
+
source: "portal",
|
|
1052
|
+
filePath,
|
|
1053
|
+
data: gate,
|
|
1054
|
+
description: gate.description,
|
|
1055
|
+
position: gate.position
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function createFlowSymbol(flow, filePath) {
|
|
1059
|
+
return createSymbolEntry({
|
|
1060
|
+
id: `gate-flow-${flow.id}`,
|
|
1061
|
+
symbol: `$${flow.id}`,
|
|
1062
|
+
type: "flow",
|
|
1063
|
+
source: "portal",
|
|
1064
|
+
filePath,
|
|
1065
|
+
data: flow,
|
|
1066
|
+
description: flow.description
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function parseAnchorString(raw) {
|
|
1070
|
+
const colonIndex = raw.lastIndexOf(":");
|
|
1071
|
+
if (colonIndex === -1 || colonIndex === raw.length - 1) {
|
|
1072
|
+
return { path: raw, lines: 0, raw };
|
|
1073
|
+
}
|
|
1074
|
+
const afterColon = raw.slice(colonIndex + 1);
|
|
1075
|
+
const filePath = raw.slice(0, colonIndex);
|
|
1076
|
+
if (!/^[\d,\- ]+$/.test(afterColon)) {
|
|
1077
|
+
return { path: raw, lines: 0, raw };
|
|
1078
|
+
}
|
|
1079
|
+
if (afterColon.includes("-")) {
|
|
1080
|
+
const [start, end] = afterColon.split("-").map(Number);
|
|
1081
|
+
return { path: filePath, lines: [start, end], raw };
|
|
1082
|
+
} else if (afterColon.includes(",")) {
|
|
1083
|
+
const lines = afterColon.split(",").map(Number);
|
|
1084
|
+
return { path: filePath, lines, raw };
|
|
1085
|
+
} else {
|
|
1086
|
+
return { path: filePath, lines: Number(afterColon), raw };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
var SYMBOL_BLOCKLIST2 = /* @__PURE__ */ new Set([
|
|
1090
|
+
"$lib",
|
|
1091
|
+
"$env",
|
|
1092
|
+
"$app",
|
|
1093
|
+
"$service-worker",
|
|
1094
|
+
"$virtual",
|
|
1095
|
+
"$schema",
|
|
1096
|
+
"$ref",
|
|
1097
|
+
"$id",
|
|
1098
|
+
"$type"
|
|
1099
|
+
]);
|
|
1100
|
+
function resolveReferences(symbols) {
|
|
1101
|
+
const symbolMap = new Map(symbols.map((s) => [s.symbol, s]));
|
|
1102
|
+
for (const symbol of symbols) {
|
|
1103
|
+
const dataStr = JSON.stringify(symbol.data);
|
|
1104
|
+
const refPattern = /(?:\?[@#$%~^!]|[@#$%~^!?])[a-zA-Z][\w-]*/g;
|
|
1105
|
+
const matches = (dataStr.match(refPattern) || []).filter((m) => !SYMBOL_BLOCKLIST2.has(m));
|
|
1106
|
+
for (const match of matches) {
|
|
1107
|
+
if (match !== symbol.symbol && symbolMap.has(match)) {
|
|
1108
|
+
if (!symbol.references.includes(match)) {
|
|
1109
|
+
symbol.references.push(match);
|
|
1110
|
+
}
|
|
1111
|
+
const target = symbolMap.get(match);
|
|
1112
|
+
if (target && !target.referencedBy.includes(symbol.symbol)) {
|
|
1113
|
+
target.referencedBy.push(symbol.symbol);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
async function aggregateFromDirectory(rootDir) {
|
|
1120
|
+
const premiseFile = {
|
|
1121
|
+
version: "1.0.0",
|
|
1122
|
+
metadata: {
|
|
1123
|
+
name: path3.basename(rootDir),
|
|
1124
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1125
|
+
modified: (/* @__PURE__ */ new Date()).toISOString()
|
|
1126
|
+
},
|
|
1127
|
+
sources: {
|
|
1128
|
+
purpose: [{ path: "./" }],
|
|
1129
|
+
portal: [{ path: "./" }]
|
|
1130
|
+
},
|
|
1131
|
+
nodes: [],
|
|
1132
|
+
connections: [],
|
|
1133
|
+
layout: {
|
|
1134
|
+
viewport: { x: 0, y: 0, zoom: 1 }
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
return aggregateFromPremise(premiseFile, rootDir);
|
|
1138
|
+
}
|
|
1139
|
+
function createSymbolIndex() {
|
|
1140
|
+
return {
|
|
1141
|
+
entries: /* @__PURE__ */ new Map(),
|
|
1142
|
+
byType: /* @__PURE__ */ new Map(),
|
|
1143
|
+
bySource: /* @__PURE__ */ new Map(),
|
|
1144
|
+
timestamp: 0
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function buildSymbolIndex(result) {
|
|
1148
|
+
const index = createSymbolIndex();
|
|
1149
|
+
index.timestamp = result.timestamp;
|
|
1150
|
+
for (const symbol of result.symbols) {
|
|
1151
|
+
index.entries.set(symbol.id, symbol);
|
|
1152
|
+
if (!index.byType.has(symbol.type)) {
|
|
1153
|
+
index.byType.set(symbol.type, []);
|
|
1154
|
+
}
|
|
1155
|
+
index.byType.get(symbol.type).push(symbol);
|
|
1156
|
+
if (!index.bySource.has(symbol.source)) {
|
|
1157
|
+
index.bySource.set(symbol.source, []);
|
|
1158
|
+
}
|
|
1159
|
+
index.bySource.get(symbol.source).push(symbol);
|
|
1160
|
+
}
|
|
1161
|
+
return index;
|
|
1162
|
+
}
|
|
1163
|
+
function getSymbol(index, symbol) {
|
|
1164
|
+
for (const entry of index.entries.values()) {
|
|
1165
|
+
if (entry.symbol === symbol) {
|
|
1166
|
+
return entry;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return void 0;
|
|
1170
|
+
}
|
|
1171
|
+
function getSymbolsByType(index, type) {
|
|
1172
|
+
return index.byType.get(type) || [];
|
|
1173
|
+
}
|
|
1174
|
+
function searchSymbols(index, query) {
|
|
1175
|
+
const lowerQuery = query.toLowerCase();
|
|
1176
|
+
const results = [];
|
|
1177
|
+
for (const entry of index.entries.values()) {
|
|
1178
|
+
if (entry.symbol.toLowerCase().includes(lowerQuery)) {
|
|
1179
|
+
results.push(entry);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (entry.description?.toLowerCase().includes(lowerQuery)) {
|
|
1183
|
+
results.push(entry);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (entry.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery))) {
|
|
1187
|
+
results.push(entry);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return results;
|
|
1191
|
+
}
|
|
1192
|
+
function getReferencesTo(index, symbol) {
|
|
1193
|
+
const entry = getSymbol(index, symbol);
|
|
1194
|
+
if (!entry) return [];
|
|
1195
|
+
return entry.referencedBy.map((ref) => getSymbol(index, ref)).filter((e) => e !== void 0);
|
|
1196
|
+
}
|
|
1197
|
+
function getReferencesFrom(index, symbol) {
|
|
1198
|
+
const entry = getSymbol(index, symbol);
|
|
1199
|
+
if (!entry) return [];
|
|
1200
|
+
return entry.references.map((ref) => getSymbol(index, ref)).filter((e) => e !== void 0);
|
|
1201
|
+
}
|
|
1202
|
+
function getSymbolCounts(index) {
|
|
1203
|
+
const counts = {
|
|
1204
|
+
component: 0,
|
|
1205
|
+
flow: 0,
|
|
1206
|
+
gate: 0,
|
|
1207
|
+
signal: 0,
|
|
1208
|
+
aspect: 0
|
|
1209
|
+
};
|
|
1210
|
+
for (const [type, symbols] of index.byType) {
|
|
1211
|
+
if (type in counts) {
|
|
1212
|
+
counts[type] = symbols.length;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return counts;
|
|
1216
|
+
}
|
|
1217
|
+
function getAllSymbols(index) {
|
|
1218
|
+
return Array.from(index.entries.values());
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// ../paradigm-mcp/node_modules/.pnpm/@a-company+probe-core@3.5.0/node_modules/@a-company/probe-core/dist/generator.js
|
|
1222
|
+
var PARADIGM_VERSION = "0.1.0";
|
|
1223
|
+
var SCHEMA_VERSION = "1.0.0";
|
|
1224
|
+
function generateScanIndex(input, options) {
|
|
1225
|
+
const index = {
|
|
1226
|
+
$meta: createMeta(options.projectName, input),
|
|
1227
|
+
components: {},
|
|
1228
|
+
features: {},
|
|
1229
|
+
flows: {},
|
|
1230
|
+
state: {},
|
|
1231
|
+
gates: {},
|
|
1232
|
+
signals: {},
|
|
1233
|
+
aspects: {},
|
|
1234
|
+
screens: {},
|
|
1235
|
+
symbolMap: {}
|
|
1236
|
+
};
|
|
1237
|
+
for (const symbol of input.symbols) {
|
|
1238
|
+
processSymbol(symbol, index, options);
|
|
1239
|
+
}
|
|
1240
|
+
if (options.screenDefinitions) {
|
|
1241
|
+
for (const [screenId, def] of Object.entries(options.screenDefinitions)) {
|
|
1242
|
+
if (!index.screens[screenId]) {
|
|
1243
|
+
index.screens[screenId] = {
|
|
1244
|
+
id: screenId,
|
|
1245
|
+
name: formatName(screenId),
|
|
1246
|
+
route: def.route,
|
|
1247
|
+
path: "",
|
|
1248
|
+
// Will be resolved if found
|
|
1249
|
+
components: def.components,
|
|
1250
|
+
features: def.features
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
buildScreenReferences(index);
|
|
1256
|
+
return index;
|
|
1257
|
+
}
|
|
1258
|
+
function createMeta(projectName, input) {
|
|
1259
|
+
return {
|
|
1260
|
+
version: SCHEMA_VERSION,
|
|
1261
|
+
project: projectName,
|
|
1262
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1263
|
+
paradigmVersion: PARADIGM_VERSION,
|
|
1264
|
+
sources: {
|
|
1265
|
+
purposeFiles: input.purposeFiles.length,
|
|
1266
|
+
portalFiles: input.portalFiles.length,
|
|
1267
|
+
premiseFiles: input.symbols.filter((s) => s.source === "premise").length > 0 ? 1 : 0
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function processSymbol(symbol, index, options) {
|
|
1272
|
+
const { type } = symbol;
|
|
1273
|
+
switch (type) {
|
|
1274
|
+
case "component":
|
|
1275
|
+
addComponent(symbol, index, options);
|
|
1276
|
+
break;
|
|
1277
|
+
case "feature":
|
|
1278
|
+
addFeature(symbol, index, options);
|
|
1279
|
+
break;
|
|
1280
|
+
case "flow":
|
|
1281
|
+
addFlow(symbol, index);
|
|
1282
|
+
break;
|
|
1283
|
+
case "state":
|
|
1284
|
+
addState(symbol, index);
|
|
1285
|
+
break;
|
|
1286
|
+
case "gate":
|
|
1287
|
+
addGate(symbol, index);
|
|
1288
|
+
break;
|
|
1289
|
+
case "signal":
|
|
1290
|
+
addSignal(symbol, index);
|
|
1291
|
+
break;
|
|
1292
|
+
case "aspect":
|
|
1293
|
+
addAspect(symbol, index);
|
|
1294
|
+
break;
|
|
1295
|
+
default:
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function addComponent(symbol, index, options) {
|
|
1300
|
+
const id = extractId(symbol.symbol);
|
|
1301
|
+
const visualTags = inferVisualTags(id, symbol.data, options.visualTagMappings);
|
|
1302
|
+
const element = {
|
|
1303
|
+
id,
|
|
1304
|
+
name: formatName(id),
|
|
1305
|
+
symbol: symbol.symbol,
|
|
1306
|
+
category: "components",
|
|
1307
|
+
path: symbol.filePath,
|
|
1308
|
+
description: symbol.description,
|
|
1309
|
+
visualTags,
|
|
1310
|
+
related: symbol.references
|
|
1311
|
+
};
|
|
1312
|
+
index.components[id] = element;
|
|
1313
|
+
index.symbolMap[symbol.symbol] = { category: "components", id };
|
|
1314
|
+
}
|
|
1315
|
+
function addFeature(symbol, index, options) {
|
|
1316
|
+
const id = extractId(symbol.symbol);
|
|
1317
|
+
const visualTags = inferVisualTags(id, symbol.data, options.visualTagMappings);
|
|
1318
|
+
const element = {
|
|
1319
|
+
id,
|
|
1320
|
+
name: formatName(id),
|
|
1321
|
+
symbol: symbol.symbol,
|
|
1322
|
+
category: "features",
|
|
1323
|
+
path: symbol.filePath,
|
|
1324
|
+
description: symbol.description,
|
|
1325
|
+
visualTags,
|
|
1326
|
+
related: symbol.references
|
|
1327
|
+
};
|
|
1328
|
+
index.features[id] = element;
|
|
1329
|
+
index.symbolMap[symbol.symbol] = { category: "features", id };
|
|
1330
|
+
}
|
|
1331
|
+
function addFlow(symbol, index) {
|
|
1332
|
+
const id = extractId(symbol.symbol);
|
|
1333
|
+
const data = symbol.data;
|
|
1334
|
+
const steps = [];
|
|
1335
|
+
if (data?.steps) {
|
|
1336
|
+
for (let i = 0; i < data.steps.length; i++) {
|
|
1337
|
+
const step = data.steps[i];
|
|
1338
|
+
steps.push({
|
|
1339
|
+
id: `${id}-step-${i}`,
|
|
1340
|
+
name: step.action || `Step ${i + 1}`,
|
|
1341
|
+
target: step.component,
|
|
1342
|
+
description: step.description,
|
|
1343
|
+
order: i
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const flow = {
|
|
1348
|
+
id,
|
|
1349
|
+
name: formatName(id),
|
|
1350
|
+
symbol: symbol.symbol,
|
|
1351
|
+
path: symbol.filePath,
|
|
1352
|
+
description: symbol.description,
|
|
1353
|
+
steps
|
|
1354
|
+
};
|
|
1355
|
+
index.flows[id] = flow;
|
|
1356
|
+
index.symbolMap[symbol.symbol] = { category: "flows", id };
|
|
1357
|
+
}
|
|
1358
|
+
function addState(symbol, index) {
|
|
1359
|
+
const id = extractId(symbol.symbol);
|
|
1360
|
+
const state = {
|
|
1361
|
+
id,
|
|
1362
|
+
name: formatName(id),
|
|
1363
|
+
symbol: symbol.symbol,
|
|
1364
|
+
path: symbol.filePath,
|
|
1365
|
+
description: symbol.description,
|
|
1366
|
+
consumers: symbol.referencedBy
|
|
1367
|
+
};
|
|
1368
|
+
index.state[id] = state;
|
|
1369
|
+
index.symbolMap[symbol.symbol] = { category: "state", id };
|
|
1370
|
+
}
|
|
1371
|
+
function addGate(symbol, index) {
|
|
1372
|
+
const id = extractId(symbol.symbol);
|
|
1373
|
+
const element = {
|
|
1374
|
+
id,
|
|
1375
|
+
name: formatName(id),
|
|
1376
|
+
symbol: symbol.symbol,
|
|
1377
|
+
category: "gates",
|
|
1378
|
+
path: symbol.filePath,
|
|
1379
|
+
description: symbol.description,
|
|
1380
|
+
related: symbol.references
|
|
1381
|
+
};
|
|
1382
|
+
index.gates[id] = element;
|
|
1383
|
+
index.symbolMap[symbol.symbol] = { category: "gates", id };
|
|
1384
|
+
}
|
|
1385
|
+
function addSignal(symbol, index) {
|
|
1386
|
+
const id = extractId(symbol.symbol);
|
|
1387
|
+
const element = {
|
|
1388
|
+
id,
|
|
1389
|
+
name: formatName(id),
|
|
1390
|
+
symbol: symbol.symbol,
|
|
1391
|
+
category: "signals",
|
|
1392
|
+
path: symbol.filePath,
|
|
1393
|
+
description: symbol.description,
|
|
1394
|
+
related: symbol.references
|
|
1395
|
+
};
|
|
1396
|
+
index.signals[id] = element;
|
|
1397
|
+
index.symbolMap[symbol.symbol] = { category: "signals", id };
|
|
1398
|
+
}
|
|
1399
|
+
function addAspect(symbol, index) {
|
|
1400
|
+
const id = extractId(symbol.symbol);
|
|
1401
|
+
const element = {
|
|
1402
|
+
id,
|
|
1403
|
+
name: formatName(id),
|
|
1404
|
+
symbol: symbol.symbol,
|
|
1405
|
+
category: "aspects",
|
|
1406
|
+
path: symbol.filePath,
|
|
1407
|
+
description: symbol.description,
|
|
1408
|
+
related: symbol.references
|
|
1409
|
+
};
|
|
1410
|
+
index.aspects[id] = element;
|
|
1411
|
+
index.symbolMap[symbol.symbol] = { category: "aspects", id };
|
|
1412
|
+
}
|
|
1413
|
+
function buildScreenReferences(index) {
|
|
1414
|
+
for (const screen of Object.values(index.screens)) {
|
|
1415
|
+
if (screen.components) {
|
|
1416
|
+
for (const compId of screen.components) {
|
|
1417
|
+
const comp = index.components[compId];
|
|
1418
|
+
if (comp) {
|
|
1419
|
+
comp.screens = comp.screens || [];
|
|
1420
|
+
if (!comp.screens.includes(screen.id)) {
|
|
1421
|
+
comp.screens.push(screen.id);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function extractId(symbol) {
|
|
1429
|
+
return symbol.slice(1);
|
|
1430
|
+
}
|
|
1431
|
+
function formatName(id) {
|
|
1432
|
+
return id.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1433
|
+
}
|
|
1434
|
+
function inferVisualTags(id, _data, customMappings) {
|
|
1435
|
+
const tags = [];
|
|
1436
|
+
if (customMappings?.[id]) {
|
|
1437
|
+
return customMappings[id];
|
|
1438
|
+
}
|
|
1439
|
+
const patterns = [
|
|
1440
|
+
[/button/i, "button"],
|
|
1441
|
+
[/btn/i, "button"],
|
|
1442
|
+
[/form/i, "form"],
|
|
1443
|
+
[/input/i, "input"],
|
|
1444
|
+
[/field/i, "input"],
|
|
1445
|
+
[/select/i, "input"],
|
|
1446
|
+
[/card/i, "card"],
|
|
1447
|
+
[/list/i, "list"],
|
|
1448
|
+
[/table/i, "list"],
|
|
1449
|
+
[/modal/i, "modal"],
|
|
1450
|
+
[/dialog/i, "modal"],
|
|
1451
|
+
[/drawer/i, "modal"],
|
|
1452
|
+
[/nav/i, "nav"],
|
|
1453
|
+
[/menu/i, "menu"],
|
|
1454
|
+
[/dropdown/i, "menu"],
|
|
1455
|
+
[/header/i, "header"],
|
|
1456
|
+
[/footer/i, "footer"],
|
|
1457
|
+
[/sidebar/i, "sidebar"],
|
|
1458
|
+
[/hero/i, "hero"],
|
|
1459
|
+
[/grid/i, "grid"],
|
|
1460
|
+
[/chart/i, "chart"],
|
|
1461
|
+
[/graph/i, "chart"],
|
|
1462
|
+
[/icon/i, "icon"],
|
|
1463
|
+
[/image/i, "image"],
|
|
1464
|
+
[/avatar/i, "avatar"],
|
|
1465
|
+
[/badge/i, "badge"],
|
|
1466
|
+
[/tag/i, "badge"],
|
|
1467
|
+
[/tab/i, "tab"],
|
|
1468
|
+
[/accordion/i, "accordion"],
|
|
1469
|
+
[/toast/i, "toast"],
|
|
1470
|
+
[/notification/i, "toast"],
|
|
1471
|
+
[/alert/i, "toast"],
|
|
1472
|
+
[/spinner/i, "spinner"],
|
|
1473
|
+
[/loader/i, "spinner"],
|
|
1474
|
+
[/loading/i, "spinner"],
|
|
1475
|
+
[/skeleton/i, "skeleton"]
|
|
1476
|
+
];
|
|
1477
|
+
for (const [pattern, tag] of patterns) {
|
|
1478
|
+
if (pattern.test(id)) {
|
|
1479
|
+
tags.push(tag);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return tags;
|
|
1483
|
+
}
|
|
1484
|
+
function serializeScanIndex(index) {
|
|
1485
|
+
return JSON.stringify(index, null, 2);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// ../paradigm-mcp/src/utils/session-tracker.ts
|
|
1489
|
+
import * as fs4 from "fs";
|
|
1490
|
+
import * as path5 from "path";
|
|
1491
|
+
|
|
1492
|
+
// ../paradigm-mcp/src/utils/global-store.ts
|
|
1493
|
+
import * as fs3 from "fs";
|
|
1494
|
+
import * as path4 from "path";
|
|
1495
|
+
import * as os from "os";
|
|
1496
|
+
import * as crypto from "crypto";
|
|
1497
|
+
import * as yaml4 from "js-yaml";
|
|
1498
|
+
function getGlobalDir() {
|
|
1499
|
+
const dir = path4.join(os.homedir(), ".paradigm");
|
|
1500
|
+
if (!fs3.existsSync(dir)) {
|
|
1501
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1502
|
+
}
|
|
1503
|
+
return dir;
|
|
1504
|
+
}
|
|
1505
|
+
function getProjectHash(rootDir) {
|
|
1506
|
+
const absolute = path4.resolve(rootDir);
|
|
1507
|
+
return crypto.createHash("sha256").update(absolute).digest("hex").slice(0, 12);
|
|
1508
|
+
}
|
|
1509
|
+
function getSessionDir(rootDir) {
|
|
1510
|
+
const hash = getProjectHash(rootDir);
|
|
1511
|
+
const dir = path4.join(getGlobalDir(), "sessions", hash);
|
|
1512
|
+
if (!fs3.existsSync(dir)) {
|
|
1513
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1514
|
+
}
|
|
1515
|
+
const handoffsDir = path4.join(dir, "pending-handoffs");
|
|
1516
|
+
if (!fs3.existsSync(handoffsDir)) {
|
|
1517
|
+
fs3.mkdirSync(handoffsDir, { recursive: true });
|
|
1518
|
+
}
|
|
1519
|
+
return dir;
|
|
1520
|
+
}
|
|
1521
|
+
function writeProjectMeta(rootDir) {
|
|
1522
|
+
const sessionDir = getSessionDir(rootDir);
|
|
1523
|
+
const metaPath = path4.join(sessionDir, "_project-meta.json");
|
|
1524
|
+
const projectName = path4.basename(path4.resolve(rootDir));
|
|
1525
|
+
const meta = {
|
|
1526
|
+
name: projectName,
|
|
1527
|
+
path: path4.resolve(rootDir),
|
|
1528
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1529
|
+
};
|
|
1530
|
+
fs3.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
1531
|
+
}
|
|
1532
|
+
function writePendingHandoff(rootDir, handoff) {
|
|
1533
|
+
const sessionDir = getSessionDir(rootDir);
|
|
1534
|
+
const filePath = path4.join(sessionDir, "pending-handoffs", `${handoff.id}.json`);
|
|
1535
|
+
fs3.writeFileSync(filePath, JSON.stringify(handoff, null, 2));
|
|
1536
|
+
}
|
|
1537
|
+
function loadPendingHandoffs(rootDir) {
|
|
1538
|
+
const sessionDir = getSessionDir(rootDir);
|
|
1539
|
+
const handoffsDir = path4.join(sessionDir, "pending-handoffs");
|
|
1540
|
+
if (!fs3.existsSync(handoffsDir)) {
|
|
1541
|
+
return [];
|
|
1542
|
+
}
|
|
1543
|
+
const handoffs = [];
|
|
1544
|
+
try {
|
|
1545
|
+
const files = fs3.readdirSync(handoffsDir);
|
|
1546
|
+
for (const file of files) {
|
|
1547
|
+
if (!file.endsWith(".json")) continue;
|
|
1548
|
+
try {
|
|
1549
|
+
const content = fs3.readFileSync(path4.join(handoffsDir, file), "utf8");
|
|
1550
|
+
const handoff = JSON.parse(content);
|
|
1551
|
+
if (handoff.status === "pending") {
|
|
1552
|
+
handoffs.push(handoff);
|
|
1553
|
+
}
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
handoffs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
1560
|
+
return handoffs;
|
|
1561
|
+
}
|
|
1562
|
+
function markHandoffDelivered(rootDir, handoffId) {
|
|
1563
|
+
const sessionDir = getSessionDir(rootDir);
|
|
1564
|
+
const filePath = path4.join(sessionDir, "pending-handoffs", `${handoffId}.json`);
|
|
1565
|
+
if (!fs3.existsSync(filePath)) return;
|
|
1566
|
+
try {
|
|
1567
|
+
const content = fs3.readFileSync(filePath, "utf8");
|
|
1568
|
+
const handoff = JSON.parse(content);
|
|
1569
|
+
handoff.status = "delivered";
|
|
1570
|
+
fs3.writeFileSync(filePath, JSON.stringify(handoff, null, 2));
|
|
1571
|
+
} catch {
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function getGlobalWisdomDir() {
|
|
1575
|
+
const dir = path4.join(getGlobalDir(), "wisdom");
|
|
1576
|
+
if (!fs3.existsSync(dir)) {
|
|
1577
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1578
|
+
}
|
|
1579
|
+
return dir;
|
|
1580
|
+
}
|
|
1581
|
+
function loadGlobalAntipatterns() {
|
|
1582
|
+
const filePath = path4.join(getGlobalWisdomDir(), "antipatterns.yaml");
|
|
1583
|
+
if (!fs3.existsSync(filePath)) return [];
|
|
1584
|
+
try {
|
|
1585
|
+
const content = fs3.readFileSync(filePath, "utf8");
|
|
1586
|
+
const data = yaml4.load(content);
|
|
1587
|
+
return data?.antipatterns || [];
|
|
1588
|
+
} catch {
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function loadGlobalDecisions() {
|
|
1593
|
+
const decisionsDir = path4.join(getGlobalWisdomDir(), "decisions");
|
|
1594
|
+
if (!fs3.existsSync(decisionsDir)) return [];
|
|
1595
|
+
const decisions = [];
|
|
1596
|
+
try {
|
|
1597
|
+
const files = fs3.readdirSync(decisionsDir);
|
|
1598
|
+
for (const file of files) {
|
|
1599
|
+
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
|
|
1600
|
+
try {
|
|
1601
|
+
const content = fs3.readFileSync(path4.join(decisionsDir, file), "utf8");
|
|
1602
|
+
const decision = yaml4.load(content);
|
|
1603
|
+
decisions.push(decision);
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
decisions.sort((a, b) => a.id.localeCompare(b.id));
|
|
1610
|
+
return decisions;
|
|
1611
|
+
}
|
|
1612
|
+
function loadGlobalPreferences() {
|
|
1613
|
+
const filePath = path4.join(getGlobalWisdomDir(), "preferences.yaml");
|
|
1614
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
1615
|
+
try {
|
|
1616
|
+
const content = fs3.readFileSync(filePath, "utf8");
|
|
1617
|
+
return yaml4.load(content);
|
|
1618
|
+
} catch {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function recordGlobalAntipattern(antipattern) {
|
|
1623
|
+
const filePath = path4.join(getGlobalWisdomDir(), "antipatterns.yaml");
|
|
1624
|
+
let data = { version: "1.0", antipatterns: [] };
|
|
1625
|
+
if (fs3.existsSync(filePath)) {
|
|
1626
|
+
try {
|
|
1627
|
+
const content = fs3.readFileSync(filePath, "utf8");
|
|
1628
|
+
data = yaml4.load(content);
|
|
1629
|
+
if (!data.antipatterns) data.antipatterns = [];
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
data.antipatterns.push({
|
|
1634
|
+
...antipattern,
|
|
1635
|
+
added: (/* @__PURE__ */ new Date()).toISOString()
|
|
1636
|
+
});
|
|
1637
|
+
fs3.writeFileSync(filePath, yaml4.dump(data, { lineWidth: -1 }));
|
|
1638
|
+
}
|
|
1639
|
+
function recordGlobalDecision(decision) {
|
|
1640
|
+
const decisionsDir = path4.join(getGlobalWisdomDir(), "decisions");
|
|
1641
|
+
if (!fs3.existsSync(decisionsDir)) {
|
|
1642
|
+
fs3.mkdirSync(decisionsDir, { recursive: true });
|
|
1643
|
+
}
|
|
1644
|
+
const slug = decision.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1645
|
+
const fileName = `${decision.id}-${slug}.yaml`;
|
|
1646
|
+
const filePath = path4.join(decisionsDir, fileName);
|
|
1647
|
+
fs3.writeFileSync(filePath, yaml4.dump(decision, { lineWidth: -1 }));
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ../paradigm-mcp/src/utils/session-tracker.ts
|
|
1651
|
+
var MODEL_PRICING = {
|
|
1652
|
+
"claude-opus-4": { input: 15, output: 75, name: "Claude Opus 4" },
|
|
1653
|
+
"claude-sonnet-4": { input: 3, output: 15, name: "Claude Sonnet 4" },
|
|
1654
|
+
"claude-haiku-3.5": { input: 0.8, output: 4, name: "Claude Haiku 3.5" }
|
|
1655
|
+
};
|
|
1656
|
+
var MAX_BREADCRUMBS = 50;
|
|
1657
|
+
var BREADCRUMBS_FILE = ".paradigm/session-breadcrumbs.json";
|
|
1658
|
+
var CHECKPOINT_FILE = ".paradigm/session-checkpoint.json";
|
|
1659
|
+
var CHECKPOINT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1660
|
+
var SessionTracker = class {
|
|
1661
|
+
session;
|
|
1662
|
+
rootDir = null;
|
|
1663
|
+
_recovered = false;
|
|
1664
|
+
lastLoreEntryId = null;
|
|
1665
|
+
constructor() {
|
|
1666
|
+
this.session = this.createNewSession();
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Set the project root directory (for persisting breadcrumbs)
|
|
1670
|
+
*/
|
|
1671
|
+
setRootDir(rootDir) {
|
|
1672
|
+
this.rootDir = rootDir;
|
|
1673
|
+
}
|
|
1674
|
+
createNewSession() {
|
|
1675
|
+
return {
|
|
1676
|
+
sessionId: `s${Date.now().toString(36)}`,
|
|
1677
|
+
startTime: Date.now(),
|
|
1678
|
+
lastActivity: Date.now(),
|
|
1679
|
+
model: "claude-sonnet-4",
|
|
1680
|
+
resourceReads: [],
|
|
1681
|
+
toolCalls: [],
|
|
1682
|
+
breadcrumbs: [],
|
|
1683
|
+
totals: {
|
|
1684
|
+
resourceReadCount: 0,
|
|
1685
|
+
toolCallCount: 0,
|
|
1686
|
+
totalBytes: 0,
|
|
1687
|
+
totalTokens: 0,
|
|
1688
|
+
estimatedCostUsd: 0
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Add a breadcrumb (summarized action for session recovery)
|
|
1694
|
+
*/
|
|
1695
|
+
addBreadcrumb(action, summary, options = {}) {
|
|
1696
|
+
this.session.breadcrumbs.push({
|
|
1697
|
+
timestamp: Date.now(),
|
|
1698
|
+
action,
|
|
1699
|
+
tool: options.tool,
|
|
1700
|
+
symbol: options.symbol,
|
|
1701
|
+
summary
|
|
1702
|
+
});
|
|
1703
|
+
if (this.session.breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
1704
|
+
this.session.breadcrumbs = this.session.breadcrumbs.slice(-MAX_BREADCRUMBS);
|
|
1705
|
+
}
|
|
1706
|
+
this.persistBreadcrumbs();
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Get recent breadcrumbs
|
|
1710
|
+
*/
|
|
1711
|
+
getBreadcrumbs(limit = 20) {
|
|
1712
|
+
return this.session.breadcrumbs.slice(-limit);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Persist breadcrumbs to file (dual-write: local + global)
|
|
1716
|
+
*/
|
|
1717
|
+
persistBreadcrumbs() {
|
|
1718
|
+
if (!this.rootDir) return;
|
|
1719
|
+
const data = {
|
|
1720
|
+
sessionId: this.session.sessionId,
|
|
1721
|
+
startTime: this.session.startTime,
|
|
1722
|
+
lastActivity: this.session.lastActivity,
|
|
1723
|
+
breadcrumbs: this.session.breadcrumbs,
|
|
1724
|
+
symbolsModified: this.extractSymbolsFromBreadcrumbs(),
|
|
1725
|
+
filesExplored: this.extractFilesFromBreadcrumbs()
|
|
1726
|
+
};
|
|
1727
|
+
let jsonData;
|
|
1728
|
+
try {
|
|
1729
|
+
jsonData = JSON.stringify(data, null, 2);
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
console.error("[paradigm-mcp] persistBreadcrumbs: JSON.stringify failed:", err.message);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
const filePath = path5.join(this.rootDir, BREADCRUMBS_FILE);
|
|
1736
|
+
const dir = path5.dirname(filePath);
|
|
1737
|
+
if (!fs4.existsSync(dir)) {
|
|
1738
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1739
|
+
}
|
|
1740
|
+
fs4.writeFileSync(filePath, jsonData);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
console.error("[paradigm-mcp] persistBreadcrumbs: local write failed:", err.message);
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
const globalSessionDir = getSessionDir(this.rootDir);
|
|
1746
|
+
fs4.writeFileSync(path5.join(globalSessionDir, "breadcrumbs.json"), jsonData);
|
|
1747
|
+
writeProjectMeta(this.rootDir);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
console.error("[paradigm-mcp] persistBreadcrumbs: global write failed:", err.message);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Load previous session breadcrumbs from file.
|
|
1754
|
+
* Prefers global path (~/.paradigm/sessions/{hash}/breadcrumbs.json),
|
|
1755
|
+
* falls back to local (.paradigm/session-breadcrumbs.json).
|
|
1756
|
+
*/
|
|
1757
|
+
loadPreviousSession() {
|
|
1758
|
+
if (!this.rootDir) return null;
|
|
1759
|
+
try {
|
|
1760
|
+
const globalSessionDir = getSessionDir(this.rootDir);
|
|
1761
|
+
const globalPath = path5.join(globalSessionDir, "breadcrumbs.json");
|
|
1762
|
+
if (fs4.existsSync(globalPath)) {
|
|
1763
|
+
const content = fs4.readFileSync(globalPath, "utf8");
|
|
1764
|
+
return JSON.parse(content);
|
|
1765
|
+
}
|
|
1766
|
+
} catch {
|
|
1767
|
+
}
|
|
1768
|
+
try {
|
|
1769
|
+
const filePath = path5.join(this.rootDir, BREADCRUMBS_FILE);
|
|
1770
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
1771
|
+
const content = fs4.readFileSync(filePath, "utf8");
|
|
1772
|
+
return JSON.parse(content);
|
|
1773
|
+
} catch {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Save a cognitive-transition checkpoint for crash recovery.
|
|
1779
|
+
* Fills in timestamp, sessionId, and snapshots recent breadcrumbs.
|
|
1780
|
+
* Returns the checkpoint and whether it was persisted to disk.
|
|
1781
|
+
*/
|
|
1782
|
+
saveCheckpoint(data) {
|
|
1783
|
+
const checkpoint = {
|
|
1784
|
+
phase: data.phase,
|
|
1785
|
+
context: data.context,
|
|
1786
|
+
timestamp: Date.now(),
|
|
1787
|
+
sessionId: this.session.sessionId,
|
|
1788
|
+
plan: data.plan,
|
|
1789
|
+
modifiedFiles: data.modifiedFiles,
|
|
1790
|
+
symbolsTouched: data.symbolsTouched,
|
|
1791
|
+
decisions: data.decisions,
|
|
1792
|
+
recentBreadcrumbs: this.session.breadcrumbs.slice(-10)
|
|
1793
|
+
};
|
|
1794
|
+
const persisted = this.persistCheckpoint(checkpoint);
|
|
1795
|
+
return { checkpoint, persisted };
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Load the most recent checkpoint.
|
|
1799
|
+
* Prefers global path, falls back to local.
|
|
1800
|
+
* Returns null for checkpoints older than 7 days.
|
|
1801
|
+
*/
|
|
1802
|
+
loadCheckpoint() {
|
|
1803
|
+
if (!this.rootDir) return null;
|
|
1804
|
+
let checkpoint = null;
|
|
1805
|
+
try {
|
|
1806
|
+
const globalSessionDir = getSessionDir(this.rootDir);
|
|
1807
|
+
const globalPath = path5.join(globalSessionDir, "checkpoint.json");
|
|
1808
|
+
if (fs4.existsSync(globalPath)) {
|
|
1809
|
+
const content = fs4.readFileSync(globalPath, "utf8");
|
|
1810
|
+
checkpoint = JSON.parse(content);
|
|
1811
|
+
}
|
|
1812
|
+
} catch {
|
|
1813
|
+
}
|
|
1814
|
+
if (!checkpoint) {
|
|
1815
|
+
try {
|
|
1816
|
+
const localPath = path5.join(this.rootDir, CHECKPOINT_FILE);
|
|
1817
|
+
if (fs4.existsSync(localPath)) {
|
|
1818
|
+
const content = fs4.readFileSync(localPath, "utf8");
|
|
1819
|
+
checkpoint = JSON.parse(content);
|
|
1820
|
+
}
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (checkpoint && Date.now() - checkpoint.timestamp > CHECKPOINT_MAX_AGE_MS) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
return checkpoint;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Persist checkpoint to both local and global paths.
|
|
1831
|
+
* Returns which writes succeeded so callers can report accurately.
|
|
1832
|
+
*/
|
|
1833
|
+
persistCheckpoint(checkpoint) {
|
|
1834
|
+
const result = { local: false, global: false };
|
|
1835
|
+
if (!this.rootDir) {
|
|
1836
|
+
console.error("[paradigm-mcp] persistCheckpoint: rootDir not set, skipping write");
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
let jsonData;
|
|
1840
|
+
try {
|
|
1841
|
+
jsonData = JSON.stringify(checkpoint, null, 2);
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
console.error("[paradigm-mcp] persistCheckpoint: JSON.stringify failed:", err.message);
|
|
1844
|
+
return result;
|
|
1845
|
+
}
|
|
1846
|
+
try {
|
|
1847
|
+
const filePath = path5.join(this.rootDir, CHECKPOINT_FILE);
|
|
1848
|
+
const dir = path5.dirname(filePath);
|
|
1849
|
+
if (!fs4.existsSync(dir)) {
|
|
1850
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1851
|
+
}
|
|
1852
|
+
fs4.writeFileSync(filePath, jsonData);
|
|
1853
|
+
result.local = true;
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
console.error("[paradigm-mcp] persistCheckpoint: local write failed:", err.message);
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
const globalSessionDir = getSessionDir(this.rootDir);
|
|
1859
|
+
fs4.writeFileSync(path5.join(globalSessionDir, "checkpoint.json"), jsonData);
|
|
1860
|
+
writeProjectMeta(this.rootDir);
|
|
1861
|
+
result.global = true;
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
console.error("[paradigm-mcp] persistCheckpoint: global write failed:", err.message);
|
|
1864
|
+
}
|
|
1865
|
+
return result;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Set the last lore entry ID recorded in this session
|
|
1869
|
+
*/
|
|
1870
|
+
setLastLoreEntryId(id) {
|
|
1871
|
+
this.lastLoreEntryId = id;
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Get the last lore entry ID recorded in this session
|
|
1875
|
+
*/
|
|
1876
|
+
getLastLoreEntryId() {
|
|
1877
|
+
return this.lastLoreEntryId;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Check whether auto-recovery has already fired this session.
|
|
1881
|
+
*/
|
|
1882
|
+
hasRecoveredThisSession() {
|
|
1883
|
+
return this._recovered;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Mark that auto-recovery has fired (so it only fires once per session).
|
|
1887
|
+
*/
|
|
1888
|
+
markRecovered() {
|
|
1889
|
+
this._recovered = true;
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Extract symbols from breadcrumbs
|
|
1893
|
+
*/
|
|
1894
|
+
extractSymbolsFromBreadcrumbs() {
|
|
1895
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
1896
|
+
for (const bc of this.session.breadcrumbs) {
|
|
1897
|
+
if (bc.symbol) symbols.add(bc.symbol);
|
|
1898
|
+
}
|
|
1899
|
+
return Array.from(symbols);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Extract files from breadcrumbs
|
|
1903
|
+
*/
|
|
1904
|
+
extractFilesFromBreadcrumbs() {
|
|
1905
|
+
const files = /* @__PURE__ */ new Set();
|
|
1906
|
+
for (const bc of this.session.breadcrumbs) {
|
|
1907
|
+
const matches = bc.summary.match(/\b[\w./]+\.(ts|js|tsx|jsx|py|go|rs|yaml|json|md)\b/g);
|
|
1908
|
+
if (matches) {
|
|
1909
|
+
for (const m of matches) {
|
|
1910
|
+
files.add(m);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return Array.from(files);
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Estimate tokens from text (approx 3.5 chars per token)
|
|
1918
|
+
*/
|
|
1919
|
+
estimateTokens(text) {
|
|
1920
|
+
const len = typeof text === "number" ? text : text.length;
|
|
1921
|
+
return Math.ceil(len / 3.5);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Calculate cost for tokens using current model pricing
|
|
1925
|
+
*/
|
|
1926
|
+
calculateCost(tokens, isOutput = true) {
|
|
1927
|
+
const pricing = MODEL_PRICING[this.session.model];
|
|
1928
|
+
const rate = isOutput ? pricing.output : pricing.input;
|
|
1929
|
+
return tokens / 1e6 * rate;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Set the model for cost calculations
|
|
1933
|
+
*/
|
|
1934
|
+
setModel(model) {
|
|
1935
|
+
this.session.model = model;
|
|
1936
|
+
this.recalculateTotals();
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Get current model
|
|
1940
|
+
*/
|
|
1941
|
+
getModel() {
|
|
1942
|
+
return this.session.model;
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Track a resource read
|
|
1946
|
+
*/
|
|
1947
|
+
trackResourceRead(uri, bytes) {
|
|
1948
|
+
const resourceType = this.extractResourceType(uri);
|
|
1949
|
+
const tokens = this.estimateTokens(bytes);
|
|
1950
|
+
this.session.resourceReads.push({
|
|
1951
|
+
timestamp: Date.now(),
|
|
1952
|
+
resourceType,
|
|
1953
|
+
uri,
|
|
1954
|
+
bytes,
|
|
1955
|
+
tokens
|
|
1956
|
+
});
|
|
1957
|
+
this.session.lastActivity = Date.now();
|
|
1958
|
+
this.updateTotals(bytes, tokens);
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Track a tool call
|
|
1962
|
+
*/
|
|
1963
|
+
trackToolCall(toolName, responseBytes) {
|
|
1964
|
+
const tokens = this.estimateTokens(responseBytes);
|
|
1965
|
+
this.session.toolCalls.push({
|
|
1966
|
+
timestamp: Date.now(),
|
|
1967
|
+
toolName,
|
|
1968
|
+
responseBytes,
|
|
1969
|
+
responseTokens: tokens
|
|
1970
|
+
});
|
|
1971
|
+
this.session.lastActivity = Date.now();
|
|
1972
|
+
this.updateTotals(responseBytes, tokens);
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Update running totals
|
|
1976
|
+
*/
|
|
1977
|
+
updateTotals(bytes, tokens) {
|
|
1978
|
+
this.session.totals.resourceReadCount = this.session.resourceReads.length;
|
|
1979
|
+
this.session.totals.toolCallCount = this.session.toolCalls.length;
|
|
1980
|
+
this.session.totals.totalBytes += bytes;
|
|
1981
|
+
this.session.totals.totalTokens += tokens;
|
|
1982
|
+
this.session.totals.estimatedCostUsd = this.calculateCost(this.session.totals.totalTokens);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Recalculate totals (used when model changes)
|
|
1986
|
+
*/
|
|
1987
|
+
recalculateTotals() {
|
|
1988
|
+
this.session.totals.estimatedCostUsd = this.calculateCost(this.session.totals.totalTokens);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Extract resource type from URI
|
|
1992
|
+
*/
|
|
1993
|
+
extractResourceType(uri) {
|
|
1994
|
+
const path10 = uri.replace("paradigm://", "");
|
|
1995
|
+
const firstPart = path10.split("/")[0];
|
|
1996
|
+
return firstPart || "unknown";
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Get session statistics
|
|
2000
|
+
*/
|
|
2001
|
+
getStats() {
|
|
2002
|
+
return { ...this.session };
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Get detailed cost breakdown
|
|
2006
|
+
*/
|
|
2007
|
+
getCostBreakdown() {
|
|
2008
|
+
const resourcesByType = {};
|
|
2009
|
+
let resourceBytes = 0;
|
|
2010
|
+
let resourceTokens = 0;
|
|
2011
|
+
for (const read of this.session.resourceReads) {
|
|
2012
|
+
if (!resourcesByType[read.resourceType]) {
|
|
2013
|
+
resourcesByType[read.resourceType] = { count: 0, bytes: 0, tokens: 0 };
|
|
2014
|
+
}
|
|
2015
|
+
resourcesByType[read.resourceType].count++;
|
|
2016
|
+
resourcesByType[read.resourceType].bytes += read.bytes;
|
|
2017
|
+
resourcesByType[read.resourceType].tokens += read.tokens;
|
|
2018
|
+
resourceBytes += read.bytes;
|
|
2019
|
+
resourceTokens += read.tokens;
|
|
2020
|
+
}
|
|
2021
|
+
const toolsByName = {};
|
|
2022
|
+
let toolBytes = 0;
|
|
2023
|
+
let toolTokens = 0;
|
|
2024
|
+
for (const call of this.session.toolCalls) {
|
|
2025
|
+
if (!toolsByName[call.toolName]) {
|
|
2026
|
+
toolsByName[call.toolName] = { count: 0, bytes: 0, tokens: 0 };
|
|
2027
|
+
}
|
|
2028
|
+
toolsByName[call.toolName].count++;
|
|
2029
|
+
toolsByName[call.toolName].bytes += call.responseBytes;
|
|
2030
|
+
toolsByName[call.toolName].tokens += call.responseTokens;
|
|
2031
|
+
toolBytes += call.responseBytes;
|
|
2032
|
+
toolTokens += call.responseTokens;
|
|
2033
|
+
}
|
|
2034
|
+
const totalTokens = resourceTokens + toolTokens;
|
|
2035
|
+
const totalCost = this.calculateCost(totalTokens);
|
|
2036
|
+
return {
|
|
2037
|
+
model: MODEL_PRICING[this.session.model].name,
|
|
2038
|
+
modelId: this.session.model,
|
|
2039
|
+
pricing: MODEL_PRICING[this.session.model],
|
|
2040
|
+
resources: {
|
|
2041
|
+
count: this.session.resourceReads.length,
|
|
2042
|
+
bytes: resourceBytes,
|
|
2043
|
+
tokens: resourceTokens,
|
|
2044
|
+
costUsd: this.calculateCost(resourceTokens),
|
|
2045
|
+
byType: resourcesByType
|
|
2046
|
+
},
|
|
2047
|
+
tools: {
|
|
2048
|
+
count: this.session.toolCalls.length,
|
|
2049
|
+
bytes: toolBytes,
|
|
2050
|
+
tokens: toolTokens,
|
|
2051
|
+
costUsd: this.calculateCost(toolTokens),
|
|
2052
|
+
byName: toolsByName
|
|
2053
|
+
},
|
|
2054
|
+
total: {
|
|
2055
|
+
tokens: totalTokens,
|
|
2056
|
+
costUsd: totalCost
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Get handoff recommendation based on context usage
|
|
2062
|
+
*/
|
|
2063
|
+
getHandoffRecommendation(contextWindowSize = 2e5, estimatedTotalTokens) {
|
|
2064
|
+
const mcpTokens = this.session.totals.totalTokens;
|
|
2065
|
+
const estimatedConversationOverhead = mcpTokens * 4;
|
|
2066
|
+
const totalEstimate = estimatedTotalTokens || mcpTokens + estimatedConversationOverhead;
|
|
2067
|
+
const usagePercent = Math.round(totalEstimate / contextWindowSize * 100);
|
|
2068
|
+
let recommendation;
|
|
2069
|
+
let message;
|
|
2070
|
+
if (usagePercent >= 85) {
|
|
2071
|
+
recommendation = "handoff-urgent";
|
|
2072
|
+
message = "Context is nearly full. Initiate handoff immediately to preserve session continuity.";
|
|
2073
|
+
} else if (usagePercent >= 70) {
|
|
2074
|
+
recommendation = "handoff-recommended";
|
|
2075
|
+
message = "Context usage is high. Consider initiating handoff soon to ensure smooth transition.";
|
|
2076
|
+
} else if (usagePercent >= 50) {
|
|
2077
|
+
recommendation = "consider-handoff";
|
|
2078
|
+
message = "Context usage is moderate. Plan a good stopping point for potential handoff.";
|
|
2079
|
+
} else {
|
|
2080
|
+
recommendation = "continue";
|
|
2081
|
+
message = "Context usage is healthy. Continue working.";
|
|
2082
|
+
}
|
|
2083
|
+
const signals = [];
|
|
2084
|
+
const durationMin = Math.round((Date.now() - this.session.startTime) / 6e4);
|
|
2085
|
+
const totalCalls = this.session.toolCalls.length + this.session.resourceReads.length;
|
|
2086
|
+
if (totalCalls > 50) {
|
|
2087
|
+
signals.push(`High number of MCP interactions (${totalCalls})`);
|
|
2088
|
+
}
|
|
2089
|
+
if (durationMin > 30) {
|
|
2090
|
+
signals.push(`Session duration >30 min (${durationMin} min)`);
|
|
2091
|
+
}
|
|
2092
|
+
if (this.session.totals.totalBytes > 5e5) {
|
|
2093
|
+
signals.push(`Large data volume (${Math.round(this.session.totals.totalBytes / 1024)}KB)`);
|
|
2094
|
+
}
|
|
2095
|
+
return { recommendation, message, usagePercent, signals };
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Get session duration in minutes
|
|
2099
|
+
*/
|
|
2100
|
+
getDurationMinutes() {
|
|
2101
|
+
return Math.round((Date.now() - this.session.startTime) / 6e4);
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Reset session (for handoff or new session)
|
|
2105
|
+
*/
|
|
2106
|
+
reset() {
|
|
2107
|
+
this.session = this.createNewSession();
|
|
2108
|
+
this._recovered = false;
|
|
2109
|
+
this.lastLoreEntryId = null;
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
var tracker = null;
|
|
2113
|
+
function getSessionTracker() {
|
|
2114
|
+
if (!tracker) {
|
|
2115
|
+
tracker = new SessionTracker();
|
|
2116
|
+
}
|
|
2117
|
+
return tracker;
|
|
2118
|
+
}
|
|
2119
|
+
function resetSessionTracker() {
|
|
2120
|
+
if (tracker) {
|
|
2121
|
+
tracker.reset();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// ../paradigm-mcp/src/tools/context.ts
|
|
2126
|
+
function trackToolCall(responseSize, toolName = "unknown") {
|
|
2127
|
+
getSessionTracker().trackToolCall(toolName, responseSize);
|
|
2128
|
+
}
|
|
2129
|
+
function trackResourceRead(responseSize, uri = "paradigm://unknown") {
|
|
2130
|
+
getSessionTracker().trackResourceRead(uri, responseSize);
|
|
2131
|
+
}
|
|
2132
|
+
function resetSession() {
|
|
2133
|
+
resetSessionTracker();
|
|
2134
|
+
}
|
|
2135
|
+
function extractBreadcrumbInfo(toolName, args) {
|
|
2136
|
+
switch (toolName) {
|
|
2137
|
+
case "paradigm_search":
|
|
2138
|
+
return {
|
|
2139
|
+
summary: `Searched for "${args.query}"${args.type ? ` (type: ${args.type})` : ""}`,
|
|
2140
|
+
symbol: args.query
|
|
2141
|
+
};
|
|
2142
|
+
case "paradigm_ripple":
|
|
2143
|
+
return {
|
|
2144
|
+
summary: `Ripple analysis on ${args.symbol}${args.depth ? ` (depth: ${args.depth})` : ""}`,
|
|
2145
|
+
symbol: args.symbol
|
|
2146
|
+
};
|
|
2147
|
+
case "paradigm_related":
|
|
2148
|
+
return {
|
|
2149
|
+
summary: `Checked relations for ${args.symbol}`,
|
|
2150
|
+
symbol: args.symbol
|
|
2151
|
+
};
|
|
2152
|
+
case "paradigm_status":
|
|
2153
|
+
return { summary: "Checked project status" };
|
|
2154
|
+
case "paradigm_navigate": {
|
|
2155
|
+
const intent = args.intent;
|
|
2156
|
+
const target = args.target;
|
|
2157
|
+
const task = args.task;
|
|
2158
|
+
if (intent === "context" && task) return { summary: `Navigate context: "${task}"` };
|
|
2159
|
+
if (target) return { summary: `Navigate ${intent || "find"}: ${target}`, symbol: target };
|
|
2160
|
+
return { summary: `Navigate (${intent || "unknown"})` };
|
|
2161
|
+
}
|
|
2162
|
+
case "paradigm_gates_for_route":
|
|
2163
|
+
return { summary: `Gate suggestions for ${args.method || "GET"} ${args.route}` };
|
|
2164
|
+
case "paradigm_wisdom_context":
|
|
2165
|
+
return {
|
|
2166
|
+
summary: `Checked wisdom for ${Array.isArray(args.symbols) ? args.symbols.join(", ") : "symbols"}`,
|
|
2167
|
+
symbol: Array.isArray(args.symbols) ? args.symbols[0] : void 0
|
|
2168
|
+
};
|
|
2169
|
+
case "paradigm_history_context":
|
|
2170
|
+
return {
|
|
2171
|
+
summary: `Checked history for ${Array.isArray(args.symbols) ? args.symbols.join(", ") : "symbols"}`,
|
|
2172
|
+
symbol: Array.isArray(args.symbols) ? args.symbols[0] : void 0
|
|
2173
|
+
};
|
|
2174
|
+
case "paradigm_history_record":
|
|
2175
|
+
return {
|
|
2176
|
+
summary: `Recorded ${args.type}: ${(args.description || "").slice(0, 60)}`,
|
|
2177
|
+
symbol: Array.isArray(args.symbols) ? args.symbols[0] : void 0
|
|
2178
|
+
};
|
|
2179
|
+
case "paradigm_history_fragility":
|
|
2180
|
+
return {
|
|
2181
|
+
summary: `Checked fragility for ${Array.isArray(args.symbols) ? args.symbols.join(", ") : "symbols"}`,
|
|
2182
|
+
symbol: Array.isArray(args.symbols) ? args.symbols[0] : void 0
|
|
2183
|
+
};
|
|
2184
|
+
case "paradigm_flows_affected":
|
|
2185
|
+
return {
|
|
2186
|
+
summary: `Checked flows affected by ${args.symbol}`,
|
|
2187
|
+
symbol: args.symbol
|
|
2188
|
+
};
|
|
2189
|
+
case "paradigm_reindex":
|
|
2190
|
+
return { summary: "Rebuilt static index files" };
|
|
2191
|
+
case "paradigm_session_checkpoint":
|
|
2192
|
+
return {
|
|
2193
|
+
summary: `Checkpoint: phase=${args.phase}, ${(args.context || "").slice(0, 60)}`
|
|
2194
|
+
};
|
|
2195
|
+
case "paradigm_task_create":
|
|
2196
|
+
return { summary: `Created task: "${(args.blurb || "").slice(0, 60)}"` };
|
|
2197
|
+
case "paradigm_task_done":
|
|
2198
|
+
return { summary: `Completed task ${args.id}` };
|
|
2199
|
+
case "paradigm_task_shelve":
|
|
2200
|
+
return { summary: `Shelved task ${args.id}` };
|
|
2201
|
+
case "paradigm_task_list":
|
|
2202
|
+
return { summary: `Listed tasks (status: ${args.status || "open"})` };
|
|
2203
|
+
case "paradigm_task_update":
|
|
2204
|
+
return { summary: `Updated task ${args.id}` };
|
|
2205
|
+
case "paradigm_assessment_record":
|
|
2206
|
+
return {
|
|
2207
|
+
summary: `Assessment: ${(args.title || "").slice(0, 60)} \u2192 ${args.arc_id}`,
|
|
2208
|
+
symbol: Array.isArray(args.symbols) ? args.symbols[0] : void 0
|
|
2209
|
+
};
|
|
2210
|
+
case "paradigm_assessment_list":
|
|
2211
|
+
return { summary: args.arc_id ? `Listed entries in ${args.arc_id}` : "Listed assessment arcs" };
|
|
2212
|
+
case "paradigm_assessment_search":
|
|
2213
|
+
return {
|
|
2214
|
+
summary: `Searched assessments${args.symbol ? ` for ${args.symbol}` : ""}`,
|
|
2215
|
+
symbol: args.symbol
|
|
2216
|
+
};
|
|
2217
|
+
case "paradigm_assessment_arc_create":
|
|
2218
|
+
return { summary: `Created arc: ${args.id}` };
|
|
2219
|
+
case "paradigm_assessment_arc_close":
|
|
2220
|
+
return { summary: `Closed arc: ${args.arc_id}` };
|
|
2221
|
+
default: {
|
|
2222
|
+
const shortName = toolName.replace(/^paradigm_/, "");
|
|
2223
|
+
const firstArg = Object.values(args).find((v) => typeof v === "string" && v.length > 0);
|
|
2224
|
+
return {
|
|
2225
|
+
summary: firstArg ? `${shortName}: ${firstArg.slice(0, 60)}` : shortName,
|
|
2226
|
+
symbol: args.symbol || void 0
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function addToolBreadcrumb(toolName, args) {
|
|
2232
|
+
const tracker2 = getSessionTracker();
|
|
2233
|
+
const { summary, symbol } = extractBreadcrumbInfo(toolName, args);
|
|
2234
|
+
tracker2.addBreadcrumb("tool-call", summary, { tool: toolName, symbol });
|
|
2235
|
+
}
|
|
2236
|
+
function getContextToolsList() {
|
|
2237
|
+
return [
|
|
2238
|
+
{
|
|
2239
|
+
name: "paradigm_context_check",
|
|
2240
|
+
description: "Check if context handoff is recommended based on session activity. Call this periodically during long sessions. Returns usage percentage and recommendation (continue, consider-handoff, handoff-recommended, handoff-urgent). ~100 tokens.",
|
|
2241
|
+
inputSchema: {
|
|
2242
|
+
type: "object",
|
|
2243
|
+
properties: {
|
|
2244
|
+
estimatedTotalTokens: {
|
|
2245
|
+
type: "number",
|
|
2246
|
+
description: "Optional: Your estimate of total conversation tokens (if available)"
|
|
2247
|
+
},
|
|
2248
|
+
contextWindowSize: {
|
|
2249
|
+
type: "number",
|
|
2250
|
+
description: "Context window size in tokens (default: 200000)"
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
},
|
|
2254
|
+
annotations: {
|
|
2255
|
+
readOnlyHint: true,
|
|
2256
|
+
destructiveHint: false
|
|
2257
|
+
}
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
name: "paradigm_handoff_prepare",
|
|
2261
|
+
description: "Prepare a handoff summary. Generates a structured handoff file with markdown summary and recovery instructions. Returns structured markdown with summary, modified files, and next steps. ~300 tokens.",
|
|
2262
|
+
inputSchema: {
|
|
2263
|
+
type: "object",
|
|
2264
|
+
properties: {
|
|
2265
|
+
summary: {
|
|
2266
|
+
type: "string",
|
|
2267
|
+
description: "Brief summary of work done in this session"
|
|
2268
|
+
},
|
|
2269
|
+
nextSteps: {
|
|
2270
|
+
type: "array",
|
|
2271
|
+
items: { type: "string" },
|
|
2272
|
+
description: "List of next steps for the continuing session"
|
|
2273
|
+
},
|
|
2274
|
+
agent: {
|
|
2275
|
+
type: "string",
|
|
2276
|
+
description: 'Target agent role (e.g., "builder", "architect")'
|
|
2277
|
+
},
|
|
2278
|
+
modifiedFiles: {
|
|
2279
|
+
type: "array",
|
|
2280
|
+
items: { type: "string" },
|
|
2281
|
+
description: "List of files modified in this session"
|
|
2282
|
+
},
|
|
2283
|
+
symbolsTouched: {
|
|
2284
|
+
type: "array",
|
|
2285
|
+
items: { type: "string" },
|
|
2286
|
+
description: "List of symbols (@feature, #component, etc.) touched"
|
|
2287
|
+
},
|
|
2288
|
+
openQuestions: {
|
|
2289
|
+
type: "array",
|
|
2290
|
+
items: { type: "string" },
|
|
2291
|
+
description: "Unresolved questions or decisions needed"
|
|
2292
|
+
}
|
|
2293
|
+
},
|
|
2294
|
+
required: ["summary"]
|
|
2295
|
+
},
|
|
2296
|
+
annotations: {
|
|
2297
|
+
readOnlyHint: false,
|
|
2298
|
+
destructiveHint: false
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "paradigm_session_stats",
|
|
2303
|
+
description: "Get current session statistics (MCP interactions, estimated tokens). Returns tool call count, estimated tokens used, and cost breakdown. ~100 tokens.",
|
|
2304
|
+
inputSchema: {
|
|
2305
|
+
type: "object",
|
|
2306
|
+
properties: {}
|
|
2307
|
+
},
|
|
2308
|
+
annotations: {
|
|
2309
|
+
readOnlyHint: true,
|
|
2310
|
+
destructiveHint: false
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
name: "paradigm_session_recover",
|
|
2315
|
+
description: "Load previous session breadcrumbs for continuity. Call this at the start of a new session to understand what was done before. Returns symbols modified, files explored, recent actions, and suggestions for continuity. ~200 tokens.",
|
|
2316
|
+
inputSchema: {
|
|
2317
|
+
type: "object",
|
|
2318
|
+
properties: {}
|
|
2319
|
+
},
|
|
2320
|
+
annotations: {
|
|
2321
|
+
readOnlyHint: true,
|
|
2322
|
+
destructiveHint: false
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
name: "paradigm_session_checkpoint",
|
|
2327
|
+
description: "Save a cognitive-transition checkpoint for crash recovery. Call when transitioning between phases (planning \u2192 implementing \u2192 validating \u2192 complete). ~100 tokens.",
|
|
2328
|
+
inputSchema: {
|
|
2329
|
+
type: "object",
|
|
2330
|
+
properties: {
|
|
2331
|
+
phase: {
|
|
2332
|
+
type: "string",
|
|
2333
|
+
enum: ["planning", "implementing", "validating", "complete"],
|
|
2334
|
+
description: "Current workflow phase"
|
|
2335
|
+
},
|
|
2336
|
+
context: {
|
|
2337
|
+
type: "string",
|
|
2338
|
+
description: "What's top-of-mind right now (1-3 sentences)"
|
|
2339
|
+
},
|
|
2340
|
+
plan: {
|
|
2341
|
+
type: "string",
|
|
2342
|
+
description: "Optional: the current plan or approach"
|
|
2343
|
+
},
|
|
2344
|
+
modifiedFiles: {
|
|
2345
|
+
type: "array",
|
|
2346
|
+
items: { type: "string" },
|
|
2347
|
+
description: "Optional: files modified so far"
|
|
2348
|
+
},
|
|
2349
|
+
symbolsTouched: {
|
|
2350
|
+
type: "array",
|
|
2351
|
+
items: { type: "string" },
|
|
2352
|
+
description: "Optional: symbols touched so far"
|
|
2353
|
+
},
|
|
2354
|
+
decisions: {
|
|
2355
|
+
type: "array",
|
|
2356
|
+
items: { type: "string" },
|
|
2357
|
+
description: "Optional: key decisions made so far"
|
|
2358
|
+
}
|
|
2359
|
+
},
|
|
2360
|
+
required: ["phase", "context"]
|
|
2361
|
+
},
|
|
2362
|
+
annotations: {
|
|
2363
|
+
readOnlyHint: false,
|
|
2364
|
+
destructiveHint: false
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
name: "paradigm_session_checkpoint",
|
|
2369
|
+
description: "Save a cognitive-transition checkpoint for crash recovery. Call when transitioning between phases (planning \u2192 implementing \u2192 validating \u2192 complete). ~100 tokens.",
|
|
2370
|
+
inputSchema: {
|
|
2371
|
+
type: "object",
|
|
2372
|
+
properties: {
|
|
2373
|
+
phase: {
|
|
2374
|
+
type: "string",
|
|
2375
|
+
enum: ["planning", "implementing", "validating", "complete"],
|
|
2376
|
+
description: "Current workflow phase"
|
|
2377
|
+
},
|
|
2378
|
+
context: {
|
|
2379
|
+
type: "string",
|
|
2380
|
+
description: "What's top-of-mind right now (1-3 sentences)"
|
|
2381
|
+
},
|
|
2382
|
+
plan: {
|
|
2383
|
+
type: "string",
|
|
2384
|
+
description: "Optional: the current plan or approach"
|
|
2385
|
+
},
|
|
2386
|
+
modifiedFiles: {
|
|
2387
|
+
type: "array",
|
|
2388
|
+
items: { type: "string" },
|
|
2389
|
+
description: "Optional: files modified so far"
|
|
2390
|
+
},
|
|
2391
|
+
symbolsTouched: {
|
|
2392
|
+
type: "array",
|
|
2393
|
+
items: { type: "string" },
|
|
2394
|
+
description: "Optional: symbols touched so far"
|
|
2395
|
+
},
|
|
2396
|
+
decisions: {
|
|
2397
|
+
type: "array",
|
|
2398
|
+
items: { type: "string" },
|
|
2399
|
+
description: "Optional: key decisions made so far"
|
|
2400
|
+
}
|
|
2401
|
+
},
|
|
2402
|
+
required: ["phase", "context"]
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
];
|
|
2406
|
+
}
|
|
2407
|
+
async function handleContextTool(name, args, _ctx) {
|
|
2408
|
+
const tracker2 = getSessionTracker();
|
|
2409
|
+
if (name === "paradigm_context_check") {
|
|
2410
|
+
const contextWindowSize = args.contextWindowSize || 2e5;
|
|
2411
|
+
const estimatedTotal = args.estimatedTotalTokens;
|
|
2412
|
+
const stats = tracker2.getStats();
|
|
2413
|
+
const { recommendation, message, usagePercent, signals } = tracker2.getHandoffRecommendation(
|
|
2414
|
+
contextWindowSize,
|
|
2415
|
+
estimatedTotal
|
|
2416
|
+
);
|
|
2417
|
+
const durationMin = tracker2.getDurationMinutes();
|
|
2418
|
+
return {
|
|
2419
|
+
handled: true,
|
|
2420
|
+
text: JSON.stringify({
|
|
2421
|
+
recommendation,
|
|
2422
|
+
message,
|
|
2423
|
+
stats: {
|
|
2424
|
+
sessionDurationMinutes: durationMin,
|
|
2425
|
+
mcpToolCalls: stats.totals.toolCallCount,
|
|
2426
|
+
mcpResourceReads: stats.totals.resourceReadCount,
|
|
2427
|
+
estimatedMcpTokens: stats.totals.totalTokens,
|
|
2428
|
+
estimatedTotalTokens: estimatedTotal || Math.round(stats.totals.totalTokens * 5),
|
|
2429
|
+
contextWindowSize,
|
|
2430
|
+
usagePercent
|
|
2431
|
+
},
|
|
2432
|
+
signals,
|
|
2433
|
+
action: recommendation === "continue" ? null : "Call paradigm_handoff_prepare to create handoff file"
|
|
2434
|
+
}, null, 2)
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
if (name === "paradigm_handoff_prepare") {
|
|
2438
|
+
const summary = args.summary;
|
|
2439
|
+
const nextSteps = args.nextSteps || [];
|
|
2440
|
+
const agent = args.agent || "builder";
|
|
2441
|
+
const modifiedFiles = args.modifiedFiles || [];
|
|
2442
|
+
const symbolsTouched = args.symbolsTouched || [];
|
|
2443
|
+
const openQuestions = args.openQuestions || [];
|
|
2444
|
+
const stats = tracker2.getStats();
|
|
2445
|
+
const breakdown = tracker2.getCostBreakdown();
|
|
2446
|
+
const handoffId = `h${Date.now().toString(36)}`;
|
|
2447
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2448
|
+
const handoffPayload = {
|
|
2449
|
+
id: handoffId,
|
|
2450
|
+
timestamp,
|
|
2451
|
+
from: "current-session",
|
|
2452
|
+
to: agent,
|
|
2453
|
+
summary,
|
|
2454
|
+
nextSteps,
|
|
2455
|
+
modifiedFiles,
|
|
2456
|
+
symbolsTouched,
|
|
2457
|
+
openQuestions,
|
|
2458
|
+
sessionStats: {
|
|
2459
|
+
duration: tracker2.getDurationMinutes(),
|
|
2460
|
+
mcpCalls: stats.totals.toolCallCount + stats.totals.resourceReadCount,
|
|
2461
|
+
estimatedTokens: stats.totals.totalTokens,
|
|
2462
|
+
estimatedCostUsd: breakdown.total.costUsd,
|
|
2463
|
+
model: breakdown.model
|
|
2464
|
+
},
|
|
2465
|
+
status: "pending"
|
|
2466
|
+
};
|
|
2467
|
+
try {
|
|
2468
|
+
writePendingHandoff(_ctx.rootDir, handoffPayload);
|
|
2469
|
+
} catch {
|
|
2470
|
+
}
|
|
2471
|
+
const markdownSummary = `# Handoff: ${timestamp}
|
|
2472
|
+
|
|
2473
|
+
## Session Summary
|
|
2474
|
+
${summary}
|
|
2475
|
+
|
|
2476
|
+
## Next Steps
|
|
2477
|
+
${nextSteps.map((step, i) => `${i + 1}. ${step}`).join("\n") || "(none specified)"}
|
|
2478
|
+
|
|
2479
|
+
## Key Context
|
|
2480
|
+
- Modified files: ${modifiedFiles.length > 0 ? modifiedFiles.join(", ") : "(not specified)"}
|
|
2481
|
+
- Symbols touched: ${symbolsTouched.length > 0 ? symbolsTouched.join(", ") : "(not specified)"}
|
|
2482
|
+
- Open questions: ${openQuestions.length > 0 ? openQuestions.join(", ") : "(none)"}
|
|
2483
|
+
`;
|
|
2484
|
+
resetSession();
|
|
2485
|
+
return {
|
|
2486
|
+
handled: true,
|
|
2487
|
+
text: JSON.stringify({
|
|
2488
|
+
handoff: handoffPayload,
|
|
2489
|
+
markdownSummary,
|
|
2490
|
+
persisted: true,
|
|
2491
|
+
recovery: "The next session will automatically receive this handoff via paradigm_session_recover."
|
|
2492
|
+
}, null, 2)
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
if (name === "paradigm_session_stats") {
|
|
2496
|
+
const stats = tracker2.getStats();
|
|
2497
|
+
const breakdown = tracker2.getCostBreakdown();
|
|
2498
|
+
const durationMin = tracker2.getDurationMinutes();
|
|
2499
|
+
return {
|
|
2500
|
+
handled: true,
|
|
2501
|
+
text: JSON.stringify({
|
|
2502
|
+
session: {
|
|
2503
|
+
startTime: new Date(stats.startTime).toISOString(),
|
|
2504
|
+
durationMinutes: durationMin,
|
|
2505
|
+
lastActivity: new Date(stats.lastActivity).toISOString()
|
|
2506
|
+
},
|
|
2507
|
+
model: {
|
|
2508
|
+
name: breakdown.model,
|
|
2509
|
+
id: breakdown.modelId,
|
|
2510
|
+
pricing: {
|
|
2511
|
+
inputPerMillion: `$${breakdown.pricing.input.toFixed(2)}`,
|
|
2512
|
+
outputPerMillion: `$${breakdown.pricing.output.toFixed(2)}`
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
interactions: {
|
|
2516
|
+
toolCalls: stats.totals.toolCallCount,
|
|
2517
|
+
resourceReads: stats.totals.resourceReadCount,
|
|
2518
|
+
totalInteractions: stats.totals.toolCallCount + stats.totals.resourceReadCount
|
|
2519
|
+
},
|
|
2520
|
+
tokens: {
|
|
2521
|
+
total: stats.totals.totalTokens,
|
|
2522
|
+
byCategory: {
|
|
2523
|
+
resources: breakdown.resources.tokens,
|
|
2524
|
+
tools: breakdown.tools.tokens
|
|
2525
|
+
}
|
|
2526
|
+
},
|
|
2527
|
+
cost: {
|
|
2528
|
+
totalUsd: `$${breakdown.total.costUsd.toFixed(4)}`,
|
|
2529
|
+
breakdown: {
|
|
2530
|
+
resources: `$${breakdown.resources.costUsd.toFixed(4)}`,
|
|
2531
|
+
tools: `$${breakdown.tools.costUsd.toFixed(4)}`
|
|
2532
|
+
},
|
|
2533
|
+
note: "Cost is for MCP output tokens only (responses sent to model)"
|
|
2534
|
+
},
|
|
2535
|
+
details: {
|
|
2536
|
+
resourcesByType: breakdown.resources.byType,
|
|
2537
|
+
toolsByName: breakdown.tools.byName
|
|
2538
|
+
}
|
|
2539
|
+
}, null, 2)
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
if (name === "paradigm_session_recover") {
|
|
2543
|
+
tracker2.setRootDir(_ctx.rootDir);
|
|
2544
|
+
const previousSession = tracker2.loadPreviousSession();
|
|
2545
|
+
const checkpoint = tracker2.loadCheckpoint();
|
|
2546
|
+
let pendingHandoffs = [];
|
|
2547
|
+
try {
|
|
2548
|
+
pendingHandoffs = loadPendingHandoffs(_ctx.rootDir);
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
if (!previousSession && pendingHandoffs.length === 0 && !checkpoint) {
|
|
2552
|
+
return {
|
|
2553
|
+
handled: true,
|
|
2554
|
+
text: JSON.stringify({
|
|
2555
|
+
found: false,
|
|
2556
|
+
message: "No previous session breadcrumbs, checkpoints, or pending handoffs found.",
|
|
2557
|
+
tip: "Breadcrumbs persist to ~/.paradigm/sessions/ and handoffs persist via paradigm_handoff_prepare. Checkpoints persist via paradigm_session_checkpoint."
|
|
2558
|
+
}, null, 2)
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
const result = { found: true };
|
|
2562
|
+
if (checkpoint) {
|
|
2563
|
+
const ageMs = Date.now() - checkpoint.timestamp;
|
|
2564
|
+
const ageMinutes = Math.round(ageMs / 6e4);
|
|
2565
|
+
const ageHours = Math.round(ageMs / 36e5);
|
|
2566
|
+
result.checkpoint = {
|
|
2567
|
+
phase: checkpoint.phase,
|
|
2568
|
+
context: checkpoint.context,
|
|
2569
|
+
age: ageHours > 1 ? `${ageHours} hours ago` : `${ageMinutes} minutes ago`,
|
|
2570
|
+
timestamp: new Date(checkpoint.timestamp).toISOString(),
|
|
2571
|
+
sessionId: checkpoint.sessionId,
|
|
2572
|
+
plan: checkpoint.plan,
|
|
2573
|
+
modifiedFiles: checkpoint.modifiedFiles,
|
|
2574
|
+
symbolsTouched: checkpoint.symbolsTouched,
|
|
2575
|
+
decisions: checkpoint.decisions,
|
|
2576
|
+
recentBreadcrumbs: checkpoint.recentBreadcrumbs?.map((bc) => ({
|
|
2577
|
+
time: new Date(bc.timestamp).toISOString(),
|
|
2578
|
+
action: bc.action,
|
|
2579
|
+
tool: bc.tool,
|
|
2580
|
+
symbol: bc.symbol,
|
|
2581
|
+
summary: bc.summary
|
|
2582
|
+
}))
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
if (previousSession) {
|
|
2586
|
+
const ageMs = Date.now() - previousSession.lastActivity;
|
|
2587
|
+
const ageMinutes = Math.round(ageMs / 6e4);
|
|
2588
|
+
const ageHours = Math.round(ageMs / 36e5);
|
|
2589
|
+
const recentActions = previousSession.breadcrumbs.slice(-10);
|
|
2590
|
+
const actionSummary = recentActions.map((bc) => ({
|
|
2591
|
+
time: new Date(bc.timestamp).toISOString(),
|
|
2592
|
+
action: bc.action,
|
|
2593
|
+
tool: bc.tool,
|
|
2594
|
+
symbol: bc.symbol,
|
|
2595
|
+
summary: bc.summary
|
|
2596
|
+
}));
|
|
2597
|
+
result.previousSession = {
|
|
2598
|
+
sessionId: previousSession.sessionId,
|
|
2599
|
+
startTime: new Date(previousSession.startTime).toISOString(),
|
|
2600
|
+
lastActivity: new Date(previousSession.lastActivity).toISOString(),
|
|
2601
|
+
age: ageHours > 1 ? `${ageHours} hours ago` : `${ageMinutes} minutes ago`
|
|
2602
|
+
};
|
|
2603
|
+
result.context = {
|
|
2604
|
+
symbolsModified: previousSession.symbolsModified,
|
|
2605
|
+
filesExplored: previousSession.filesExplored
|
|
2606
|
+
};
|
|
2607
|
+
result.recentActions = actionSummary;
|
|
2608
|
+
}
|
|
2609
|
+
if (pendingHandoffs.length > 0) {
|
|
2610
|
+
result.pendingHandoffs = pendingHandoffs.map((h) => ({
|
|
2611
|
+
id: h.id,
|
|
2612
|
+
timestamp: h.timestamp,
|
|
2613
|
+
from: h.from,
|
|
2614
|
+
to: h.to,
|
|
2615
|
+
summary: h.summary,
|
|
2616
|
+
nextSteps: h.nextSteps,
|
|
2617
|
+
modifiedFiles: h.modifiedFiles,
|
|
2618
|
+
symbolsTouched: h.symbolsTouched,
|
|
2619
|
+
openQuestions: h.openQuestions
|
|
2620
|
+
}));
|
|
2621
|
+
for (const h of pendingHandoffs) {
|
|
2622
|
+
try {
|
|
2623
|
+
markHandoffDelivered(_ctx.rootDir, h.id);
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
let suggestion = "Continue where the previous session left off.";
|
|
2629
|
+
if (checkpoint) {
|
|
2630
|
+
suggestion = `Previous session was in "${checkpoint.phase}" phase: ${checkpoint.context}`;
|
|
2631
|
+
if (checkpoint.decisions?.length) {
|
|
2632
|
+
suggestion += ` Key decisions: ${checkpoint.decisions.slice(0, 2).join("; ")}`;
|
|
2633
|
+
}
|
|
2634
|
+
} else if (pendingHandoffs.length > 0) {
|
|
2635
|
+
const latest = pendingHandoffs[pendingHandoffs.length - 1];
|
|
2636
|
+
suggestion = `Handoff received: "${latest.summary}". `;
|
|
2637
|
+
if (latest.nextSteps.length > 0) {
|
|
2638
|
+
suggestion += `Start with: ${latest.nextSteps[0]}`;
|
|
2639
|
+
}
|
|
2640
|
+
} else if (previousSession) {
|
|
2641
|
+
const recentActions = previousSession.breadcrumbs.slice(-10);
|
|
2642
|
+
if (recentActions.length > 0) {
|
|
2643
|
+
const lastAction = recentActions[recentActions.length - 1];
|
|
2644
|
+
if (lastAction.symbol) {
|
|
2645
|
+
suggestion = `Last work involved ${lastAction.symbol}. Consider checking its current state with paradigm_ripple.`;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
result.suggestion = suggestion;
|
|
2650
|
+
result.agentInstruction = "Present a brief summary of the previous session, then ask the user what they would like to do: (1) Continue \u2014 pick up where the last session left off, (2) Discard \u2014 ignore the previous session and start fresh, or (3) let them describe what they want to work on instead. Do NOT automatically continue without asking.";
|
|
2651
|
+
tracker2.markRecovered();
|
|
2652
|
+
return {
|
|
2653
|
+
handled: true,
|
|
2654
|
+
text: JSON.stringify(result, null, 2)
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
if (name === "paradigm_session_checkpoint") {
|
|
2658
|
+
tracker2.setRootDir(_ctx.rootDir);
|
|
2659
|
+
const phase = args.phase;
|
|
2660
|
+
const context = args.context;
|
|
2661
|
+
const plan = args.plan;
|
|
2662
|
+
const modifiedFiles = args.modifiedFiles;
|
|
2663
|
+
const symbolsTouched = args.symbolsTouched;
|
|
2664
|
+
const decisions = args.decisions;
|
|
2665
|
+
const { checkpoint, persisted } = tracker2.saveCheckpoint({
|
|
2666
|
+
phase,
|
|
2667
|
+
context,
|
|
2668
|
+
plan,
|
|
2669
|
+
modifiedFiles,
|
|
2670
|
+
symbolsTouched,
|
|
2671
|
+
decisions
|
|
2672
|
+
});
|
|
2673
|
+
const anyPersisted = persisted.local || persisted.global;
|
|
2674
|
+
return {
|
|
2675
|
+
handled: true,
|
|
2676
|
+
text: JSON.stringify({
|
|
2677
|
+
saved: anyPersisted,
|
|
2678
|
+
persisted,
|
|
2679
|
+
checkpoint: {
|
|
2680
|
+
phase: checkpoint.phase,
|
|
2681
|
+
context: checkpoint.context,
|
|
2682
|
+
sessionId: checkpoint.sessionId,
|
|
2683
|
+
timestamp: new Date(checkpoint.timestamp).toISOString(),
|
|
2684
|
+
modifiedFiles: checkpoint.modifiedFiles?.length || 0,
|
|
2685
|
+
symbolsTouched: checkpoint.symbolsTouched?.length || 0,
|
|
2686
|
+
decisions: checkpoint.decisions?.length || 0,
|
|
2687
|
+
recentBreadcrumbs: checkpoint.recentBreadcrumbs?.length || 0
|
|
2688
|
+
},
|
|
2689
|
+
...anyPersisted ? { note: "Checkpoint saved. Recovery data will be auto-surfaced on the first tool call of the next session." } : { warning: "Checkpoint was NOT persisted to disk. Both local and global writes failed. Check MCP server stderr for details." }
|
|
2690
|
+
}, null, 2)
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
return { handled: false, text: "" };
|
|
2694
|
+
}
|
|
2695
|
+
async function buildRecoveryPreamble(rootDir) {
|
|
2696
|
+
const tracker2 = getSessionTracker();
|
|
2697
|
+
tracker2.setRootDir(rootDir);
|
|
2698
|
+
const checkpoint = tracker2.loadCheckpoint();
|
|
2699
|
+
let pendingHandoffs = [];
|
|
2700
|
+
try {
|
|
2701
|
+
pendingHandoffs = loadPendingHandoffs(rootDir);
|
|
2702
|
+
} catch {
|
|
2703
|
+
}
|
|
2704
|
+
if (!checkpoint && pendingHandoffs.length === 0) {
|
|
2705
|
+
return null;
|
|
2706
|
+
}
|
|
2707
|
+
const lines = [];
|
|
2708
|
+
lines.push("--- SESSION RECOVERY ---");
|
|
2709
|
+
if (checkpoint) {
|
|
2710
|
+
const ageMs = Date.now() - checkpoint.timestamp;
|
|
2711
|
+
const ageMinutes = Math.round(ageMs / 6e4);
|
|
2712
|
+
const ageHours = Math.round(ageMs / 36e5);
|
|
2713
|
+
const ageStr = ageHours > 1 ? `${ageHours}h ago` : `${ageMinutes}m ago`;
|
|
2714
|
+
lines.push(`Previous session was in "${checkpoint.phase}" phase (${ageStr}): ${checkpoint.context}`);
|
|
2715
|
+
if (checkpoint.modifiedFiles?.length) {
|
|
2716
|
+
lines.push(`Modified files: ${checkpoint.modifiedFiles.join(", ")}`);
|
|
2717
|
+
}
|
|
2718
|
+
if (checkpoint.symbolsTouched?.length) {
|
|
2719
|
+
lines.push(`Symbols: ${checkpoint.symbolsTouched.join(", ")}`);
|
|
2720
|
+
}
|
|
2721
|
+
if (checkpoint.decisions?.length) {
|
|
2722
|
+
lines.push(`Decisions: ${checkpoint.decisions.join("; ")}`);
|
|
2723
|
+
}
|
|
2724
|
+
if (checkpoint.plan) {
|
|
2725
|
+
lines.push(`Plan: ${checkpoint.plan.slice(0, 200)}`);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (pendingHandoffs.length > 0) {
|
|
2729
|
+
const latest = pendingHandoffs[pendingHandoffs.length - 1];
|
|
2730
|
+
lines.push(`Pending handoff: "${latest.summary}"`);
|
|
2731
|
+
if (latest.nextSteps.length > 0) {
|
|
2732
|
+
lines.push(`Next steps: ${latest.nextSteps.slice(0, 3).join(", ")}`);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
try {
|
|
2736
|
+
const { loadTasks } = await import("./task-loader-7M2FCBX6.js");
|
|
2737
|
+
const openTasks = await loadTasks(rootDir, { status: "open", limit: 5 });
|
|
2738
|
+
if (openTasks.length > 0) {
|
|
2739
|
+
lines.push("");
|
|
2740
|
+
lines.push("Open tasks:");
|
|
2741
|
+
for (const task of openTasks) {
|
|
2742
|
+
const tags = task.tags.length > 0 ? ` [${task.tags.join(", ")}]` : "";
|
|
2743
|
+
lines.push(` [${task.priority}] ${task.id}: ${task.blurb}${tags}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
} catch {
|
|
2747
|
+
}
|
|
2748
|
+
try {
|
|
2749
|
+
const { loadArcs } = await import("./assessment-loader-C5EOUM47.js");
|
|
2750
|
+
const activeArcs = await loadArcs(rootDir, "active");
|
|
2751
|
+
if (activeArcs.length > 0) {
|
|
2752
|
+
const checkpointSymbols = checkpoint?.symbolsTouched || [];
|
|
2753
|
+
const relevantArcs = checkpointSymbols.length > 0 ? activeArcs.filter((arc) => arc.symbols.some((s) => checkpointSymbols.includes(s))) : activeArcs.slice(0, 3);
|
|
2754
|
+
if (relevantArcs.length > 0) {
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
lines.push("Active assessment arcs:");
|
|
2757
|
+
for (const arc of relevantArcs) {
|
|
2758
|
+
lines.push(` ${arc.id}: ${arc.name} (${arc.entry_count} entries)`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
lines.push("");
|
|
2765
|
+
lines.push("IMPORTANT: Present a brief summary of this recovery data to the user, then ask what they would like to do: (1) Continue \u2014 pick up where the last session left off, (2) Discard \u2014 ignore the previous session and start fresh, or (3) let them describe what they want to work on instead. Do NOT automatically continue without asking.");
|
|
2766
|
+
lines.push("---");
|
|
2767
|
+
return lines.join("\n");
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// ../paradigm-mcp/src/utils/tool-cache.ts
|
|
2771
|
+
var ToolCache = class {
|
|
2772
|
+
cache = /* @__PURE__ */ new Map();
|
|
2773
|
+
ttlMs;
|
|
2774
|
+
constructor(ttlMs = 3e4) {
|
|
2775
|
+
this.ttlMs = ttlMs;
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Get a cached value, or compute and cache it
|
|
2779
|
+
*/
|
|
2780
|
+
async getOrCompute(key, compute) {
|
|
2781
|
+
const existing = this.cache.get(key);
|
|
2782
|
+
if (existing && Date.now() - existing.createdAt < this.ttlMs) {
|
|
2783
|
+
return existing.data;
|
|
2784
|
+
}
|
|
2785
|
+
const data = await compute();
|
|
2786
|
+
this.cache.set(key, { data, createdAt: Date.now() });
|
|
2787
|
+
return data;
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Invalidate a specific key
|
|
2791
|
+
*/
|
|
2792
|
+
invalidate(key) {
|
|
2793
|
+
this.cache.delete(key);
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Invalidate all keys matching a prefix
|
|
2797
|
+
*/
|
|
2798
|
+
invalidatePrefix(prefix) {
|
|
2799
|
+
for (const key of this.cache.keys()) {
|
|
2800
|
+
if (key.startsWith(prefix)) {
|
|
2801
|
+
this.cache.delete(key);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Clear the entire cache
|
|
2807
|
+
*/
|
|
2808
|
+
clear() {
|
|
2809
|
+
this.cache.clear();
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Get cache stats for debugging
|
|
2813
|
+
*/
|
|
2814
|
+
stats() {
|
|
2815
|
+
return { size: this.cache.size, ttlMs: this.ttlMs };
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
var toolCache = new ToolCache(3e4);
|
|
2819
|
+
|
|
2820
|
+
// ../paradigm-mcp/src/utils/aspect-graph.ts
|
|
2821
|
+
import * as fs5 from "fs";
|
|
2822
|
+
import * as path6 from "path";
|
|
2823
|
+
import * as crypto2 from "crypto";
|
|
2824
|
+
import { execSync } from "child_process";
|
|
2825
|
+
import initSqlJs from "sql.js";
|
|
2826
|
+
var cachedSQL = null;
|
|
2827
|
+
async function getSqlJs() {
|
|
2828
|
+
if (!cachedSQL) {
|
|
2829
|
+
cachedSQL = await initSqlJs();
|
|
2830
|
+
}
|
|
2831
|
+
return cachedSQL;
|
|
2832
|
+
}
|
|
2833
|
+
var SCHEMA_STATEMENTS = [
|
|
2834
|
+
`CREATE TABLE IF NOT EXISTS aspects (
|
|
2835
|
+
id TEXT PRIMARY KEY,
|
|
2836
|
+
description TEXT NOT NULL,
|
|
2837
|
+
category TEXT DEFAULT 'rule',
|
|
2838
|
+
severity TEXT DEFAULT 'medium',
|
|
2839
|
+
value TEXT,
|
|
2840
|
+
enforcement TEXT,
|
|
2841
|
+
defined_in TEXT NOT NULL,
|
|
2842
|
+
tags TEXT,
|
|
2843
|
+
created_at TEXT NOT NULL,
|
|
2844
|
+
updated_at TEXT NOT NULL
|
|
2845
|
+
)`,
|
|
2846
|
+
`CREATE TABLE IF NOT EXISTS anchors (
|
|
2847
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2848
|
+
aspect_id TEXT NOT NULL REFERENCES aspects(id) ON DELETE CASCADE,
|
|
2849
|
+
file_path TEXT NOT NULL,
|
|
2850
|
+
start_line INTEGER NOT NULL,
|
|
2851
|
+
end_line INTEGER NOT NULL,
|
|
2852
|
+
content_hash TEXT,
|
|
2853
|
+
normalized_hash TEXT,
|
|
2854
|
+
materialized_at_commit TEXT,
|
|
2855
|
+
last_verified TEXT,
|
|
2856
|
+
drifted INTEGER DEFAULT 0
|
|
2857
|
+
)`,
|
|
2858
|
+
`CREATE INDEX IF NOT EXISTS idx_anchors_file ON anchors(file_path)`,
|
|
2859
|
+
`CREATE INDEX IF NOT EXISTS idx_anchors_aspect ON anchors(aspect_id)`,
|
|
2860
|
+
`CREATE TABLE IF NOT EXISTS edges (
|
|
2861
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2862
|
+
source TEXT NOT NULL,
|
|
2863
|
+
target TEXT NOT NULL,
|
|
2864
|
+
relation TEXT NOT NULL,
|
|
2865
|
+
weight REAL DEFAULT 1.0,
|
|
2866
|
+
origin TEXT DEFAULT 'explicit',
|
|
2867
|
+
created_at TEXT NOT NULL
|
|
2868
|
+
)`,
|
|
2869
|
+
`CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)`,
|
|
2870
|
+
`CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)`,
|
|
2871
|
+
`CREATE TABLE IF NOT EXISTS lore_links (
|
|
2872
|
+
aspect_id TEXT NOT NULL,
|
|
2873
|
+
lore_id TEXT NOT NULL,
|
|
2874
|
+
PRIMARY KEY (aspect_id, lore_id)
|
|
2875
|
+
)`,
|
|
2876
|
+
`CREATE TABLE IF NOT EXISTS search_log (
|
|
2877
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2878
|
+
query TEXT NOT NULL,
|
|
2879
|
+
results_returned TEXT NOT NULL,
|
|
2880
|
+
selected_result TEXT,
|
|
2881
|
+
timestamp TEXT NOT NULL
|
|
2882
|
+
)`,
|
|
2883
|
+
`CREATE INDEX IF NOT EXISTS idx_search_query ON search_log(query)`,
|
|
2884
|
+
`CREATE TABLE IF NOT EXISTS search_weights (
|
|
2885
|
+
query_normalized TEXT NOT NULL,
|
|
2886
|
+
aspect_id TEXT NOT NULL,
|
|
2887
|
+
weight REAL DEFAULT 1.0,
|
|
2888
|
+
hit_count INTEGER DEFAULT 1,
|
|
2889
|
+
last_hit TEXT NOT NULL,
|
|
2890
|
+
PRIMARY KEY (query_normalized, aspect_id)
|
|
2891
|
+
)`,
|
|
2892
|
+
`CREATE TABLE IF NOT EXISTS heatmap (
|
|
2893
|
+
aspect_id TEXT NOT NULL,
|
|
2894
|
+
access_type TEXT NOT NULL,
|
|
2895
|
+
count INTEGER DEFAULT 0,
|
|
2896
|
+
last_accessed TEXT NOT NULL,
|
|
2897
|
+
PRIMARY KEY (aspect_id, access_type)
|
|
2898
|
+
)`
|
|
2899
|
+
];
|
|
2900
|
+
var FTS_SQL = `CREATE VIRTUAL TABLE IF NOT EXISTS aspects_fts USING fts5(id, description, enforcement, tags)`;
|
|
2901
|
+
function queryRows(db, sql, params) {
|
|
2902
|
+
const stmt = db.prepare(sql);
|
|
2903
|
+
if (params && params.length > 0) {
|
|
2904
|
+
stmt.bind(params);
|
|
2905
|
+
}
|
|
2906
|
+
const rows = [];
|
|
2907
|
+
while (stmt.step()) {
|
|
2908
|
+
rows.push(stmt.getAsObject());
|
|
2909
|
+
}
|
|
2910
|
+
stmt.free();
|
|
2911
|
+
return rows;
|
|
2912
|
+
}
|
|
2913
|
+
function queryOne(db, sql, params) {
|
|
2914
|
+
const stmt = db.prepare(sql);
|
|
2915
|
+
if (params && params.length > 0) {
|
|
2916
|
+
stmt.bind(params);
|
|
2917
|
+
}
|
|
2918
|
+
let result = null;
|
|
2919
|
+
if (stmt.step()) {
|
|
2920
|
+
result = stmt.getAsObject();
|
|
2921
|
+
}
|
|
2922
|
+
stmt.free();
|
|
2923
|
+
return result;
|
|
2924
|
+
}
|
|
2925
|
+
async function openAspectGraph(rootDir) {
|
|
2926
|
+
const SQL = await getSqlJs();
|
|
2927
|
+
const dbDir = path6.join(rootDir, ".paradigm");
|
|
2928
|
+
const dbPath = path6.join(dbDir, "aspect-graph.db");
|
|
2929
|
+
let db;
|
|
2930
|
+
if (fs5.existsSync(dbPath)) {
|
|
2931
|
+
const buffer = fs5.readFileSync(dbPath);
|
|
2932
|
+
db = new SQL.Database(buffer);
|
|
2933
|
+
} else {
|
|
2934
|
+
if (!fs5.existsSync(dbDir)) {
|
|
2935
|
+
fs5.mkdirSync(dbDir, { recursive: true });
|
|
2936
|
+
}
|
|
2937
|
+
db = new SQL.Database();
|
|
2938
|
+
}
|
|
2939
|
+
for (const stmt of SCHEMA_STATEMENTS) {
|
|
2940
|
+
db.run(stmt);
|
|
2941
|
+
}
|
|
2942
|
+
try {
|
|
2943
|
+
db.run(FTS_SQL);
|
|
2944
|
+
} catch {
|
|
2945
|
+
}
|
|
2946
|
+
return db;
|
|
2947
|
+
}
|
|
2948
|
+
function closeAspectGraph(db, rootDir) {
|
|
2949
|
+
if (rootDir) {
|
|
2950
|
+
const dbDir = path6.join(rootDir, ".paradigm");
|
|
2951
|
+
if (!fs5.existsSync(dbDir)) {
|
|
2952
|
+
fs5.mkdirSync(dbDir, { recursive: true });
|
|
2953
|
+
}
|
|
2954
|
+
const dbPath = path6.join(dbDir, "aspect-graph.db");
|
|
2955
|
+
const data = db.export();
|
|
2956
|
+
fs5.writeFileSync(dbPath, Buffer.from(data));
|
|
2957
|
+
}
|
|
2958
|
+
db.close();
|
|
2959
|
+
}
|
|
2960
|
+
function materializeAspects(db, symbols, rootDir) {
|
|
2961
|
+
const aspects = symbols.filter((s) => s.type === "aspect");
|
|
2962
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2963
|
+
let headCommit = null;
|
|
2964
|
+
try {
|
|
2965
|
+
headCommit = execSync("git rev-parse HEAD", { cwd: rootDir, encoding: "utf8" }).trim();
|
|
2966
|
+
} catch {
|
|
2967
|
+
}
|
|
2968
|
+
db.run("DELETE FROM anchors");
|
|
2969
|
+
db.run("DELETE FROM edges");
|
|
2970
|
+
db.run("DELETE FROM aspects");
|
|
2971
|
+
try {
|
|
2972
|
+
db.run("DELETE FROM aspects_fts");
|
|
2973
|
+
} catch {
|
|
2974
|
+
}
|
|
2975
|
+
for (const entry of aspects) {
|
|
2976
|
+
const data = entry.data ?? {};
|
|
2977
|
+
const category = inferCategory(data, entry);
|
|
2978
|
+
const severity = inferSeverity(data, entry);
|
|
2979
|
+
const value = data.value != null ? String(data.value) : null;
|
|
2980
|
+
const enforcement = entry.enforcement ?? (data.enforcement != null ? String(data.enforcement) : null);
|
|
2981
|
+
const tags = entry.tags ? JSON.stringify(entry.tags) : null;
|
|
2982
|
+
db.run(
|
|
2983
|
+
`INSERT INTO aspects (id, description, category, severity, value, enforcement, defined_in, tags, created_at, updated_at)
|
|
2984
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2985
|
+
[
|
|
2986
|
+
entry.symbol,
|
|
2987
|
+
entry.description ?? "",
|
|
2988
|
+
category,
|
|
2989
|
+
severity,
|
|
2990
|
+
value,
|
|
2991
|
+
enforcement,
|
|
2992
|
+
entry.filePath,
|
|
2993
|
+
tags,
|
|
2994
|
+
entry.created ?? now,
|
|
2995
|
+
entry.modified ?? now
|
|
2996
|
+
]
|
|
2997
|
+
);
|
|
2998
|
+
if (entry.anchors) {
|
|
2999
|
+
for (const anchor of entry.anchors) {
|
|
3000
|
+
const { startLine, endLine } = resolveAnchorLines(anchor);
|
|
3001
|
+
const hashes = computeAnchorHash(anchor, null);
|
|
3002
|
+
db.run(
|
|
3003
|
+
`INSERT INTO anchors (aspect_id, file_path, start_line, end_line, content_hash, normalized_hash, materialized_at_commit, last_verified)
|
|
3004
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3005
|
+
[entry.symbol, anchor.path, startLine, endLine, hashes.exact, hashes.normalized, headCommit, now]
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
const explicitEdges = data.edges;
|
|
3010
|
+
if (Array.isArray(explicitEdges)) {
|
|
3011
|
+
for (const edge of explicitEdges) {
|
|
3012
|
+
db.run(
|
|
3013
|
+
`INSERT INTO edges (source, target, relation, weight, origin, created_at)
|
|
3014
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
3015
|
+
[
|
|
3016
|
+
edge.source ?? entry.symbol,
|
|
3017
|
+
edge.target ?? "",
|
|
3018
|
+
edge.relation ?? "related-to",
|
|
3019
|
+
edge.weight ?? 1,
|
|
3020
|
+
edge.origin ?? "explicit",
|
|
3021
|
+
now
|
|
3022
|
+
]
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
if (entry.appliesTo) {
|
|
3027
|
+
for (const target of entry.appliesTo) {
|
|
3028
|
+
db.run(
|
|
3029
|
+
`INSERT INTO edges (source, target, relation, weight, origin, created_at)
|
|
3030
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
3031
|
+
[entry.symbol, target, "related-to", 0.5, "inferred", now]
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
try {
|
|
3036
|
+
db.run(
|
|
3037
|
+
`INSERT INTO aspects_fts (id, description, enforcement, tags)
|
|
3038
|
+
VALUES (?, ?, ?, ?)`,
|
|
3039
|
+
[entry.symbol, entry.description ?? "", enforcement ?? "", tags ?? ""]
|
|
3040
|
+
);
|
|
3041
|
+
} catch {
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function getAspect(db, aspectId) {
|
|
3046
|
+
return queryOne(db, "SELECT * FROM aspects WHERE id = ?", [aspectId]);
|
|
3047
|
+
}
|
|
3048
|
+
function getAnchorsForAspect(db, aspectId) {
|
|
3049
|
+
return queryRows(db, "SELECT * FROM anchors WHERE aspect_id = ?", [aspectId]);
|
|
3050
|
+
}
|
|
3051
|
+
function getEdgesFrom(db, symbol) {
|
|
3052
|
+
return queryRows(db, "SELECT * FROM edges WHERE source = ?", [symbol]);
|
|
3053
|
+
}
|
|
3054
|
+
function getEdgesTo(db, symbol) {
|
|
3055
|
+
return queryRows(db, "SELECT * FROM edges WHERE target = ?", [symbol]);
|
|
3056
|
+
}
|
|
3057
|
+
function getAllEdgesFor(db, symbol) {
|
|
3058
|
+
return queryRows(
|
|
3059
|
+
db,
|
|
3060
|
+
"SELECT * FROM edges WHERE source = ? OR target = ?",
|
|
3061
|
+
[symbol, symbol]
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
function incrementHeatmap(db, aspectId, accessType) {
|
|
3065
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3066
|
+
db.run(
|
|
3067
|
+
`INSERT INTO heatmap (aspect_id, access_type, count, last_accessed)
|
|
3068
|
+
VALUES (?, ?, 1, ?)
|
|
3069
|
+
ON CONFLICT(aspect_id, access_type)
|
|
3070
|
+
DO UPDATE SET count = count + 1, last_accessed = ?`,
|
|
3071
|
+
[aspectId, accessType, now, now]
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
function getHeatmap(db, limit = 20, accessType) {
|
|
3075
|
+
if (accessType) {
|
|
3076
|
+
return queryRows(
|
|
3077
|
+
db,
|
|
3078
|
+
"SELECT * FROM heatmap WHERE access_type = ? ORDER BY count DESC LIMIT ?",
|
|
3079
|
+
[accessType, limit]
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
return queryRows(
|
|
3083
|
+
db,
|
|
3084
|
+
"SELECT * FROM heatmap ORDER BY count DESC LIMIT ?",
|
|
3085
|
+
[limit]
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
function checkDrift(db, rootDir, aspectId, autoHeal = true) {
|
|
3089
|
+
const anchorRows = aspectId ? queryRows(db, "SELECT * FROM anchors WHERE aspect_id = ?", [aspectId]) : queryRows(db, "SELECT * FROM anchors");
|
|
3090
|
+
const results = [];
|
|
3091
|
+
for (const anchor of anchorRows) {
|
|
3092
|
+
const absolutePath = path6.isAbsolute(anchor.file_path) ? anchor.file_path : path6.join(rootDir, anchor.file_path);
|
|
3093
|
+
if (!fs5.existsSync(absolutePath)) {
|
|
3094
|
+
results.push({
|
|
3095
|
+
aspectId: anchor.aspect_id,
|
|
3096
|
+
path: anchor.file_path,
|
|
3097
|
+
startLine: anchor.start_line,
|
|
3098
|
+
endLine: anchor.end_line,
|
|
3099
|
+
status: "missing",
|
|
3100
|
+
resolvedBy: "none",
|
|
3101
|
+
exists: false,
|
|
3102
|
+
drifted: true
|
|
3103
|
+
});
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
try {
|
|
3107
|
+
const fileContent = fs5.readFileSync(absolutePath, "utf8");
|
|
3108
|
+
const lines = fileContent.split("\n");
|
|
3109
|
+
const startIdx = Math.max(0, anchor.start_line - 1);
|
|
3110
|
+
const endIdx = Math.min(lines.length, anchor.end_line);
|
|
3111
|
+
const sliceContent = lines.slice(startIdx, endIdx).join("\n");
|
|
3112
|
+
const currentExactHash = crypto2.createHash("sha256").update(sliceContent).digest("hex");
|
|
3113
|
+
if (anchor.content_hash != null && currentExactHash === anchor.content_hash) {
|
|
3114
|
+
results.push({
|
|
3115
|
+
aspectId: anchor.aspect_id,
|
|
3116
|
+
path: anchor.file_path,
|
|
3117
|
+
startLine: anchor.start_line,
|
|
3118
|
+
endLine: anchor.end_line,
|
|
3119
|
+
status: "clean",
|
|
3120
|
+
resolvedBy: "exact-hash",
|
|
3121
|
+
exists: true,
|
|
3122
|
+
drifted: false
|
|
3123
|
+
});
|
|
3124
|
+
if (anchor.drifted === 1) {
|
|
3125
|
+
db.run("UPDATE anchors SET drifted = 0 WHERE id = ?", [anchor.id]);
|
|
3126
|
+
}
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
const currentNormalizedHash = crypto2.createHash("sha256").update(normalizeForHash(sliceContent)).digest("hex");
|
|
3130
|
+
if (anchor.normalized_hash != null && currentNormalizedHash === anchor.normalized_hash) {
|
|
3131
|
+
db.run("UPDATE anchors SET content_hash = ?, drifted = 0 WHERE id = ?", [currentExactHash, anchor.id]);
|
|
3132
|
+
results.push({
|
|
3133
|
+
aspectId: anchor.aspect_id,
|
|
3134
|
+
path: anchor.file_path,
|
|
3135
|
+
startLine: anchor.start_line,
|
|
3136
|
+
endLine: anchor.end_line,
|
|
3137
|
+
status: "cosmetic",
|
|
3138
|
+
resolvedBy: "normalized-hash",
|
|
3139
|
+
exists: true,
|
|
3140
|
+
drifted: false
|
|
3141
|
+
});
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
if (anchor.content_hash == null && anchor.normalized_hash == null) {
|
|
3145
|
+
db.run(
|
|
3146
|
+
"UPDATE anchors SET content_hash = ?, normalized_hash = ?, drifted = 0 WHERE id = ?",
|
|
3147
|
+
[currentExactHash, currentNormalizedHash, anchor.id]
|
|
3148
|
+
);
|
|
3149
|
+
results.push({
|
|
3150
|
+
aspectId: anchor.aspect_id,
|
|
3151
|
+
path: anchor.file_path,
|
|
3152
|
+
startLine: anchor.start_line,
|
|
3153
|
+
endLine: anchor.end_line,
|
|
3154
|
+
status: "clean",
|
|
3155
|
+
resolvedBy: "exact-hash",
|
|
3156
|
+
exists: true,
|
|
3157
|
+
drifted: false
|
|
3158
|
+
});
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
let resolvedByGit = false;
|
|
3162
|
+
if (anchor.materialized_at_commit) {
|
|
3163
|
+
const mapping = computeLineShift(
|
|
3164
|
+
rootDir,
|
|
3165
|
+
anchor.file_path,
|
|
3166
|
+
anchor.materialized_at_commit,
|
|
3167
|
+
anchor.start_line,
|
|
3168
|
+
anchor.end_line
|
|
3169
|
+
);
|
|
3170
|
+
if (mapping) {
|
|
3171
|
+
const shiftedStartIdx = Math.max(0, mapping.currentStart - 1);
|
|
3172
|
+
const shiftedEndIdx = Math.min(lines.length, mapping.currentEnd);
|
|
3173
|
+
const shiftedContent = lines.slice(shiftedStartIdx, shiftedEndIdx).join("\n");
|
|
3174
|
+
const shiftedExactHash = crypto2.createHash("sha256").update(shiftedContent).digest("hex");
|
|
3175
|
+
if (anchor.content_hash != null && shiftedExactHash === anchor.content_hash) {
|
|
3176
|
+
const healed = autoHeal;
|
|
3177
|
+
if (healed) {
|
|
3178
|
+
db.run(
|
|
3179
|
+
"UPDATE anchors SET start_line = ?, end_line = ?, drifted = 0 WHERE id = ?",
|
|
3180
|
+
[mapping.currentStart, mapping.currentEnd, anchor.id]
|
|
3181
|
+
);
|
|
3182
|
+
const aspectRow = queryRows(
|
|
3183
|
+
db,
|
|
3184
|
+
"SELECT defined_in FROM aspects WHERE id = ?",
|
|
3185
|
+
[anchor.aspect_id]
|
|
3186
|
+
);
|
|
3187
|
+
if (aspectRow.length > 0) {
|
|
3188
|
+
healAnchorInPurposeFile(
|
|
3189
|
+
rootDir,
|
|
3190
|
+
aspectRow[0].defined_in,
|
|
3191
|
+
anchor.file_path,
|
|
3192
|
+
anchor.start_line,
|
|
3193
|
+
anchor.end_line,
|
|
3194
|
+
mapping.currentStart,
|
|
3195
|
+
mapping.currentEnd
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
results.push({
|
|
3200
|
+
aspectId: anchor.aspect_id,
|
|
3201
|
+
path: anchor.file_path,
|
|
3202
|
+
startLine: healed ? mapping.currentStart : anchor.start_line,
|
|
3203
|
+
endLine: healed ? mapping.currentEnd : anchor.end_line,
|
|
3204
|
+
status: "shifted",
|
|
3205
|
+
resolvedBy: "git-line-mapping",
|
|
3206
|
+
exists: true,
|
|
3207
|
+
drifted: false,
|
|
3208
|
+
suggestedStart: mapping.currentStart,
|
|
3209
|
+
suggestedEnd: mapping.currentEnd,
|
|
3210
|
+
autoHealed: healed
|
|
3211
|
+
});
|
|
3212
|
+
resolvedByGit = true;
|
|
3213
|
+
} else {
|
|
3214
|
+
const shiftedNormalized = crypto2.createHash("sha256").update(normalizeForHash(shiftedContent)).digest("hex");
|
|
3215
|
+
if (anchor.normalized_hash != null && shiftedNormalized === anchor.normalized_hash) {
|
|
3216
|
+
if (autoHeal) {
|
|
3217
|
+
const shiftedNewHash = crypto2.createHash("sha256").update(shiftedContent).digest("hex");
|
|
3218
|
+
db.run(
|
|
3219
|
+
"UPDATE anchors SET start_line = ?, end_line = ?, content_hash = ?, drifted = 0 WHERE id = ?",
|
|
3220
|
+
[mapping.currentStart, mapping.currentEnd, shiftedNewHash, anchor.id]
|
|
3221
|
+
);
|
|
3222
|
+
const aspectRow = queryRows(
|
|
3223
|
+
db,
|
|
3224
|
+
"SELECT defined_in FROM aspects WHERE id = ?",
|
|
3225
|
+
[anchor.aspect_id]
|
|
3226
|
+
);
|
|
3227
|
+
if (aspectRow.length > 0) {
|
|
3228
|
+
healAnchorInPurposeFile(
|
|
3229
|
+
rootDir,
|
|
3230
|
+
aspectRow[0].defined_in,
|
|
3231
|
+
anchor.file_path,
|
|
3232
|
+
anchor.start_line,
|
|
3233
|
+
anchor.end_line,
|
|
3234
|
+
mapping.currentStart,
|
|
3235
|
+
mapping.currentEnd
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
results.push({
|
|
3240
|
+
aspectId: anchor.aspect_id,
|
|
3241
|
+
path: anchor.file_path,
|
|
3242
|
+
startLine: autoHeal ? mapping.currentStart : anchor.start_line,
|
|
3243
|
+
endLine: autoHeal ? mapping.currentEnd : anchor.end_line,
|
|
3244
|
+
status: "shifted",
|
|
3245
|
+
resolvedBy: "git-line-mapping",
|
|
3246
|
+
exists: true,
|
|
3247
|
+
drifted: false,
|
|
3248
|
+
suggestedStart: mapping.currentStart,
|
|
3249
|
+
suggestedEnd: mapping.currentEnd,
|
|
3250
|
+
autoHealed: autoHeal
|
|
3251
|
+
});
|
|
3252
|
+
resolvedByGit = true;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (resolvedByGit) continue;
|
|
3258
|
+
db.run("UPDATE anchors SET drifted = 1 WHERE id = ?", [anchor.id]);
|
|
3259
|
+
results.push({
|
|
3260
|
+
aspectId: anchor.aspect_id,
|
|
3261
|
+
path: anchor.file_path,
|
|
3262
|
+
startLine: anchor.start_line,
|
|
3263
|
+
endLine: anchor.end_line,
|
|
3264
|
+
status: "modified",
|
|
3265
|
+
resolvedBy: "none",
|
|
3266
|
+
exists: true,
|
|
3267
|
+
currentContent: sliceContent,
|
|
3268
|
+
drifted: true
|
|
3269
|
+
});
|
|
3270
|
+
} catch {
|
|
3271
|
+
results.push({
|
|
3272
|
+
aspectId: anchor.aspect_id,
|
|
3273
|
+
path: anchor.file_path,
|
|
3274
|
+
startLine: anchor.start_line,
|
|
3275
|
+
endLine: anchor.end_line,
|
|
3276
|
+
status: "modified",
|
|
3277
|
+
resolvedBy: "none",
|
|
3278
|
+
exists: true,
|
|
3279
|
+
drifted: true
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return results;
|
|
3284
|
+
}
|
|
3285
|
+
function resolveAnchorLines(anchor) {
|
|
3286
|
+
const { lines } = anchor;
|
|
3287
|
+
if (typeof lines === "number") {
|
|
3288
|
+
return { startLine: lines, endLine: lines };
|
|
3289
|
+
}
|
|
3290
|
+
if (Array.isArray(lines)) {
|
|
3291
|
+
if (lines.length === 2) {
|
|
3292
|
+
return { startLine: lines[0], endLine: lines[1] };
|
|
3293
|
+
}
|
|
3294
|
+
return {
|
|
3295
|
+
startLine: Math.min(...lines),
|
|
3296
|
+
endLine: Math.max(...lines)
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
return { startLine: 1, endLine: 1 };
|
|
3300
|
+
}
|
|
3301
|
+
function parseUnifiedDiffHunks(diffOutput) {
|
|
3302
|
+
const hunks = [];
|
|
3303
|
+
const hunkPattern = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
|
|
3304
|
+
let match;
|
|
3305
|
+
while ((match = hunkPattern.exec(diffOutput)) !== null) {
|
|
3306
|
+
hunks.push({
|
|
3307
|
+
oldStart: parseInt(match[1], 10),
|
|
3308
|
+
oldCount: match[2] !== void 0 ? parseInt(match[2], 10) : 1,
|
|
3309
|
+
newStart: parseInt(match[3], 10),
|
|
3310
|
+
newCount: match[4] !== void 0 ? parseInt(match[4], 10) : 1
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
return hunks;
|
|
3314
|
+
}
|
|
3315
|
+
function computeLineShift(rootDir, filePath, fromCommit, originalStart, originalEnd) {
|
|
3316
|
+
let diff;
|
|
3317
|
+
try {
|
|
3318
|
+
diff = execSync(
|
|
3319
|
+
`git diff ${fromCommit}..HEAD --unified=0 -- "${filePath}"`,
|
|
3320
|
+
{ cwd: rootDir, encoding: "utf8", timeout: 5e3 }
|
|
3321
|
+
);
|
|
3322
|
+
} catch {
|
|
3323
|
+
return null;
|
|
3324
|
+
}
|
|
3325
|
+
if (!diff.trim()) {
|
|
3326
|
+
return { originalStart, originalEnd, currentStart: originalStart, currentEnd: originalEnd };
|
|
3327
|
+
}
|
|
3328
|
+
const hunks = parseUnifiedDiffHunks(diff);
|
|
3329
|
+
let offset = 0;
|
|
3330
|
+
for (const hunk of hunks) {
|
|
3331
|
+
const hunkOldEnd = hunk.oldStart + hunk.oldCount;
|
|
3332
|
+
if (hunkOldEnd <= originalStart) {
|
|
3333
|
+
offset += hunk.newCount - hunk.oldCount;
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
if (hunk.oldStart < originalEnd) {
|
|
3337
|
+
return null;
|
|
3338
|
+
}
|
|
3339
|
+
break;
|
|
3340
|
+
}
|
|
3341
|
+
if (offset === 0) return null;
|
|
3342
|
+
return {
|
|
3343
|
+
originalStart,
|
|
3344
|
+
originalEnd,
|
|
3345
|
+
currentStart: originalStart + offset,
|
|
3346
|
+
currentEnd: originalEnd + offset
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
function healAnchorInPurposeFile(rootDir, purposeFilePath, anchorFilePath, oldStart, oldEnd, newStart, newEnd) {
|
|
3350
|
+
const absolutePurpose = path6.isAbsolute(purposeFilePath) ? purposeFilePath : path6.join(rootDir, purposeFilePath);
|
|
3351
|
+
if (!fs5.existsSync(absolutePurpose)) return false;
|
|
3352
|
+
try {
|
|
3353
|
+
const content = fs5.readFileSync(absolutePurpose, "utf8");
|
|
3354
|
+
const oldAnchor = oldStart === oldEnd ? `${anchorFilePath}:${oldStart}` : `${anchorFilePath}:${oldStart}-${oldEnd}`;
|
|
3355
|
+
const newAnchor = newStart === newEnd ? `${anchorFilePath}:${newStart}` : `${anchorFilePath}:${newStart}-${newEnd}`;
|
|
3356
|
+
if (!content.includes(oldAnchor)) return false;
|
|
3357
|
+
const updated = content.replace(oldAnchor, newAnchor);
|
|
3358
|
+
fs5.writeFileSync(absolutePurpose, updated, "utf8");
|
|
3359
|
+
return true;
|
|
3360
|
+
} catch {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
function normalizeForHash(content) {
|
|
3365
|
+
return content.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "").map((line) => line.replace(/\s+/g, " ")).join("\n");
|
|
3366
|
+
}
|
|
3367
|
+
function computeAnchorHash(anchor, rootDir) {
|
|
3368
|
+
if (!rootDir) return { exact: null, normalized: null };
|
|
3369
|
+
const absolutePath = path6.isAbsolute(anchor.path) ? anchor.path : path6.join(rootDir, anchor.path);
|
|
3370
|
+
if (!fs5.existsSync(absolutePath)) return { exact: null, normalized: null };
|
|
3371
|
+
try {
|
|
3372
|
+
const fileContent = fs5.readFileSync(absolutePath, "utf8");
|
|
3373
|
+
const lines = fileContent.split("\n");
|
|
3374
|
+
const { startLine, endLine } = resolveAnchorLines(anchor);
|
|
3375
|
+
const startIdx = Math.max(0, startLine - 1);
|
|
3376
|
+
const endIdx = Math.min(lines.length, endLine);
|
|
3377
|
+
const sliceContent = lines.slice(startIdx, endIdx).join("\n");
|
|
3378
|
+
const exact = crypto2.createHash("sha256").update(sliceContent).digest("hex");
|
|
3379
|
+
const normalized = crypto2.createHash("sha256").update(normalizeForHash(sliceContent)).digest("hex");
|
|
3380
|
+
return { exact, normalized };
|
|
3381
|
+
} catch {
|
|
3382
|
+
return { exact: null, normalized: null };
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function inferCategory(data, entry) {
|
|
3386
|
+
if (typeof data.category === "string") return data.category;
|
|
3387
|
+
const desc = (entry.description ?? "").toLowerCase();
|
|
3388
|
+
if (/\b(must|require|always)\b/.test(desc)) return "rule";
|
|
3389
|
+
if (/\b(decided|chose)\b/.test(desc)) return "decision";
|
|
3390
|
+
if (/\b(limit|cannot)\b/.test(desc)) return "constraint";
|
|
3391
|
+
if (/\b(set to|configured|value)\b/.test(desc)) return "configuration";
|
|
3392
|
+
return "rule";
|
|
3393
|
+
}
|
|
3394
|
+
function inferSeverity(data, entry) {
|
|
3395
|
+
if (typeof data.severity === "string") return data.severity;
|
|
3396
|
+
const tags = entry.tags ?? [];
|
|
3397
|
+
if (tags.includes("critical")) return "critical";
|
|
3398
|
+
if (tags.includes("security") || tags.includes("compliance")) return "high";
|
|
3399
|
+
return "medium";
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// ../paradigm-mcp/src/utils/lore-loader.ts
|
|
3403
|
+
import * as fs6 from "fs";
|
|
3404
|
+
import * as path7 from "path";
|
|
3405
|
+
import * as yaml5 from "js-yaml";
|
|
3406
|
+
var LORE_DIR = ".paradigm/lore";
|
|
3407
|
+
var ENTRIES_DIR = "entries";
|
|
3408
|
+
var TIMELINE_FILE = "timeline.yaml";
|
|
3409
|
+
async function loadLoreEntries(rootDir, filter) {
|
|
3410
|
+
const entriesPath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR);
|
|
3411
|
+
if (!fs6.existsSync(entriesPath)) {
|
|
3412
|
+
return [];
|
|
3413
|
+
}
|
|
3414
|
+
migrateLegacyEntries(rootDir);
|
|
3415
|
+
const entries = [];
|
|
3416
|
+
const dateDirs = fs6.readdirSync(entriesPath).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse();
|
|
3417
|
+
for (const dateDir of dateDirs) {
|
|
3418
|
+
if (filter?.dateFrom && dateDir < filter.dateFrom.slice(0, 10)) continue;
|
|
3419
|
+
if (filter?.dateTo && dateDir > filter.dateTo.slice(0, 10)) continue;
|
|
3420
|
+
const dirPath = path7.join(entriesPath, dateDir);
|
|
3421
|
+
const files = fs6.readdirSync(dirPath).filter((f) => f.endsWith(".yaml")).sort();
|
|
3422
|
+
for (const file of files) {
|
|
3423
|
+
try {
|
|
3424
|
+
const content = fs6.readFileSync(path7.join(dirPath, file), "utf8");
|
|
3425
|
+
const entry = yaml5.load(content);
|
|
3426
|
+
entries.push(entry);
|
|
3427
|
+
} catch {
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return filter ? applyFilter(entries, filter) : entries;
|
|
3432
|
+
}
|
|
3433
|
+
async function loadLoreEntry(rootDir, entryId) {
|
|
3434
|
+
const dateMatch = entryId.match(/^L-(\d{4}-\d{2}-\d{2})-/);
|
|
3435
|
+
if (dateMatch) {
|
|
3436
|
+
const dateStr = dateMatch[1];
|
|
3437
|
+
const entryPath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr, `${entryId}.yaml`);
|
|
3438
|
+
if (fs6.existsSync(entryPath)) {
|
|
3439
|
+
try {
|
|
3440
|
+
const content = fs6.readFileSync(entryPath, "utf8");
|
|
3441
|
+
return yaml5.load(content);
|
|
3442
|
+
} catch {
|
|
3443
|
+
return null;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
const entries = await loadLoreEntries(rootDir);
|
|
3448
|
+
return entries.find((e) => e.id === entryId) || null;
|
|
3449
|
+
}
|
|
3450
|
+
async function loadLoreTimeline(rootDir) {
|
|
3451
|
+
const timelinePath = path7.join(rootDir, LORE_DIR, TIMELINE_FILE);
|
|
3452
|
+
if (!fs6.existsSync(timelinePath)) {
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
try {
|
|
3456
|
+
const content = fs6.readFileSync(timelinePath, "utf8");
|
|
3457
|
+
return yaml5.load(content);
|
|
3458
|
+
} catch {
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
async function recordLoreEntry(rootDir, entry) {
|
|
3463
|
+
const lorePath = path7.join(rootDir, LORE_DIR);
|
|
3464
|
+
const dateStr = entry.timestamp.slice(0, 10);
|
|
3465
|
+
const datePath = path7.join(lorePath, ENTRIES_DIR, dateStr);
|
|
3466
|
+
if (!fs6.existsSync(datePath)) {
|
|
3467
|
+
fs6.mkdirSync(datePath, { recursive: true });
|
|
3468
|
+
}
|
|
3469
|
+
if (!entry.id) {
|
|
3470
|
+
entry.id = generateLoreId(rootDir, dateStr);
|
|
3471
|
+
}
|
|
3472
|
+
const entryPath = path7.join(datePath, `${entry.id}.yaml`);
|
|
3473
|
+
fs6.writeFileSync(entryPath, yaml5.dump(entry, { lineWidth: -1, noRefs: true }));
|
|
3474
|
+
await rebuildTimeline(rootDir);
|
|
3475
|
+
return entry.id;
|
|
3476
|
+
}
|
|
3477
|
+
async function rebuildTimeline(rootDir) {
|
|
3478
|
+
const lorePath = path7.join(rootDir, LORE_DIR);
|
|
3479
|
+
const entriesPath = path7.join(lorePath, ENTRIES_DIR);
|
|
3480
|
+
if (!fs6.existsSync(entriesPath)) return;
|
|
3481
|
+
migrateLegacyEntries(rootDir);
|
|
3482
|
+
const authors = /* @__PURE__ */ new Set();
|
|
3483
|
+
let entryCount = 0;
|
|
3484
|
+
let lastUpdated = "";
|
|
3485
|
+
const dateDirs = fs6.readdirSync(entriesPath).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d));
|
|
3486
|
+
for (const dateDir of dateDirs) {
|
|
3487
|
+
const dirPath = path7.join(entriesPath, dateDir);
|
|
3488
|
+
const files = fs6.readdirSync(dirPath).filter((f) => f.endsWith(".yaml"));
|
|
3489
|
+
for (const file of files) {
|
|
3490
|
+
try {
|
|
3491
|
+
const content = fs6.readFileSync(path7.join(dirPath, file), "utf8");
|
|
3492
|
+
const entry = yaml5.load(content);
|
|
3493
|
+
authors.add(entry.author.id);
|
|
3494
|
+
entryCount++;
|
|
3495
|
+
if (!lastUpdated || entry.timestamp > lastUpdated) {
|
|
3496
|
+
lastUpdated = entry.timestamp;
|
|
3497
|
+
}
|
|
3498
|
+
} catch {
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
let project = "unknown";
|
|
3503
|
+
const configPath = path7.join(rootDir, ".paradigm", "config.yaml");
|
|
3504
|
+
if (fs6.existsSync(configPath)) {
|
|
3505
|
+
try {
|
|
3506
|
+
const config = yaml5.load(fs6.readFileSync(configPath, "utf8"));
|
|
3507
|
+
project = config.project || config.name || "unknown";
|
|
3508
|
+
} catch {
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
const timeline = {
|
|
3512
|
+
version: "1.0",
|
|
3513
|
+
project,
|
|
3514
|
+
entries: entryCount,
|
|
3515
|
+
last_updated: lastUpdated || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3516
|
+
authors: Array.from(authors)
|
|
3517
|
+
};
|
|
3518
|
+
if (!fs6.existsSync(lorePath)) {
|
|
3519
|
+
fs6.mkdirSync(lorePath, { recursive: true });
|
|
3520
|
+
}
|
|
3521
|
+
fs6.writeFileSync(
|
|
3522
|
+
path7.join(lorePath, TIMELINE_FILE),
|
|
3523
|
+
yaml5.dump(timeline, { lineWidth: -1, noRefs: true })
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
function migrateLegacyEntries(rootDir) {
|
|
3527
|
+
const entriesPath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR);
|
|
3528
|
+
if (!fs6.existsSync(entriesPath)) return 0;
|
|
3529
|
+
const rootFiles = fs6.readdirSync(entriesPath).filter((f) => f.endsWith(".yaml") && !f.startsWith("."));
|
|
3530
|
+
let migrated = 0;
|
|
3531
|
+
for (const file of rootFiles) {
|
|
3532
|
+
const filePath = path7.join(entriesPath, file);
|
|
3533
|
+
const stat = fs6.statSync(filePath);
|
|
3534
|
+
if (!stat.isFile()) continue;
|
|
3535
|
+
try {
|
|
3536
|
+
const content = fs6.readFileSync(filePath, "utf8");
|
|
3537
|
+
const raw = yaml5.load(content);
|
|
3538
|
+
if (raw.author && typeof raw.author === "object") continue;
|
|
3539
|
+
const dateStr = typeof raw.date === "string" ? raw.date.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3540
|
+
const datePath = path7.join(entriesPath, dateStr);
|
|
3541
|
+
if (!fs6.existsSync(datePath)) {
|
|
3542
|
+
fs6.mkdirSync(datePath, { recursive: true });
|
|
3543
|
+
}
|
|
3544
|
+
const id = generateLoreId(rootDir, dateStr);
|
|
3545
|
+
const oldType = String(raw.type || "agent-session");
|
|
3546
|
+
const v2Type = ["agent-session", "human-note", "decision", "review", "incident", "milestone"].includes(oldType) ? oldType : "agent-session";
|
|
3547
|
+
let verification;
|
|
3548
|
+
if (raw.test_results && typeof raw.test_results === "object") {
|
|
3549
|
+
const tr = raw.test_results;
|
|
3550
|
+
verification = {
|
|
3551
|
+
status: tr.total === tr.passed ? "pass" : "partial",
|
|
3552
|
+
details: { tests: tr.total === tr.passed ? "pass" : "fail" }
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
const v2Entry = {
|
|
3556
|
+
id,
|
|
3557
|
+
type: v2Type,
|
|
3558
|
+
timestamp: `${dateStr}T00:00:00.000Z`,
|
|
3559
|
+
author: { type: "agent", id: "unknown" },
|
|
3560
|
+
title: String(raw.title || file.replace(".yaml", "")),
|
|
3561
|
+
summary: String(raw.summary || ""),
|
|
3562
|
+
symbols_touched: Array.isArray(raw.symbols_touched) ? raw.symbols_touched : [],
|
|
3563
|
+
files_modified: Array.isArray(raw.files_modified) ? raw.files_modified : void 0,
|
|
3564
|
+
...verification ? { verification } : {},
|
|
3565
|
+
tags: ["migrated", oldType]
|
|
3566
|
+
};
|
|
3567
|
+
fs6.writeFileSync(
|
|
3568
|
+
path7.join(datePath, `${id}.yaml`),
|
|
3569
|
+
yaml5.dump(v2Entry, { lineWidth: -1, noRefs: true })
|
|
3570
|
+
);
|
|
3571
|
+
fs6.unlinkSync(filePath);
|
|
3572
|
+
migrated++;
|
|
3573
|
+
} catch {
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
return migrated;
|
|
3577
|
+
}
|
|
3578
|
+
async function updateLoreEntry(rootDir, entryId, partial) {
|
|
3579
|
+
const entry = await loadLoreEntry(rootDir, entryId);
|
|
3580
|
+
if (!entry) return false;
|
|
3581
|
+
const dateStr = entry.timestamp.slice(0, 10);
|
|
3582
|
+
const entryPath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr, `${entryId}.yaml`);
|
|
3583
|
+
if (!fs6.existsSync(entryPath)) return false;
|
|
3584
|
+
if (partial.title !== void 0) entry.title = partial.title;
|
|
3585
|
+
if (partial.summary !== void 0) entry.summary = partial.summary;
|
|
3586
|
+
if (partial.type !== void 0) entry.type = partial.type;
|
|
3587
|
+
if (partial.duration_minutes !== void 0) entry.duration_minutes = partial.duration_minutes;
|
|
3588
|
+
if (partial.symbols_touched !== void 0) entry.symbols_touched = partial.symbols_touched;
|
|
3589
|
+
if (partial.symbols_created !== void 0) entry.symbols_created = partial.symbols_created;
|
|
3590
|
+
if (partial.files_created !== void 0) entry.files_created = partial.files_created;
|
|
3591
|
+
if (partial.files_modified !== void 0) entry.files_modified = partial.files_modified;
|
|
3592
|
+
if (partial.lines_added !== void 0) entry.lines_added = partial.lines_added;
|
|
3593
|
+
if (partial.lines_removed !== void 0) entry.lines_removed = partial.lines_removed;
|
|
3594
|
+
if (partial.commit !== void 0) entry.commit = partial.commit;
|
|
3595
|
+
if (partial.decisions !== void 0) entry.decisions = partial.decisions;
|
|
3596
|
+
if (partial.errors_encountered !== void 0) entry.errors_encountered = partial.errors_encountered;
|
|
3597
|
+
if (partial.learnings !== void 0) entry.learnings = partial.learnings;
|
|
3598
|
+
if (partial.verification !== void 0) entry.verification = partial.verification;
|
|
3599
|
+
if (partial.tags !== void 0) entry.tags = partial.tags;
|
|
3600
|
+
fs6.writeFileSync(entryPath, yaml5.dump(entry, { lineWidth: -1, noRefs: true }));
|
|
3601
|
+
await rebuildTimeline(rootDir);
|
|
3602
|
+
return true;
|
|
3603
|
+
}
|
|
3604
|
+
async function deleteLoreEntry(rootDir, entryId) {
|
|
3605
|
+
const entry = await loadLoreEntry(rootDir, entryId);
|
|
3606
|
+
if (!entry) return false;
|
|
3607
|
+
const dateStr = entry.timestamp.slice(0, 10);
|
|
3608
|
+
const entryPath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr, `${entryId}.yaml`);
|
|
3609
|
+
if (!fs6.existsSync(entryPath)) return false;
|
|
3610
|
+
fs6.unlinkSync(entryPath);
|
|
3611
|
+
const dateDir = path7.dirname(entryPath);
|
|
3612
|
+
const remaining = fs6.readdirSync(dateDir).filter((f) => f.endsWith(".yaml"));
|
|
3613
|
+
if (remaining.length === 0) {
|
|
3614
|
+
fs6.rmdirSync(dateDir);
|
|
3615
|
+
}
|
|
3616
|
+
await rebuildTimeline(rootDir);
|
|
3617
|
+
return true;
|
|
3618
|
+
}
|
|
3619
|
+
function applyFilter(entries, filter) {
|
|
3620
|
+
let result = entries;
|
|
3621
|
+
if (filter.author) {
|
|
3622
|
+
result = result.filter((e) => e.author.id === filter.author);
|
|
3623
|
+
}
|
|
3624
|
+
if (filter.authorType) {
|
|
3625
|
+
result = result.filter((e) => e.author.type === filter.authorType);
|
|
3626
|
+
}
|
|
3627
|
+
if (filter.symbol) {
|
|
3628
|
+
result = result.filter(
|
|
3629
|
+
(e) => e.symbols_touched.includes(filter.symbol) || e.symbols_created?.includes(filter.symbol)
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
if (filter.dateFrom) {
|
|
3633
|
+
const from = new Date(filter.dateFrom).getTime();
|
|
3634
|
+
result = result.filter((e) => new Date(e.timestamp).getTime() >= from);
|
|
3635
|
+
}
|
|
3636
|
+
if (filter.dateTo) {
|
|
3637
|
+
const to = new Date(filter.dateTo).getTime();
|
|
3638
|
+
result = result.filter((e) => new Date(e.timestamp).getTime() <= to);
|
|
3639
|
+
}
|
|
3640
|
+
if (filter.type) {
|
|
3641
|
+
result = result.filter((e) => e.type === filter.type);
|
|
3642
|
+
}
|
|
3643
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
3644
|
+
result = result.filter((e) => filter.tags.some((tag) => e.tags?.includes(tag)));
|
|
3645
|
+
}
|
|
3646
|
+
if (filter.hasReview !== void 0) {
|
|
3647
|
+
result = result.filter((e) => filter.hasReview ? e.review != null : e.review == null);
|
|
3648
|
+
}
|
|
3649
|
+
result.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
3650
|
+
if (filter.offset) result = result.slice(filter.offset);
|
|
3651
|
+
if (filter.limit) result = result.slice(0, filter.limit);
|
|
3652
|
+
return result;
|
|
3653
|
+
}
|
|
3654
|
+
function generateLoreId(rootDir, dateStr) {
|
|
3655
|
+
const datePath = path7.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr);
|
|
3656
|
+
if (!fs6.existsSync(datePath)) {
|
|
3657
|
+
return `L-${dateStr}-001`;
|
|
3658
|
+
}
|
|
3659
|
+
const existing = fs6.readdirSync(datePath).filter((f) => f.startsWith("L-") && f.endsWith(".yaml")).map((f) => {
|
|
3660
|
+
const match = f.match(/L-\d{4}-\d{2}-\d{2}-(\d+)\.yaml/);
|
|
3661
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
3662
|
+
});
|
|
3663
|
+
const next = existing.length > 0 ? Math.max(...existing) + 1 : 1;
|
|
3664
|
+
return `L-${dateStr}-${String(next).padStart(3, "0")}`;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// ../paradigm-mcp/src/utils/aspect-lore-bridge.ts
|
|
3668
|
+
var LORE_ID_PATTERN = /L-\d{4}-\d{2}-\d{2}-\d{3}/g;
|
|
3669
|
+
async function materializeLoreLinks(db, rootDir) {
|
|
3670
|
+
db.run("DELETE FROM lore_links");
|
|
3671
|
+
const loreEntries = await loadLoreEntries(rootDir);
|
|
3672
|
+
if (loreEntries.length === 0) {
|
|
3673
|
+
return 0;
|
|
3674
|
+
}
|
|
3675
|
+
const symbolToLore = buildSymbolToLoreMap(loreEntries);
|
|
3676
|
+
const loreById = /* @__PURE__ */ new Map();
|
|
3677
|
+
for (const entry of loreEntries) {
|
|
3678
|
+
loreById.set(entry.id, entry);
|
|
3679
|
+
}
|
|
3680
|
+
const links = /* @__PURE__ */ new Set();
|
|
3681
|
+
const aspectRows = db.exec("SELECT id, enforcement FROM aspects");
|
|
3682
|
+
if (aspectRows.length > 0) {
|
|
3683
|
+
const { columns, values } = aspectRows[0];
|
|
3684
|
+
const idIdx = columns.indexOf("id");
|
|
3685
|
+
const enfIdx = columns.indexOf("enforcement");
|
|
3686
|
+
for (const row of values) {
|
|
3687
|
+
const aspectId = String(row[idIdx]);
|
|
3688
|
+
const enforcement = row[enfIdx];
|
|
3689
|
+
if (enforcement && typeof enforcement === "string") {
|
|
3690
|
+
const matches = enforcement.match(LORE_ID_PATTERN);
|
|
3691
|
+
if (matches) {
|
|
3692
|
+
for (const loreId of matches) {
|
|
3693
|
+
if (loreById.has(loreId)) {
|
|
3694
|
+
links.add(`${aspectId}\0${loreId}`);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
const edgeRows = db.exec("SELECT source, target FROM edges");
|
|
3702
|
+
if (edgeRows.length > 0 && aspectRows.length > 0) {
|
|
3703
|
+
const aspectSymbols = buildAspectSymbolMap(db, edgeRows);
|
|
3704
|
+
for (const [aspectId, symbols] of aspectSymbols) {
|
|
3705
|
+
for (const symbol of symbols) {
|
|
3706
|
+
const loreIds = symbolToLore.get(symbol);
|
|
3707
|
+
if (loreIds) {
|
|
3708
|
+
for (const loreId of loreIds) {
|
|
3709
|
+
links.add(`${aspectId}\0${loreId}`);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
if (links.size === 0) {
|
|
3716
|
+
return 0;
|
|
3717
|
+
}
|
|
3718
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO lore_links (aspect_id, lore_id) VALUES (?, ?)");
|
|
3719
|
+
try {
|
|
3720
|
+
for (const key of links) {
|
|
3721
|
+
const [aspectId, loreId] = key.split("\0");
|
|
3722
|
+
stmt.bind([aspectId, loreId]);
|
|
3723
|
+
stmt.step();
|
|
3724
|
+
stmt.reset();
|
|
3725
|
+
}
|
|
3726
|
+
} finally {
|
|
3727
|
+
stmt.free();
|
|
3728
|
+
}
|
|
3729
|
+
return links.size;
|
|
3730
|
+
}
|
|
3731
|
+
async function getLoreForAspect(db, rootDir, aspectId) {
|
|
3732
|
+
const result = db.exec("SELECT lore_id FROM lore_links WHERE aspect_id = ?", [aspectId]);
|
|
3733
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
3734
|
+
return [];
|
|
3735
|
+
}
|
|
3736
|
+
const loreIds = result[0].values.map((row) => String(row[0]));
|
|
3737
|
+
return loadLoreSummaries(rootDir, loreIds);
|
|
3738
|
+
}
|
|
3739
|
+
async function inferLoreEdges(db, rootDir) {
|
|
3740
|
+
const loreEntries = await loadLoreEntries(rootDir);
|
|
3741
|
+
if (loreEntries.length === 0) {
|
|
3742
|
+
return 0;
|
|
3743
|
+
}
|
|
3744
|
+
const aspectIdsResult = db.exec("SELECT id FROM aspects");
|
|
3745
|
+
const knownAspects = /* @__PURE__ */ new Set();
|
|
3746
|
+
if (aspectIdsResult.length > 0) {
|
|
3747
|
+
for (const row of aspectIdsResult[0].values) {
|
|
3748
|
+
knownAspects.add(String(row[0]));
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
if (knownAspects.size < 2) {
|
|
3752
|
+
return 0;
|
|
3753
|
+
}
|
|
3754
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3755
|
+
let edgesInferred = 0;
|
|
3756
|
+
const stmt = db.prepare(
|
|
3757
|
+
`INSERT OR IGNORE INTO edges (source, target, relation, weight, origin, created_at)
|
|
3758
|
+
VALUES (?, ?, 'related-to', 0.3, 'learned', ?)`
|
|
3759
|
+
);
|
|
3760
|
+
try {
|
|
3761
|
+
for (const entry of loreEntries) {
|
|
3762
|
+
if (!entry.symbols_touched || entry.symbols_touched.length < 2) {
|
|
3763
|
+
continue;
|
|
3764
|
+
}
|
|
3765
|
+
const aspectsInEntry = [];
|
|
3766
|
+
for (const symbol of entry.symbols_touched) {
|
|
3767
|
+
const stripped = symbol.startsWith("~") ? symbol.slice(1) : symbol;
|
|
3768
|
+
if (knownAspects.has(stripped)) {
|
|
3769
|
+
aspectsInEntry.push(stripped);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
for (let i = 0; i < aspectsInEntry.length; i++) {
|
|
3773
|
+
for (let j = i + 1; j < aspectsInEntry.length; j++) {
|
|
3774
|
+
const [source, target] = aspectsInEntry[i] < aspectsInEntry[j] ? [aspectsInEntry[i], aspectsInEntry[j]] : [aspectsInEntry[j], aspectsInEntry[i]];
|
|
3775
|
+
stmt.bind([source, target, now]);
|
|
3776
|
+
stmt.step();
|
|
3777
|
+
stmt.reset();
|
|
3778
|
+
edgesInferred++;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} finally {
|
|
3783
|
+
stmt.free();
|
|
3784
|
+
}
|
|
3785
|
+
return edgesInferred;
|
|
3786
|
+
}
|
|
3787
|
+
function buildSymbolToLoreMap(entries) {
|
|
3788
|
+
const map = /* @__PURE__ */ new Map();
|
|
3789
|
+
for (const entry of entries) {
|
|
3790
|
+
if (!entry.symbols_touched) continue;
|
|
3791
|
+
for (const symbol of entry.symbols_touched) {
|
|
3792
|
+
let set = map.get(symbol);
|
|
3793
|
+
if (!set) {
|
|
3794
|
+
set = /* @__PURE__ */ new Set();
|
|
3795
|
+
map.set(symbol, set);
|
|
3796
|
+
}
|
|
3797
|
+
set.add(entry.id);
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return map;
|
|
3801
|
+
}
|
|
3802
|
+
function buildAspectSymbolMap(db, edgeRows) {
|
|
3803
|
+
const aspectIdsResult = db.exec("SELECT id FROM aspects");
|
|
3804
|
+
const knownAspects = /* @__PURE__ */ new Set();
|
|
3805
|
+
if (aspectIdsResult.length > 0) {
|
|
3806
|
+
for (const row of aspectIdsResult[0].values) {
|
|
3807
|
+
knownAspects.add(String(row[0]));
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
const map = /* @__PURE__ */ new Map();
|
|
3811
|
+
if (edgeRows.length === 0) return map;
|
|
3812
|
+
const { columns, values } = edgeRows[0];
|
|
3813
|
+
const srcIdx = columns.indexOf("source");
|
|
3814
|
+
const tgtIdx = columns.indexOf("target");
|
|
3815
|
+
for (const row of values) {
|
|
3816
|
+
const source = String(row[srcIdx]);
|
|
3817
|
+
const target = String(row[tgtIdx]);
|
|
3818
|
+
if (knownAspects.has(source)) {
|
|
3819
|
+
let set = map.get(source);
|
|
3820
|
+
if (!set) {
|
|
3821
|
+
set = /* @__PURE__ */ new Set();
|
|
3822
|
+
map.set(source, set);
|
|
3823
|
+
}
|
|
3824
|
+
set.add(target);
|
|
3825
|
+
set.add(`~${source}`);
|
|
3826
|
+
}
|
|
3827
|
+
if (knownAspects.has(target)) {
|
|
3828
|
+
let set = map.get(target);
|
|
3829
|
+
if (!set) {
|
|
3830
|
+
set = /* @__PURE__ */ new Set();
|
|
3831
|
+
map.set(target, set);
|
|
3832
|
+
}
|
|
3833
|
+
set.add(source);
|
|
3834
|
+
set.add(`~${target}`);
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
return map;
|
|
3838
|
+
}
|
|
3839
|
+
async function loadLoreSummaries(rootDir, loreIds) {
|
|
3840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3841
|
+
const summaries = [];
|
|
3842
|
+
for (const loreId of loreIds) {
|
|
3843
|
+
if (seen.has(loreId)) continue;
|
|
3844
|
+
seen.add(loreId);
|
|
3845
|
+
const entry = await loadLoreEntry(rootDir, loreId);
|
|
3846
|
+
if (entry) {
|
|
3847
|
+
summaries.push(toLoreSummary(entry));
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
return summaries;
|
|
3851
|
+
}
|
|
3852
|
+
function toLoreSummary(entry) {
|
|
3853
|
+
return {
|
|
3854
|
+
id: entry.id,
|
|
3855
|
+
title: entry.title,
|
|
3856
|
+
summary: entry.summary,
|
|
3857
|
+
timestamp: entry.timestamp,
|
|
3858
|
+
symbolsTouched: entry.symbols_touched
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// ../paradigm-mcp/src/utils/personas-loader.ts
|
|
3863
|
+
import * as fs7 from "fs";
|
|
3864
|
+
import * as path8 from "path";
|
|
3865
|
+
import * as yaml6 from "js-yaml";
|
|
3866
|
+
var PERSONAS_ROOT = ".paradigm/personas";
|
|
3867
|
+
var INDEX_FILE = "index.yaml";
|
|
3868
|
+
async function loadPersonas(rootDir, filter) {
|
|
3869
|
+
const personasDir = path8.join(rootDir, PERSONAS_ROOT);
|
|
3870
|
+
if (!fs7.existsSync(personasDir)) return [];
|
|
3871
|
+
const files = fs7.readdirSync(personasDir).filter((f) => f.endsWith(".persona"));
|
|
3872
|
+
const personas = [];
|
|
3873
|
+
for (const file of files) {
|
|
3874
|
+
try {
|
|
3875
|
+
const content = fs7.readFileSync(path8.join(personasDir, file), "utf8");
|
|
3876
|
+
const persona = yaml6.load(content);
|
|
3877
|
+
if (persona && persona.id) {
|
|
3878
|
+
personas.push(persona);
|
|
3879
|
+
}
|
|
3880
|
+
} catch {
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
return applyFilter2(personas, filter);
|
|
3884
|
+
}
|
|
3885
|
+
async function loadPersona(rootDir, id) {
|
|
3886
|
+
const filePath = path8.join(rootDir, PERSONAS_ROOT, `${id}.persona`);
|
|
3887
|
+
if (!fs7.existsSync(filePath)) return null;
|
|
3888
|
+
try {
|
|
3889
|
+
return yaml6.load(fs7.readFileSync(filePath, "utf8"));
|
|
3890
|
+
} catch {
|
|
3891
|
+
return null;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function applyFilter2(personas, filter) {
|
|
3895
|
+
if (!filter) return personas;
|
|
3896
|
+
let result = personas;
|
|
3897
|
+
if (filter.tag) {
|
|
3898
|
+
result = result.filter((p) => p.tags?.includes(filter.tag));
|
|
3899
|
+
}
|
|
3900
|
+
if (filter.trigger_type) {
|
|
3901
|
+
result = result.filter((p) => p.trigger.type === filter.trigger_type);
|
|
3902
|
+
}
|
|
3903
|
+
if (filter.gate) {
|
|
3904
|
+
result = result.filter(
|
|
3905
|
+
(p) => p.journey.some((s) => s.gates.includes(filter.gate))
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
if (filter.flow) {
|
|
3909
|
+
result = result.filter(
|
|
3910
|
+
(p) => p.journey.some((s) => s.flow === filter.flow)
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
if (filter.limit) {
|
|
3914
|
+
result = result.slice(0, filter.limit);
|
|
3915
|
+
}
|
|
3916
|
+
return result;
|
|
3917
|
+
}
|
|
3918
|
+
async function createPersona(rootDir, data) {
|
|
3919
|
+
const personasDir = path8.join(rootDir, PERSONAS_ROOT);
|
|
3920
|
+
fs7.mkdirSync(personasDir, { recursive: true });
|
|
3921
|
+
const filePath = path8.join(personasDir, `${data.id}.persona`);
|
|
3922
|
+
if (fs7.existsSync(filePath)) {
|
|
3923
|
+
throw new Error(`Persona ${data.id} already exists`);
|
|
3924
|
+
}
|
|
3925
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3926
|
+
const persona = {
|
|
3927
|
+
version: "1.0",
|
|
3928
|
+
id: data.id,
|
|
3929
|
+
name: data.name,
|
|
3930
|
+
description: data.description,
|
|
3931
|
+
traits: data.traits,
|
|
3932
|
+
trigger: data.trigger,
|
|
3933
|
+
fixtures: data.fixtures,
|
|
3934
|
+
tags: data.tags || [],
|
|
3935
|
+
journey: data.journey || [],
|
|
3936
|
+
created: now,
|
|
3937
|
+
updated: now
|
|
3938
|
+
};
|
|
3939
|
+
fs7.writeFileSync(filePath, yaml6.dump(persona, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }));
|
|
3940
|
+
await rebuildPersonaIndex(rootDir);
|
|
3941
|
+
return data.id;
|
|
3942
|
+
}
|
|
3943
|
+
async function updatePersona(rootDir, id, partial) {
|
|
3944
|
+
const persona = await loadPersona(rootDir, id);
|
|
3945
|
+
if (!persona) return false;
|
|
3946
|
+
const filePath = path8.join(rootDir, PERSONAS_ROOT, `${id}.persona`);
|
|
3947
|
+
const updated = {
|
|
3948
|
+
...persona,
|
|
3949
|
+
...partial,
|
|
3950
|
+
id: persona.id,
|
|
3951
|
+
// immutable
|
|
3952
|
+
version: persona.version,
|
|
3953
|
+
// immutable
|
|
3954
|
+
created: persona.created,
|
|
3955
|
+
// immutable
|
|
3956
|
+
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
3957
|
+
};
|
|
3958
|
+
fs7.writeFileSync(filePath, yaml6.dump(updated, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }));
|
|
3959
|
+
await rebuildPersonaIndex(rootDir);
|
|
3960
|
+
return true;
|
|
3961
|
+
}
|
|
3962
|
+
async function deletePersona(rootDir, id) {
|
|
3963
|
+
const filePath = path8.join(rootDir, PERSONAS_ROOT, `${id}.persona`);
|
|
3964
|
+
if (!fs7.existsSync(filePath)) {
|
|
3965
|
+
return { deleted: false, warnings: [] };
|
|
3966
|
+
}
|
|
3967
|
+
const warnings = [];
|
|
3968
|
+
const all = await loadPersonas(rootDir);
|
|
3969
|
+
for (const p of all) {
|
|
3970
|
+
if (p.id === id) continue;
|
|
3971
|
+
if (p.trigger.spawned_by?.startsWith(id + ".")) {
|
|
3972
|
+
warnings.push(`Persona ${p.id} is spawned by ${id} \u2014 it will become orphaned`);
|
|
3973
|
+
}
|
|
3974
|
+
for (const step of p.journey) {
|
|
3975
|
+
if (step.spawns?.some((s) => s.persona === id)) {
|
|
3976
|
+
warnings.push(`Persona ${p.id} step ${step.id} spawns ${id} \u2014 spawn will break`);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
fs7.unlinkSync(filePath);
|
|
3981
|
+
await rebuildPersonaIndex(rootDir);
|
|
3982
|
+
return { deleted: true, warnings };
|
|
3983
|
+
}
|
|
3984
|
+
async function addStep(rootDir, personaId, step, afterStepId) {
|
|
3985
|
+
const persona = await loadPersona(rootDir, personaId);
|
|
3986
|
+
if (!persona) return false;
|
|
3987
|
+
if (persona.journey.some((s) => s.id === step.id)) {
|
|
3988
|
+
throw new Error(`Step ${step.id} already exists in persona ${personaId}`);
|
|
3989
|
+
}
|
|
3990
|
+
if (afterStepId) {
|
|
3991
|
+
const idx = persona.journey.findIndex((s) => s.id === afterStepId);
|
|
3992
|
+
if (idx === -1) throw new Error(`Step ${afterStepId} not found in persona ${personaId}`);
|
|
3993
|
+
persona.journey.splice(idx + 1, 0, step);
|
|
3994
|
+
} else {
|
|
3995
|
+
persona.journey.push(step);
|
|
3996
|
+
}
|
|
3997
|
+
return updatePersona(rootDir, personaId, { journey: persona.journey });
|
|
3998
|
+
}
|
|
3999
|
+
async function removeStep(rootDir, personaId, stepId) {
|
|
4000
|
+
const persona = await loadPersona(rootDir, personaId);
|
|
4001
|
+
if (!persona) return { removed: false, warnings: [] };
|
|
4002
|
+
const idx = persona.journey.findIndex((s) => s.id === stepId);
|
|
4003
|
+
if (idx === -1) return { removed: false, warnings: [] };
|
|
4004
|
+
const step = persona.journey[idx];
|
|
4005
|
+
const warnings = [];
|
|
4006
|
+
if (step.produces) {
|
|
4007
|
+
for (const key of Object.keys(step.produces)) {
|
|
4008
|
+
const pattern = `{{produces.${key}}}`;
|
|
4009
|
+
for (let i = idx + 1; i < persona.journey.length; i++) {
|
|
4010
|
+
const later = persona.journey[i];
|
|
4011
|
+
const serialized = JSON.stringify(later);
|
|
4012
|
+
if (serialized.includes(pattern)) {
|
|
4013
|
+
warnings.push(`Step ${later.id} consumes {{produces.${key}}} from this step`);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
if (step.spawns && step.spawns.length > 0) {
|
|
4019
|
+
for (const spawn of step.spawns) {
|
|
4020
|
+
warnings.push(`Step spawns persona ${spawn.persona} \u2014 spawn chain will break`);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
persona.journey.splice(idx, 1);
|
|
4024
|
+
await updatePersona(rootDir, personaId, { journey: persona.journey });
|
|
4025
|
+
return { removed: true, warnings };
|
|
4026
|
+
}
|
|
4027
|
+
var PERSONA_ID_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
4028
|
+
var STEP_ID_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
4029
|
+
var ROUTE_REGEX = /^(GET|POST|PUT|PATCH|DELETE)\s+\//;
|
|
4030
|
+
async function validatePersona(rootDir, persona, deep = false) {
|
|
4031
|
+
const errors = [];
|
|
4032
|
+
const warnings = [];
|
|
4033
|
+
if (!PERSONA_ID_REGEX.test(persona.id)) {
|
|
4034
|
+
errors.push({ type: "invalid-id", detail: `ID "${persona.id}" must match /^[a-z][a-z0-9-]*$/` });
|
|
4035
|
+
}
|
|
4036
|
+
if (!persona.name || persona.name.trim() === "") {
|
|
4037
|
+
errors.push({ type: "missing-name", detail: "Name is required" });
|
|
4038
|
+
}
|
|
4039
|
+
if (!persona.trigger || !persona.trigger.type) {
|
|
4040
|
+
errors.push({ type: "missing-trigger", detail: "Trigger with type is required" });
|
|
4041
|
+
}
|
|
4042
|
+
if (persona.trigger.type !== "root" && !persona.trigger.spawned_by) {
|
|
4043
|
+
errors.push({ type: "missing-spawned-by", detail: `Non-root trigger type "${persona.trigger.type}" requires spawned_by` });
|
|
4044
|
+
}
|
|
4045
|
+
if (!persona.journey || persona.journey.length === 0) {
|
|
4046
|
+
errors.push({ type: "empty-journey", detail: "Journey must have at least one step" });
|
|
4047
|
+
}
|
|
4048
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
4049
|
+
const producedKeys = /* @__PURE__ */ new Set();
|
|
4050
|
+
for (const step of persona.journey) {
|
|
4051
|
+
if (!STEP_ID_REGEX.test(step.id)) {
|
|
4052
|
+
errors.push({ type: "invalid-step-id", step: step.id, detail: `Step ID must match /^[a-z][a-z0-9-]*$/` });
|
|
4053
|
+
}
|
|
4054
|
+
if (stepIds.has(step.id)) {
|
|
4055
|
+
errors.push({ type: "duplicate-step-id", step: step.id, detail: `Duplicate step ID "${step.id}"` });
|
|
4056
|
+
}
|
|
4057
|
+
stepIds.add(step.id);
|
|
4058
|
+
if (!ROUTE_REGEX.test(step.route)) {
|
|
4059
|
+
errors.push({ type: "invalid-route", step: step.id, route: step.route, detail: `Route must match "METHOD /path" (e.g., "POST /api/auth/signup")` });
|
|
4060
|
+
}
|
|
4061
|
+
if (!step.gates || step.gates.length === 0) {
|
|
4062
|
+
errors.push({ type: "missing-gates", step: step.id, detail: "Step must have at least one gate" });
|
|
4063
|
+
}
|
|
4064
|
+
if (!step.expect || step.expect.status === void 0) {
|
|
4065
|
+
errors.push({ type: "missing-expect", step: step.id, detail: "Step must have expect with status" });
|
|
4066
|
+
}
|
|
4067
|
+
const serialized = JSON.stringify(step);
|
|
4068
|
+
const producesRefs = serialized.match(/\{\{produces\.([^}]+)\}\}/g) || [];
|
|
4069
|
+
for (const ref of producesRefs) {
|
|
4070
|
+
const key = ref.replace("{{produces.", "").replace("}}", "");
|
|
4071
|
+
if (!producedKeys.has(key)) {
|
|
4072
|
+
errors.push({ type: "unresolved-produces", step: step.id, key, detail: `{{produces.${key}}} used but not produced by a prior step` });
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
if (step.produces) {
|
|
4076
|
+
for (const key of Object.keys(step.produces)) {
|
|
4077
|
+
producedKeys.add(key);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
if (deep) {
|
|
4082
|
+
const portalPath = path8.join(rootDir, "portal.yaml");
|
|
4083
|
+
let portalGates = [];
|
|
4084
|
+
let portalRoutes = [];
|
|
4085
|
+
if (fs7.existsSync(portalPath)) {
|
|
4086
|
+
try {
|
|
4087
|
+
const portal = yaml6.load(fs7.readFileSync(portalPath, "utf8"));
|
|
4088
|
+
if (portal.gates && typeof portal.gates === "object") {
|
|
4089
|
+
portalGates = Object.keys(portal.gates);
|
|
4090
|
+
}
|
|
4091
|
+
if (portal.routes && typeof portal.routes === "object") {
|
|
4092
|
+
portalRoutes = Object.keys(portal.routes);
|
|
4093
|
+
}
|
|
4094
|
+
} catch {
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
for (const step of persona.journey) {
|
|
4098
|
+
for (const gate of step.gates) {
|
|
4099
|
+
if (portalGates.length > 0 && !portalGates.includes(gate)) {
|
|
4100
|
+
errors.push({ type: "gate-not-found", step: step.id, gate, detail: `Gate ${gate} not defined in portal.yaml` });
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
for (const step of persona.journey) {
|
|
4105
|
+
if (portalRoutes.length > 0) {
|
|
4106
|
+
const stepRoute = step.route;
|
|
4107
|
+
const matchesAny = portalRoutes.some((pr) => routeMatches(pr, stepRoute));
|
|
4108
|
+
if (!matchesAny) {
|
|
4109
|
+
warnings.push({ type: "route-not-in-portal", detail: `Route "${stepRoute}" (step ${step.id}) not found in portal.yaml` });
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
for (const step of persona.journey) {
|
|
4114
|
+
if (step.spawns) {
|
|
4115
|
+
for (const spawn of step.spawns) {
|
|
4116
|
+
const target = await loadPersona(rootDir, spawn.persona);
|
|
4117
|
+
if (!target) {
|
|
4118
|
+
errors.push({ type: "spawn-target-missing", step: step.id, detail: `Spawn target persona "${spawn.persona}" does not exist` });
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
const spawnCycle = await detectSpawnCycle(rootDir, persona.id);
|
|
4124
|
+
if (spawnCycle) {
|
|
4125
|
+
errors.push({ type: "spawn-cycle", detail: `Circular spawn dependency: ${spawnCycle.join(" \u2192 ")}` });
|
|
4126
|
+
}
|
|
4127
|
+
if (portalRoutes.length > 0 || portalGates.length > 0) {
|
|
4128
|
+
const allPersonas = await loadPersonas(rootDir);
|
|
4129
|
+
const allGatesUsed = /* @__PURE__ */ new Set();
|
|
4130
|
+
const allRoutesUsed = /* @__PURE__ */ new Set();
|
|
4131
|
+
for (const p of allPersonas) {
|
|
4132
|
+
for (const step of p.journey) {
|
|
4133
|
+
for (const gate of step.gates) allGatesUsed.add(gate);
|
|
4134
|
+
allRoutesUsed.add(step.route);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
let allFlows = [];
|
|
4138
|
+
const flowIndexPath = path8.join(rootDir, ".paradigm", "flow-index.json");
|
|
4139
|
+
if (fs7.existsSync(flowIndexPath)) {
|
|
4140
|
+
try {
|
|
4141
|
+
const flowIndex = JSON.parse(fs7.readFileSync(flowIndexPath, "utf8"));
|
|
4142
|
+
allFlows = Object.keys(flowIndex.flows || {});
|
|
4143
|
+
} catch {
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
const allFlowsUsed = /* @__PURE__ */ new Set();
|
|
4147
|
+
for (const p of allPersonas) {
|
|
4148
|
+
for (const step of p.journey) {
|
|
4149
|
+
if (step.flow) allFlowsUsed.add(step.flow);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
return {
|
|
4153
|
+
persona: persona.id,
|
|
4154
|
+
valid: errors.length === 0,
|
|
4155
|
+
errors,
|
|
4156
|
+
warnings,
|
|
4157
|
+
coverage: {
|
|
4158
|
+
routes: {
|
|
4159
|
+
covered: allRoutesUsed.size,
|
|
4160
|
+
total: portalRoutes.length,
|
|
4161
|
+
uncovered: portalRoutes.filter((r) => !allRoutesUsed.has(r))
|
|
4162
|
+
},
|
|
4163
|
+
gates: {
|
|
4164
|
+
covered: allGatesUsed.size,
|
|
4165
|
+
total: portalGates.length,
|
|
4166
|
+
uncovered: portalGates.filter((g) => !allGatesUsed.has(g))
|
|
4167
|
+
},
|
|
4168
|
+
flows: {
|
|
4169
|
+
covered: allFlowsUsed.size,
|
|
4170
|
+
total: allFlows.length,
|
|
4171
|
+
uncovered: allFlows.filter((f) => !allFlowsUsed.has(f))
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
return {
|
|
4178
|
+
persona: persona.id,
|
|
4179
|
+
valid: errors.length === 0,
|
|
4180
|
+
errors,
|
|
4181
|
+
warnings
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
function routeMatches(portalRoute, stepRoute) {
|
|
4185
|
+
const normalize = (r) => r.replace(/:[a-zA-Z_]+/g, ":param").replace(/\{\{[^}]+\}\}/g, ":param");
|
|
4186
|
+
return normalize(portalRoute) === normalize(stepRoute);
|
|
4187
|
+
}
|
|
4188
|
+
async function detectSpawnCycle(rootDir, startId) {
|
|
4189
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4190
|
+
const stack = [];
|
|
4191
|
+
async function visit(id) {
|
|
4192
|
+
if (visited.has(id)) {
|
|
4193
|
+
const cycleStart = stack.indexOf(id);
|
|
4194
|
+
if (cycleStart !== -1) {
|
|
4195
|
+
return [...stack.slice(cycleStart), id];
|
|
4196
|
+
}
|
|
4197
|
+
return null;
|
|
4198
|
+
}
|
|
4199
|
+
visited.add(id);
|
|
4200
|
+
stack.push(id);
|
|
4201
|
+
const persona = await loadPersona(rootDir, id);
|
|
4202
|
+
if (persona) {
|
|
4203
|
+
for (const step of persona.journey) {
|
|
4204
|
+
if (step.spawns) {
|
|
4205
|
+
for (const spawn of step.spawns) {
|
|
4206
|
+
const cycle = await visit(spawn.persona);
|
|
4207
|
+
if (cycle) return cycle;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
stack.pop();
|
|
4213
|
+
return null;
|
|
4214
|
+
}
|
|
4215
|
+
return visit(startId);
|
|
4216
|
+
}
|
|
4217
|
+
async function rebuildPersonaIndex(rootDir) {
|
|
4218
|
+
const personasDir = path8.join(rootDir, PERSONAS_ROOT);
|
|
4219
|
+
fs7.mkdirSync(personasDir, { recursive: true });
|
|
4220
|
+
const personas = await loadPersonas(rootDir);
|
|
4221
|
+
const entries = {};
|
|
4222
|
+
const gateCoverage = {};
|
|
4223
|
+
const routeCoverage = {};
|
|
4224
|
+
for (const p of personas) {
|
|
4225
|
+
const gates = /* @__PURE__ */ new Set();
|
|
4226
|
+
const flows = /* @__PURE__ */ new Set();
|
|
4227
|
+
const routes = [];
|
|
4228
|
+
const spawns = /* @__PURE__ */ new Set();
|
|
4229
|
+
for (const step of p.journey) {
|
|
4230
|
+
for (const g of step.gates) gates.add(g);
|
|
4231
|
+
if (step.flow) flows.add(step.flow);
|
|
4232
|
+
routes.push(step.route);
|
|
4233
|
+
if (step.spawns) {
|
|
4234
|
+
for (const s of step.spawns) spawns.add(s.persona);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
entries[p.id] = {
|
|
4238
|
+
name: p.name,
|
|
4239
|
+
trigger: p.trigger.type,
|
|
4240
|
+
spawned_by: p.trigger.spawned_by,
|
|
4241
|
+
steps: p.journey.length,
|
|
4242
|
+
gates: [...gates],
|
|
4243
|
+
flows: [...flows],
|
|
4244
|
+
routes,
|
|
4245
|
+
spawns: [...spawns],
|
|
4246
|
+
tags: p.tags || []
|
|
4247
|
+
};
|
|
4248
|
+
for (const gate of gates) {
|
|
4249
|
+
if (!gateCoverage[gate]) gateCoverage[gate] = [];
|
|
4250
|
+
gateCoverage[gate].push(p.id);
|
|
4251
|
+
}
|
|
4252
|
+
for (const route of routes) {
|
|
4253
|
+
if (!routeCoverage[route]) routeCoverage[route] = [];
|
|
4254
|
+
routeCoverage[route].push(p.id);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
let uncoveredRoutes = [];
|
|
4258
|
+
const portalPath = path8.join(rootDir, "portal.yaml");
|
|
4259
|
+
if (fs7.existsSync(portalPath)) {
|
|
4260
|
+
try {
|
|
4261
|
+
const portal = yaml6.load(fs7.readFileSync(portalPath, "utf8"));
|
|
4262
|
+
if (portal.routes && typeof portal.routes === "object") {
|
|
4263
|
+
const portalRoutes = Object.keys(portal.routes);
|
|
4264
|
+
uncoveredRoutes = portalRoutes.filter((pr) => {
|
|
4265
|
+
return !Object.keys(routeCoverage).some((sr) => routeMatches(pr, sr));
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
} catch {
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
const chains = {};
|
|
4272
|
+
const chainsDir = path8.join(personasDir, "chains");
|
|
4273
|
+
if (fs7.existsSync(chainsDir)) {
|
|
4274
|
+
const chainFiles = fs7.readdirSync(chainsDir).filter((f) => f.endsWith(".yaml"));
|
|
4275
|
+
for (const file of chainFiles) {
|
|
4276
|
+
try {
|
|
4277
|
+
const content = fs7.readFileSync(path8.join(chainsDir, file), "utf8");
|
|
4278
|
+
const chain = yaml6.load(content);
|
|
4279
|
+
if (chain && chain.id) {
|
|
4280
|
+
const orderIds = chain.order.map((o) => o.persona);
|
|
4281
|
+
let totalSteps = 0;
|
|
4282
|
+
const totalGates = /* @__PURE__ */ new Set();
|
|
4283
|
+
for (const pid of orderIds) {
|
|
4284
|
+
const entry = entries[pid];
|
|
4285
|
+
if (entry) {
|
|
4286
|
+
totalSteps += entry.steps;
|
|
4287
|
+
for (const g of entry.gates) totalGates.add(g);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
chains[chain.id] = {
|
|
4291
|
+
description: chain.description || "",
|
|
4292
|
+
order: orderIds,
|
|
4293
|
+
total_steps: totalSteps,
|
|
4294
|
+
total_gates: totalGates.size
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
} catch {
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
const index = {
|
|
4302
|
+
version: "1.0",
|
|
4303
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4304
|
+
personas: entries,
|
|
4305
|
+
chains,
|
|
4306
|
+
gate_coverage: gateCoverage,
|
|
4307
|
+
route_coverage: routeCoverage,
|
|
4308
|
+
uncovered_routes: uncoveredRoutes
|
|
4309
|
+
};
|
|
4310
|
+
fs7.writeFileSync(
|
|
4311
|
+
path8.join(personasDir, INDEX_FILE),
|
|
4312
|
+
yaml6.dump(index, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false })
|
|
4313
|
+
);
|
|
4314
|
+
return index;
|
|
4315
|
+
}
|
|
4316
|
+
async function getPersonaCoverage(rootDir) {
|
|
4317
|
+
const personas = await loadPersonas(rootDir);
|
|
4318
|
+
const allGatesUsed = /* @__PURE__ */ new Set();
|
|
4319
|
+
const allRoutesUsed = /* @__PURE__ */ new Set();
|
|
4320
|
+
const allFlowsUsed = /* @__PURE__ */ new Set();
|
|
4321
|
+
for (const p of personas) {
|
|
4322
|
+
for (const step of p.journey) {
|
|
4323
|
+
for (const gate of step.gates) allGatesUsed.add(gate);
|
|
4324
|
+
allRoutesUsed.add(step.route);
|
|
4325
|
+
if (step.flow) allFlowsUsed.add(step.flow);
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
let portalGates = [];
|
|
4329
|
+
let portalRoutes = [];
|
|
4330
|
+
const portalPath = path8.join(rootDir, "portal.yaml");
|
|
4331
|
+
if (fs7.existsSync(portalPath)) {
|
|
4332
|
+
try {
|
|
4333
|
+
const portal = yaml6.load(fs7.readFileSync(portalPath, "utf8"));
|
|
4334
|
+
if (portal.gates && typeof portal.gates === "object") portalGates = Object.keys(portal.gates);
|
|
4335
|
+
if (portal.routes && typeof portal.routes === "object") portalRoutes = Object.keys(portal.routes);
|
|
4336
|
+
} catch {
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
let allFlows = [];
|
|
4340
|
+
const flowIndexPath = path8.join(rootDir, ".paradigm", "flow-index.json");
|
|
4341
|
+
if (fs7.existsSync(flowIndexPath)) {
|
|
4342
|
+
try {
|
|
4343
|
+
const flowIndex = JSON.parse(fs7.readFileSync(flowIndexPath, "utf8"));
|
|
4344
|
+
allFlows = Object.keys(flowIndex.flows || {});
|
|
4345
|
+
} catch {
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
return {
|
|
4349
|
+
routes: {
|
|
4350
|
+
covered: allRoutesUsed.size,
|
|
4351
|
+
total: portalRoutes.length,
|
|
4352
|
+
uncovered: portalRoutes.filter(
|
|
4353
|
+
(r) => !Array.from(allRoutesUsed).some((sr) => routeMatches(r, sr))
|
|
4354
|
+
)
|
|
4355
|
+
},
|
|
4356
|
+
gates: {
|
|
4357
|
+
covered: allGatesUsed.size,
|
|
4358
|
+
total: portalGates.length,
|
|
4359
|
+
uncovered: portalGates.filter((g) => !allGatesUsed.has(g))
|
|
4360
|
+
},
|
|
4361
|
+
flows: {
|
|
4362
|
+
covered: allFlowsUsed.size,
|
|
4363
|
+
total: allFlows.length,
|
|
4364
|
+
uncovered: allFlows.filter((f) => !allFlowsUsed.has(f))
|
|
4365
|
+
},
|
|
4366
|
+
personas: personas.length
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
async function getAffectedPersonas(rootDir, symbol) {
|
|
4370
|
+
const personas = await loadPersonas(rootDir);
|
|
4371
|
+
const results = [];
|
|
4372
|
+
for (const p of personas) {
|
|
4373
|
+
const affectedSteps = [];
|
|
4374
|
+
const spawnsBlocked = [];
|
|
4375
|
+
for (const step of p.journey) {
|
|
4376
|
+
const matches = step.gates.includes(symbol) || step.flow === symbol || step.route === symbol || step.signals?.includes(symbol);
|
|
4377
|
+
if (matches) {
|
|
4378
|
+
affectedSteps.push(step.id);
|
|
4379
|
+
if (step.spawns) {
|
|
4380
|
+
for (const spawn of step.spawns) {
|
|
4381
|
+
spawnsBlocked.push(spawn.persona);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
if (affectedSteps.length > 0) {
|
|
4387
|
+
results.push({ persona: p.id, steps: affectedSteps, spawns_blocked: spawnsBlocked });
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
return results;
|
|
4391
|
+
}
|
|
4392
|
+
function deepGet(obj, path10) {
|
|
4393
|
+
const parts = path10.split(/[.\[\]]+/).filter(Boolean);
|
|
4394
|
+
let current = obj;
|
|
4395
|
+
for (const part of parts) {
|
|
4396
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
4397
|
+
current = current[part];
|
|
4398
|
+
}
|
|
4399
|
+
return current;
|
|
4400
|
+
}
|
|
4401
|
+
function assertStep(step, event) {
|
|
4402
|
+
const assertions = [];
|
|
4403
|
+
if (event.status !== step.expect.status) {
|
|
4404
|
+
assertions.push({
|
|
4405
|
+
type: "status",
|
|
4406
|
+
field: "status",
|
|
4407
|
+
expected: step.expect.status,
|
|
4408
|
+
actual: event.status,
|
|
4409
|
+
message: `Step ${step.id}: status is ${event.status}, expected ${step.expect.status}`
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
if (step.expect.body?.has) {
|
|
4413
|
+
const body = event.body;
|
|
4414
|
+
for (const key of step.expect.body.has) {
|
|
4415
|
+
if (!body || typeof body !== "object" || !(key in body)) {
|
|
4416
|
+
assertions.push({
|
|
4417
|
+
type: "body.has",
|
|
4418
|
+
field: key,
|
|
4419
|
+
expected: true,
|
|
4420
|
+
actual: false,
|
|
4421
|
+
message: `Step ${step.id}: body missing key '${key}'`
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
if (step.expect.body?.match) {
|
|
4427
|
+
const body = event.body;
|
|
4428
|
+
for (const [field, expected] of Object.entries(step.expect.body.match)) {
|
|
4429
|
+
const actual = body ? deepGet(body, field) : void 0;
|
|
4430
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
4431
|
+
assertions.push({
|
|
4432
|
+
type: "body.match",
|
|
4433
|
+
field,
|
|
4434
|
+
expected,
|
|
4435
|
+
actual: actual ?? null,
|
|
4436
|
+
message: `Step ${step.id}: '${field}' is ${JSON.stringify(actual ?? null)}, expected ${JSON.stringify(expected)}`
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
if (step.signals && step.signals.length > 0) {
|
|
4442
|
+
const firedSignals = event.signals_fired || [];
|
|
4443
|
+
for (const signal of step.signals) {
|
|
4444
|
+
if (!firedSignals.includes(signal)) {
|
|
4445
|
+
assertions.push({
|
|
4446
|
+
type: "signal",
|
|
4447
|
+
field: "signals_fired",
|
|
4448
|
+
expected: signal,
|
|
4449
|
+
actual: firedSignals,
|
|
4450
|
+
message: `Step ${step.id}: signal '${signal}' was not fired`
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
if (step.gates.length > 0 && event.gates_traversed) {
|
|
4456
|
+
for (const gate of step.gates) {
|
|
4457
|
+
if (!event.gates_traversed.includes(gate)) {
|
|
4458
|
+
assertions.push({
|
|
4459
|
+
type: "gate",
|
|
4460
|
+
field: "gates_traversed",
|
|
4461
|
+
expected: gate,
|
|
4462
|
+
actual: event.gates_traversed,
|
|
4463
|
+
message: `Step ${step.id}: gate '${gate}' was not traversed`
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
return assertions;
|
|
4469
|
+
}
|
|
4470
|
+
async function validateAgainstSentinel(persona, options = {}) {
|
|
4471
|
+
const steps = [];
|
|
4472
|
+
try {
|
|
4473
|
+
const { SentinelStorage } = await import("./dist-YB7T54QE.js");
|
|
4474
|
+
const storage = new SentinelStorage();
|
|
4475
|
+
const events = storage.queryEvents?.({
|
|
4476
|
+
schemaId: "paradigm-personas",
|
|
4477
|
+
eventType: "persona.step.complete",
|
|
4478
|
+
scopeValue: persona.id,
|
|
4479
|
+
limit: 500
|
|
4480
|
+
}) || [];
|
|
4481
|
+
const filtered = events.filter((e) => {
|
|
4482
|
+
const data = JSON.parse(e.data_json || "{}");
|
|
4483
|
+
if (options.run_id && data.run_id !== options.run_id) return false;
|
|
4484
|
+
if (options.chain_id && data.chain_id !== options.chain_id) return false;
|
|
4485
|
+
if (options.environment && data.environment !== options.environment) return false;
|
|
4486
|
+
return true;
|
|
4487
|
+
});
|
|
4488
|
+
const failEvents = storage.queryEvents?.({
|
|
4489
|
+
schemaId: "paradigm-personas",
|
|
4490
|
+
eventType: "persona.step.fail",
|
|
4491
|
+
scopeValue: persona.id,
|
|
4492
|
+
limit: 500
|
|
4493
|
+
}) || [];
|
|
4494
|
+
const filteredFails = failEvents.filter((e) => {
|
|
4495
|
+
const data = JSON.parse(e.data_json || "{}");
|
|
4496
|
+
if (options.run_id && data.run_id !== options.run_id) return false;
|
|
4497
|
+
if (options.chain_id && data.chain_id !== options.chain_id) return false;
|
|
4498
|
+
if (options.environment && data.environment !== options.environment) return false;
|
|
4499
|
+
return true;
|
|
4500
|
+
});
|
|
4501
|
+
const eventMap = /* @__PURE__ */ new Map();
|
|
4502
|
+
for (const e of [...filtered, ...filteredFails]) {
|
|
4503
|
+
const data = JSON.parse(e.data_json || "{}");
|
|
4504
|
+
if (data.step_id) {
|
|
4505
|
+
eventMap.set(data.step_id, data);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
for (const step of persona.journey) {
|
|
4509
|
+
const eventData = eventMap.get(step.id);
|
|
4510
|
+
if (!eventData) {
|
|
4511
|
+
steps.push({
|
|
4512
|
+
step_id: step.id,
|
|
4513
|
+
matched: false,
|
|
4514
|
+
assertions: [],
|
|
4515
|
+
message: `No Sentinel event found for step '${step.id}' \u2014 step was never exercised`
|
|
4516
|
+
});
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4519
|
+
const assertions = assertStep(step, {
|
|
4520
|
+
status: eventData.status,
|
|
4521
|
+
body: eventData.body,
|
|
4522
|
+
gates_traversed: eventData.gates_traversed,
|
|
4523
|
+
signals_fired: eventData.signals_fired
|
|
4524
|
+
});
|
|
4525
|
+
steps.push({
|
|
4526
|
+
step_id: step.id,
|
|
4527
|
+
matched: true,
|
|
4528
|
+
passed: assertions.length === 0,
|
|
4529
|
+
assertions
|
|
4530
|
+
});
|
|
4531
|
+
}
|
|
4532
|
+
} catch {
|
|
4533
|
+
for (const step of persona.journey) {
|
|
4534
|
+
steps.push({
|
|
4535
|
+
step_id: step.id,
|
|
4536
|
+
matched: false,
|
|
4537
|
+
assertions: [],
|
|
4538
|
+
message: "Sentinel unavailable \u2014 cannot validate events"
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
const matched = steps.filter((s) => s.matched).length;
|
|
4543
|
+
const passed = steps.filter((s) => s.passed).length;
|
|
4544
|
+
const failed = steps.filter((s) => s.matched && !s.passed).length;
|
|
4545
|
+
const totalAssertionFailures = steps.reduce((sum, s) => sum + s.assertions.length, 0);
|
|
4546
|
+
return {
|
|
4547
|
+
run_id: options.run_id,
|
|
4548
|
+
environment: options.environment,
|
|
4549
|
+
steps,
|
|
4550
|
+
summary: {
|
|
4551
|
+
total_steps: persona.journey.length,
|
|
4552
|
+
matched,
|
|
4553
|
+
unmatched: persona.journey.length - matched,
|
|
4554
|
+
passed,
|
|
4555
|
+
failed,
|
|
4556
|
+
assertion_failures: totalAssertionFailures
|
|
4557
|
+
}
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
// ../paradigm-mcp/src/tools/reindex.ts
|
|
4562
|
+
var SYMBOL_CATEGORIES = {
|
|
4563
|
+
"@": { category: "features", prefix: "@" },
|
|
4564
|
+
"#": { category: "components", prefix: "#" },
|
|
4565
|
+
"^": { category: "gates", prefix: "^" },
|
|
4566
|
+
"$": { category: "flows", prefix: "$" },
|
|
4567
|
+
"&": { category: "integrations", prefix: "&" },
|
|
4568
|
+
"!": { category: "signals", prefix: "!" },
|
|
4569
|
+
"%": { category: "state", prefix: "%" }
|
|
4570
|
+
};
|
|
4571
|
+
var DIRECTORY_PATTERNS = {
|
|
4572
|
+
features: ["src/features/", "features/", "app/", "src/app/", "src/modules/", "modules/"],
|
|
4573
|
+
components: ["src/components/", "components/", "src/lib/", "lib/", "src/ui/", "ui/"],
|
|
4574
|
+
gates: ["middleware/", "src/middleware/", "auth/", "src/auth/", "guards/", "src/guards/"],
|
|
4575
|
+
flows: ["flows/", "src/flows/", "workflows/", "src/workflows/", "sagas/", "src/sagas/"],
|
|
4576
|
+
integrations: ["integrations/", "src/integrations/", "external/", "src/external/", "vendors/"],
|
|
4577
|
+
signals: ["events/", "src/events/", "handlers/", "src/handlers/"],
|
|
4578
|
+
state: ["stores/", "src/stores/", "state/", "src/state/", "reducers/", "src/reducers/"]
|
|
4579
|
+
};
|
|
4580
|
+
var KEY_FILE_PATTERNS = {
|
|
4581
|
+
config: [".paradigm/config.yaml", "package.json", "tsconfig.json", ".env.example"],
|
|
4582
|
+
entry: ["src/index.ts", "src/index.tsx", "src/main.ts", "src/main.tsx", "index.ts", "main.ts", "src/app.ts", "src/app.tsx"],
|
|
4583
|
+
types: ["src/types/", "types/", "src/types.ts", "types.ts"]
|
|
4584
|
+
};
|
|
4585
|
+
var DEFAULT_SKIP_PATTERNS = {
|
|
4586
|
+
always: [
|
|
4587
|
+
"node_modules/",
|
|
4588
|
+
"dist/",
|
|
4589
|
+
"build/",
|
|
4590
|
+
".git/",
|
|
4591
|
+
".next/",
|
|
4592
|
+
".nuxt/",
|
|
4593
|
+
".cache/",
|
|
4594
|
+
"*.lock",
|
|
4595
|
+
"*.log"
|
|
4596
|
+
],
|
|
4597
|
+
unless_testing: [
|
|
4598
|
+
"**/*.test.ts",
|
|
4599
|
+
"**/*.test.tsx",
|
|
4600
|
+
"**/*.spec.ts",
|
|
4601
|
+
"**/*.spec.tsx",
|
|
4602
|
+
"__tests__/",
|
|
4603
|
+
"test/",
|
|
4604
|
+
"tests/"
|
|
4605
|
+
],
|
|
4606
|
+
unless_docs: ["docs/", "*.md", "README*", "CHANGELOG*"]
|
|
4607
|
+
};
|
|
4608
|
+
function getReindexToolsList() {
|
|
4609
|
+
return [
|
|
4610
|
+
{
|
|
4611
|
+
name: "paradigm_reindex",
|
|
4612
|
+
description: "Rebuild scan-index.json, navigator.yaml, and flow-index.json from .purpose files. Call after modifying paradigm files or at the end of a work session to ensure static index files are fresh. Returns counts of indexed symbols, files processed, and any errors. ~150 tokens.",
|
|
4613
|
+
inputSchema: {
|
|
4614
|
+
type: "object",
|
|
4615
|
+
properties: {}
|
|
4616
|
+
},
|
|
4617
|
+
annotations: {
|
|
4618
|
+
readOnlyHint: false,
|
|
4619
|
+
destructiveHint: true
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
];
|
|
4623
|
+
}
|
|
4624
|
+
async function handleReindexTool(name, _args, ctx, reloadContext) {
|
|
4625
|
+
if (name !== "paradigm_reindex") {
|
|
4626
|
+
return { handled: false, text: "" };
|
|
4627
|
+
}
|
|
4628
|
+
try {
|
|
4629
|
+
const result = await rebuildStaticFiles(ctx.rootDir, ctx);
|
|
4630
|
+
await reloadContext();
|
|
4631
|
+
toolCache.clear();
|
|
4632
|
+
const text = JSON.stringify(result, null, 2);
|
|
4633
|
+
trackToolCall(text.length, name);
|
|
4634
|
+
return { handled: true, text };
|
|
4635
|
+
} catch (err) {
|
|
4636
|
+
const text = JSON.stringify({ error: err.message }, null, 2);
|
|
4637
|
+
trackToolCall(text.length, name);
|
|
4638
|
+
return { handled: true, text };
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
async function rebuildStaticFiles(rootDir, ctx) {
|
|
4642
|
+
const filesWritten = [];
|
|
4643
|
+
let aggregation;
|
|
4644
|
+
if (ctx) {
|
|
4645
|
+
aggregation = ctx.aggregation;
|
|
4646
|
+
} else {
|
|
4647
|
+
aggregation = await aggregateFromDirectory(rootDir);
|
|
4648
|
+
}
|
|
4649
|
+
const projectName = ctx?.projectName || path9.basename(rootDir);
|
|
4650
|
+
const paradigmDir = path9.join(rootDir, ".paradigm");
|
|
4651
|
+
if (!fs8.existsSync(paradigmDir)) {
|
|
4652
|
+
fs8.mkdirSync(paradigmDir, { recursive: true });
|
|
4653
|
+
}
|
|
4654
|
+
const scanIndex = generateScanIndex(
|
|
4655
|
+
{
|
|
4656
|
+
symbols: aggregation.symbols,
|
|
4657
|
+
purposeFiles: aggregation.purposeFiles,
|
|
4658
|
+
portalFiles: aggregation.portalFiles
|
|
4659
|
+
},
|
|
4660
|
+
{ projectName }
|
|
4661
|
+
);
|
|
4662
|
+
const scanIndexPath = path9.join(paradigmDir, "scan-index.json");
|
|
4663
|
+
fs8.writeFileSync(scanIndexPath, serializeScanIndex(scanIndex), "utf8");
|
|
4664
|
+
filesWritten.push(".paradigm/scan-index.json");
|
|
4665
|
+
const navigatorData = buildNavigatorData(rootDir, aggregation);
|
|
4666
|
+
const navigatorPath = path9.join(paradigmDir, "navigator.yaml");
|
|
4667
|
+
fs8.writeFileSync(
|
|
4668
|
+
navigatorPath,
|
|
4669
|
+
yaml7.dump(navigatorData, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }),
|
|
4670
|
+
"utf8"
|
|
4671
|
+
);
|
|
4672
|
+
filesWritten.push(".paradigm/navigator.yaml");
|
|
4673
|
+
const flowIndex = generateFlowIndex(rootDir, aggregation.purposeFiles);
|
|
4674
|
+
let flowCount = 0;
|
|
4675
|
+
if (flowIndex && Object.keys(flowIndex.flows).length > 0) {
|
|
4676
|
+
const flowIndexPath = path9.join(paradigmDir, "flow-index.json");
|
|
4677
|
+
fs8.writeFileSync(flowIndexPath, JSON.stringify(flowIndex, null, 2), "utf8");
|
|
4678
|
+
filesWritten.push(".paradigm/flow-index.json");
|
|
4679
|
+
flowCount = Object.keys(flowIndex.flows).length;
|
|
4680
|
+
}
|
|
4681
|
+
let aspectGraphStats;
|
|
4682
|
+
try {
|
|
4683
|
+
const db = await openAspectGraph(rootDir);
|
|
4684
|
+
materializeAspects(db, aggregation.symbols, rootDir);
|
|
4685
|
+
const loreLinks = await materializeLoreLinks(db, rootDir);
|
|
4686
|
+
const inferredEdges = await inferLoreEdges(db, rootDir);
|
|
4687
|
+
const aspectCount = db.exec("SELECT COUNT(*) FROM aspects")[0]?.values[0]?.[0] ?? 0;
|
|
4688
|
+
const anchorCount = db.exec("SELECT COUNT(*) FROM anchors")[0]?.values[0]?.[0] ?? 0;
|
|
4689
|
+
const edgeCount = db.exec("SELECT COUNT(*) FROM edges")[0]?.values[0]?.[0] ?? 0;
|
|
4690
|
+
closeAspectGraph(db, rootDir);
|
|
4691
|
+
filesWritten.push(".paradigm/aspect-graph.db");
|
|
4692
|
+
aspectGraphStats = {
|
|
4693
|
+
aspects: aspectCount,
|
|
4694
|
+
anchors: anchorCount,
|
|
4695
|
+
edges: edgeCount,
|
|
4696
|
+
loreLinks
|
|
4697
|
+
};
|
|
4698
|
+
} catch {
|
|
4699
|
+
}
|
|
4700
|
+
let personaCount = 0;
|
|
4701
|
+
try {
|
|
4702
|
+
const personaIndex = await rebuildPersonaIndex(rootDir);
|
|
4703
|
+
personaCount = Object.keys(personaIndex.personas).length;
|
|
4704
|
+
if (personaCount > 0) {
|
|
4705
|
+
filesWritten.push(".paradigm/personas/index.yaml");
|
|
4706
|
+
}
|
|
4707
|
+
} catch {
|
|
4708
|
+
}
|
|
4709
|
+
const breakdown = {};
|
|
4710
|
+
for (const sym of aggregation.symbols) {
|
|
4711
|
+
breakdown[sym.type] = (breakdown[sym.type] || 0) + 1;
|
|
4712
|
+
}
|
|
4713
|
+
return {
|
|
4714
|
+
action: "reindex",
|
|
4715
|
+
filesWritten,
|
|
4716
|
+
symbolCount: aggregation.symbols.length,
|
|
4717
|
+
breakdown,
|
|
4718
|
+
flowCount,
|
|
4719
|
+
aspectGraphStats,
|
|
4720
|
+
personaCount
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
function buildNavigatorData(rootDir, aggregation) {
|
|
4724
|
+
return {
|
|
4725
|
+
version: "1.0",
|
|
4726
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4727
|
+
structure: buildStructure(rootDir),
|
|
4728
|
+
key_files: buildKeyFiles(rootDir),
|
|
4729
|
+
skip_patterns: buildSkipPatterns(rootDir),
|
|
4730
|
+
symbols: buildSymbolMap(aggregation.symbols, aggregation.purposeFiles, rootDir)
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
function buildStructure(rootDir) {
|
|
4734
|
+
const structure = {};
|
|
4735
|
+
for (const [category, patterns] of Object.entries(DIRECTORY_PATTERNS)) {
|
|
4736
|
+
const existingPaths = patterns.filter((p) => fs8.existsSync(path9.join(rootDir, p)));
|
|
4737
|
+
if (existingPaths.length > 0) {
|
|
4738
|
+
const symbolInfo = Object.values(SYMBOL_CATEGORIES).find((s) => s.category === category);
|
|
4739
|
+
structure[category] = { paths: existingPaths, symbol: symbolInfo?.prefix || "@" };
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
return structure;
|
|
4743
|
+
}
|
|
4744
|
+
function buildKeyFiles(rootDir) {
|
|
4745
|
+
const keyFiles = {};
|
|
4746
|
+
for (const [category, patterns] of Object.entries(KEY_FILE_PATTERNS)) {
|
|
4747
|
+
const existingPaths = patterns.filter((p) => fs8.existsSync(path9.join(rootDir, p)));
|
|
4748
|
+
if (existingPaths.length > 0) {
|
|
4749
|
+
keyFiles[category] = existingPaths;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
if (!keyFiles.config) keyFiles.config = [];
|
|
4753
|
+
if (!keyFiles.entry) keyFiles.entry = [];
|
|
4754
|
+
if (!keyFiles.types) keyFiles.types = [];
|
|
4755
|
+
return keyFiles;
|
|
4756
|
+
}
|
|
4757
|
+
function buildSkipPatterns(rootDir) {
|
|
4758
|
+
const patterns = {
|
|
4759
|
+
always: [...DEFAULT_SKIP_PATTERNS.always],
|
|
4760
|
+
unless_testing: [...DEFAULT_SKIP_PATTERNS.unless_testing],
|
|
4761
|
+
unless_docs: [...DEFAULT_SKIP_PATTERNS.unless_docs]
|
|
4762
|
+
};
|
|
4763
|
+
const gitignorePath = path9.join(rootDir, ".gitignore");
|
|
4764
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
4765
|
+
try {
|
|
4766
|
+
const content = fs8.readFileSync(gitignorePath, "utf8");
|
|
4767
|
+
const gitignorePatterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).filter(
|
|
4768
|
+
(line) => line.endsWith("/") || line.includes("*") || ["node_modules", "dist", "build", ".cache"].some((p) => line.includes(p))
|
|
4769
|
+
).slice(0, 20);
|
|
4770
|
+
for (const pattern of gitignorePatterns) {
|
|
4771
|
+
if (!patterns.always.includes(pattern)) {
|
|
4772
|
+
patterns.always.push(pattern);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
} catch {
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
return patterns;
|
|
4779
|
+
}
|
|
4780
|
+
function getSymbolPrefix(type) {
|
|
4781
|
+
switch (type) {
|
|
4782
|
+
case "feature":
|
|
4783
|
+
return "@";
|
|
4784
|
+
case "component":
|
|
4785
|
+
return "#";
|
|
4786
|
+
case "gate":
|
|
4787
|
+
return "^";
|
|
4788
|
+
case "flow":
|
|
4789
|
+
return "$";
|
|
4790
|
+
case "integration":
|
|
4791
|
+
return "&";
|
|
4792
|
+
case "signal":
|
|
4793
|
+
return "!";
|
|
4794
|
+
case "state":
|
|
4795
|
+
return "%";
|
|
4796
|
+
case "idea":
|
|
4797
|
+
return "?";
|
|
4798
|
+
case "deprecated":
|
|
4799
|
+
return "~";
|
|
4800
|
+
case "aspect":
|
|
4801
|
+
return "~";
|
|
4802
|
+
default:
|
|
4803
|
+
return "@";
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
function buildSymbolMap(symbols, purposeFiles, _rootDir) {
|
|
4807
|
+
const symbolMap = {};
|
|
4808
|
+
for (const symbol of symbols) {
|
|
4809
|
+
const prefix = getSymbolPrefix(symbol.type);
|
|
4810
|
+
const symbolId = `${prefix}${symbol.id}`;
|
|
4811
|
+
if (symbol.filePath) {
|
|
4812
|
+
symbolMap[symbolId] = symbol.filePath;
|
|
4813
|
+
} else {
|
|
4814
|
+
const matchingPurpose = purposeFiles.find((pf) => {
|
|
4815
|
+
const dir = path9.dirname(pf);
|
|
4816
|
+
return dir.toLowerCase().includes(symbol.id.toLowerCase());
|
|
4817
|
+
});
|
|
4818
|
+
if (matchingPurpose) {
|
|
4819
|
+
symbolMap[symbolId] = path9.dirname(matchingPurpose) + "/";
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
return symbolMap;
|
|
4824
|
+
}
|
|
4825
|
+
function generateFlowIndex(rootDir, purposeFiles) {
|
|
4826
|
+
const flows = {};
|
|
4827
|
+
const symbolToFlows = {};
|
|
4828
|
+
for (const filePath of purposeFiles) {
|
|
4829
|
+
try {
|
|
4830
|
+
const content = fs8.readFileSync(filePath, "utf8");
|
|
4831
|
+
const data = yaml7.load(content);
|
|
4832
|
+
if (!data?.flows) continue;
|
|
4833
|
+
if (Array.isArray(data.flows)) {
|
|
4834
|
+
for (const flowItem of data.flows) {
|
|
4835
|
+
const flow = flowItem;
|
|
4836
|
+
if (!flow.name) continue;
|
|
4837
|
+
const flowId = `$${flow.name}`;
|
|
4838
|
+
const steps = parseFlowSteps(flow.steps);
|
|
4839
|
+
if (steps.length > 0) {
|
|
4840
|
+
flows[flowId] = {
|
|
4841
|
+
id: flowId,
|
|
4842
|
+
description: flow.description || "",
|
|
4843
|
+
steps,
|
|
4844
|
+
definedIn: path9.relative(rootDir, filePath)
|
|
4845
|
+
};
|
|
4846
|
+
indexFlowSymbols(flowId, steps, symbolToFlows);
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
} else {
|
|
4850
|
+
for (const [name, flowDef] of Object.entries(data.flows)) {
|
|
4851
|
+
const flowId = name.startsWith("$") ? name : `$${name}`;
|
|
4852
|
+
const steps = parseFlowSteps(flowDef.steps);
|
|
4853
|
+
if (steps.length > 0) {
|
|
4854
|
+
flows[flowId] = {
|
|
4855
|
+
id: flowId,
|
|
4856
|
+
description: flowDef.description || "",
|
|
4857
|
+
trigger: flowDef.trigger,
|
|
4858
|
+
steps,
|
|
4859
|
+
validation: flowDef.validation,
|
|
4860
|
+
definedIn: path9.relative(rootDir, filePath)
|
|
4861
|
+
};
|
|
4862
|
+
indexFlowSymbols(flowId, steps, symbolToFlows);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
} catch {
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
if (Object.keys(flows).length === 0) {
|
|
4870
|
+
return null;
|
|
4871
|
+
}
|
|
4872
|
+
return {
|
|
4873
|
+
version: "1.0",
|
|
4874
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4875
|
+
flows,
|
|
4876
|
+
symbolToFlows
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4879
|
+
function parseFlowSteps(steps) {
|
|
4880
|
+
if (!steps || !Array.isArray(steps)) return [];
|
|
4881
|
+
const result = [];
|
|
4882
|
+
for (let i = 0; i < steps.length; i++) {
|
|
4883
|
+
const step = steps[i];
|
|
4884
|
+
if (typeof step === "object" && step !== null) {
|
|
4885
|
+
const s = step;
|
|
4886
|
+
const action = s.action || s.description || s.component || "";
|
|
4887
|
+
if (action) {
|
|
4888
|
+
result.push({
|
|
4889
|
+
id: s.id || `step-${i + 1}`,
|
|
4890
|
+
action,
|
|
4891
|
+
symbol: s.symbol || s.component,
|
|
4892
|
+
expect: s.expect
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
return result;
|
|
4898
|
+
}
|
|
4899
|
+
function indexFlowSymbols(flowId, steps, symbolToFlows) {
|
|
4900
|
+
for (const step of steps) {
|
|
4901
|
+
if (step.symbol) {
|
|
4902
|
+
if (!symbolToFlows[step.symbol]) {
|
|
4903
|
+
symbolToFlows[step.symbol] = [];
|
|
4904
|
+
}
|
|
4905
|
+
if (!symbolToFlows[step.symbol].includes(flowId)) {
|
|
4906
|
+
symbolToFlows[step.symbol].push(flowId);
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
export {
|
|
4913
|
+
parsePurposeFileDetailed,
|
|
4914
|
+
serializePurposeFile,
|
|
4915
|
+
findPurposeFiles,
|
|
4916
|
+
validatePurposeFile,
|
|
4917
|
+
parseGateConfig,
|
|
4918
|
+
aggregateFromDirectory,
|
|
4919
|
+
buildSymbolIndex,
|
|
4920
|
+
getSymbol,
|
|
4921
|
+
getSymbolsByType,
|
|
4922
|
+
searchSymbols,
|
|
4923
|
+
getReferencesTo,
|
|
4924
|
+
getReferencesFrom,
|
|
4925
|
+
getSymbolCounts,
|
|
4926
|
+
getAllSymbols,
|
|
4927
|
+
loadGlobalAntipatterns,
|
|
4928
|
+
loadGlobalDecisions,
|
|
4929
|
+
loadGlobalPreferences,
|
|
4930
|
+
recordGlobalAntipattern,
|
|
4931
|
+
recordGlobalDecision,
|
|
4932
|
+
getSessionTracker,
|
|
4933
|
+
trackToolCall,
|
|
4934
|
+
trackResourceRead,
|
|
4935
|
+
addToolBreadcrumb,
|
|
4936
|
+
getContextToolsList,
|
|
4937
|
+
handleContextTool,
|
|
4938
|
+
buildRecoveryPreamble,
|
|
4939
|
+
toolCache,
|
|
4940
|
+
openAspectGraph,
|
|
4941
|
+
closeAspectGraph,
|
|
4942
|
+
getAspect,
|
|
4943
|
+
getAnchorsForAspect,
|
|
4944
|
+
getEdgesFrom,
|
|
4945
|
+
getEdgesTo,
|
|
4946
|
+
getAllEdgesFor,
|
|
4947
|
+
incrementHeatmap,
|
|
4948
|
+
getHeatmap,
|
|
4949
|
+
checkDrift,
|
|
4950
|
+
loadLoreEntries,
|
|
4951
|
+
loadLoreEntry,
|
|
4952
|
+
loadLoreTimeline,
|
|
4953
|
+
recordLoreEntry,
|
|
4954
|
+
updateLoreEntry,
|
|
4955
|
+
deleteLoreEntry,
|
|
4956
|
+
getLoreForAspect,
|
|
4957
|
+
loadPersonas,
|
|
4958
|
+
loadPersona,
|
|
4959
|
+
createPersona,
|
|
4960
|
+
updatePersona,
|
|
4961
|
+
deletePersona,
|
|
4962
|
+
addStep,
|
|
4963
|
+
removeStep,
|
|
4964
|
+
validatePersona,
|
|
4965
|
+
getPersonaCoverage,
|
|
4966
|
+
getAffectedPersonas,
|
|
4967
|
+
validateAgainstSentinel,
|
|
4968
|
+
getReindexToolsList,
|
|
4969
|
+
handleReindexTool,
|
|
4970
|
+
rebuildStaticFiles
|
|
4971
|
+
};
|