coaiajs 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/agents/custom_gpt/ceremony-media-proxy.instructions.md +86 -0
- package/agents/custom_gpt/ceremony-media-proxy.yml +158 -0
- package/agents/custom_gpt/ceremony-observations.instructions.md +38 -5
- package/agents/custom_gpt/ceremony-observations.yml +737 -287
- package/dist/mcp/config.d.ts.map +1 -1
- package/dist/mcp/config.js +13 -1
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/server.js +11 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/src/cli.js +17 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +10 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/langfuse/index.d.ts +2 -1
- package/dist/src/langfuse/index.d.ts.map +1 -1
- package/dist/src/langfuse/index.js +1 -1
- package/dist/src/langfuse/index.js.map +1 -1
- package/dist/src/langfuse/media.d.ts +30 -0
- package/dist/src/langfuse/media.d.ts.map +1 -1
- package/dist/src/langfuse/media.js +128 -77
- package/dist/src/langfuse/media.js.map +1 -1
- package/dist/src/media-upload-proxy.d.ts +32 -0
- package/dist/src/media-upload-proxy.d.ts.map +1 -0
- package/dist/src/media-upload-proxy.js +210 -0
- package/dist/src/media-upload-proxy.js.map +1 -0
- package/dist/src/narrative/argument-hygiene.d.ts +46 -0
- package/dist/src/narrative/argument-hygiene.d.ts.map +1 -0
- package/dist/src/narrative/argument-hygiene.js +155 -0
- package/dist/src/narrative/argument-hygiene.js.map +1 -0
- package/dist/src/narrative/contract.d.ts +200 -0
- package/dist/src/narrative/contract.d.ts.map +1 -0
- package/dist/src/narrative/contract.js +274 -0
- package/dist/src/narrative/contract.js.map +1 -0
- package/dist/src/narrative/github-bridge.d.ts +23 -0
- package/dist/src/narrative/github-bridge.d.ts.map +1 -0
- package/dist/src/narrative/github-bridge.js +320 -0
- package/dist/src/narrative/github-bridge.js.map +1 -0
- package/dist/src/narrative/graph-manager.d.ts +83 -2
- package/dist/src/narrative/graph-manager.d.ts.map +1 -1
- package/dist/src/narrative/graph-manager.js +444 -54
- package/dist/src/narrative/graph-manager.js.map +1 -1
- package/dist/src/narrative/index.d.ts +24 -3
- package/dist/src/narrative/index.d.ts.map +1 -1
- package/dist/src/narrative/index.js +36 -15
- package/dist/src/narrative/index.js.map +1 -1
- package/dist/src/narrative/jsonl-preservation.d.ts +26 -0
- package/dist/src/narrative/jsonl-preservation.d.ts.map +1 -0
- package/dist/src/narrative/jsonl-preservation.js +293 -0
- package/dist/src/narrative/jsonl-preservation.js.map +1 -0
- package/dist/src/narrative/jsonl-records.d.ts +31 -0
- package/dist/src/narrative/jsonl-records.d.ts.map +1 -0
- package/dist/src/narrative/jsonl-records.js +69 -0
- package/dist/src/narrative/jsonl-records.js.map +1 -0
- package/dist/src/narrative/tool-definitions.d.ts +2 -1
- package/dist/src/narrative/tool-definitions.d.ts.map +1 -1
- package/dist/src/narrative/tool-definitions.js +134 -1
- package/dist/src/narrative/tool-definitions.js.map +1 -1
- package/dist/src/narrative/tool-handlers.d.ts.map +1 -1
- package/dist/src/narrative/tool-handlers.js +203 -20
- package/dist/src/narrative/tool-handlers.js.map +1 -1
- package/dist/src/narrative/types.d.ts +1 -1
- package/dist/src/narrative/types.d.ts.map +1 -1
- package/dist/src/narrative/validation.d.ts +13 -2
- package/dist/src/narrative/validation.d.ts.map +1 -1
- package/dist/src/narrative/validation.js +19 -4
- package/dist/src/narrative/validation.js.map +1 -1
- package/dist/src/redis.d.ts +10 -5
- package/dist/src/redis.d.ts.map +1 -1
- package/dist/src/redis.js +94 -8
- package/dist/src/redis.js.map +1 -1
- package/dist/src/types.d.ts +189 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/llms-full.txt +73 -7
- package/llms.txt +5 -3
- package/package.json +11 -2
- package/rispecs/03-langfuse-module.spec.md +2 -1
|
@@ -5,57 +5,82 @@
|
|
|
5
5
|
* Implements structural tension charts, narrative beats, MMOT evaluation,
|
|
6
6
|
* and general-purpose entity/relation CRUD over a JSONL file.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { readJsonlMemoryFile, writeJsonlMemoryFile } from './jsonl-preservation.js';
|
|
9
|
+
import { assertNoUnparsedCallSyntax } from './argument-hygiene.js';
|
|
10
|
+
import { createGithubProjectFieldProjection, } from './github-bridge.js';
|
|
11
|
+
/**
|
|
12
|
+
* Parse a GitHub issue reference written as `owner/repo#number`.
|
|
13
|
+
*
|
|
14
|
+
* The full path is required. A bare `#42` is rejected: charts travel between
|
|
15
|
+
* repositories, and a bare number silently resolves against whichever repo the
|
|
16
|
+
* reader happens to be in — which is how an issue from one project ends up
|
|
17
|
+
* cited as another project's.
|
|
18
|
+
*/
|
|
19
|
+
export function parseGithubIssueSpec(spec) {
|
|
20
|
+
const match = /^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)#(\d+)$/.exec(spec.trim());
|
|
21
|
+
if (!match) {
|
|
22
|
+
throw new Error(`Invalid GitHub issue reference: "${spec}". Use the full path 'owner/repo#number' (e.g. jgwill/coaiajs#1). A bare '#number' is not accepted — it resolves against the wrong repository as soon as the chart is read somewhere else.`);
|
|
23
|
+
}
|
|
24
|
+
const [, owner, repo, number] = match;
|
|
25
|
+
return {
|
|
26
|
+
owner: owner,
|
|
27
|
+
repo: repo,
|
|
28
|
+
number: Number(number),
|
|
29
|
+
url: `https://github.com/${owner}/${repo}/issues/${number}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
9
32
|
export class KnowledgeGraphManager {
|
|
10
33
|
memoryFilePath;
|
|
11
34
|
constructor(memoryFilePath) {
|
|
35
|
+
/**
|
|
36
|
+
* Refuse a path that still carries an unexpanded shell variable.
|
|
37
|
+
*
|
|
38
|
+
* Paid for 2026-08-11 in the coaia-narrative lineage. A seat booted with its
|
|
39
|
+
* memory-path variable unset, so its `.mcp.json` handed the process the
|
|
40
|
+
* placeholder verbatim. The server started happily and wrote a live chart into
|
|
41
|
+
* a file literally NAMED `${VAR}` in whatever directory it was launched from.
|
|
42
|
+
* Nothing failed. The charts were simply somewhere nobody would look.
|
|
43
|
+
*
|
|
44
|
+
* The sibling failure is worse and is why this is fatal rather than a warning:
|
|
45
|
+
* had the variable been set to a path that does not exist, this would have
|
|
46
|
+
* started CLEAN and EMPTY, and the caller would have reported its whole store
|
|
47
|
+
* lost. A store is the one input where "start anyway" is never the kind answer.
|
|
48
|
+
*
|
|
49
|
+
* `${` cannot appear in a legitimate path here: `$` is legal in a filename, but
|
|
50
|
+
* a caller writing `${...}` is quoting a shell it expected to have run.
|
|
51
|
+
*/
|
|
52
|
+
if (memoryFilePath.includes('${')) {
|
|
53
|
+
throw new Error(`Refusing to open memory store: the path contains an unexpanded shell variable.\n` +
|
|
54
|
+
` got: ${memoryFilePath}\n` +
|
|
55
|
+
` The variable was not set in the environment that launched this process, so this ` +
|
|
56
|
+
`would create a file with that literal name and write your charts into it where ` +
|
|
57
|
+
`nothing will find them.\n` +
|
|
58
|
+
` Fix the environment (or pass a real path) and start again — no store was touched.`);
|
|
59
|
+
}
|
|
12
60
|
this.memoryFilePath = memoryFilePath;
|
|
13
61
|
}
|
|
14
62
|
// ─── JSONL Storage ──────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Read the store through the preserving parser, which keeps every line's raw
|
|
65
|
+
* record on the graph so a later write can round-trip fields this package does
|
|
66
|
+
* not model. See jsonl-preservation.ts.
|
|
67
|
+
*/
|
|
15
68
|
async loadGraph() {
|
|
16
|
-
|
|
17
|
-
const data = await fs.readFile(this.memoryFilePath, 'utf-8');
|
|
18
|
-
const lines = data.split('\n').filter(line => line.trim() !== '');
|
|
19
|
-
return lines.reduce((graph, line) => {
|
|
20
|
-
const item = JSON.parse(line);
|
|
21
|
-
if (item.type === 'entity')
|
|
22
|
-
graph.entities.push(item);
|
|
23
|
-
if (item.type === 'relation')
|
|
24
|
-
graph.relations.push(item);
|
|
25
|
-
// Support narrative_beat entities stored in legacy format
|
|
26
|
-
if (item.type === 'narrative_beat') {
|
|
27
|
-
const narrativeBeat = {
|
|
28
|
-
name: item.name,
|
|
29
|
-
entityType: 'narrative_beat',
|
|
30
|
-
observations: item.observations || [],
|
|
31
|
-
metadata: {
|
|
32
|
-
...item.metadata,
|
|
33
|
-
narrative: item.narrative,
|
|
34
|
-
relationalAlignment: item.relational_alignment,
|
|
35
|
-
fourDirections: item.four_directions,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
graph.entities.push(narrativeBeat);
|
|
39
|
-
}
|
|
40
|
-
return graph;
|
|
41
|
-
}, { entities: [], relations: [] });
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
45
|
-
return { entities: [], relations: [] };
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
69
|
+
return readJsonlMemoryFile(this.memoryFilePath);
|
|
49
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Write the store atomically and without loss. The previous implementation
|
|
73
|
+
* re-serialised only the fields this package knows, so any consumer's metadata
|
|
74
|
+
* on a record was dropped the first time coaiajs touched it.
|
|
75
|
+
*/
|
|
50
76
|
async saveGraph(graph) {
|
|
51
|
-
|
|
52
|
-
...graph.entities.map(e => JSON.stringify({ type: 'entity', ...e })),
|
|
53
|
-
...graph.relations.map(r => JSON.stringify({ type: 'relation', ...r })),
|
|
54
|
-
];
|
|
55
|
-
await fs.writeFile(this.memoryFilePath, lines.join('\n'));
|
|
77
|
+
await writeJsonlMemoryFile(this.memoryFilePath, graph);
|
|
56
78
|
}
|
|
57
79
|
// ─── Knowledge Graph CRUD ───────────────────────────────────────────
|
|
58
80
|
async createEntities(entities) {
|
|
81
|
+
// Every chart body eventually lands here. Check before loading the graph, so a
|
|
82
|
+
// refusal leaves the store exactly as it was.
|
|
83
|
+
entities.forEach((entity, index) => assertNoUnparsedCallSyntax(entity.observations, `entities[${index}].observations`));
|
|
59
84
|
const graph = await this.loadGraph();
|
|
60
85
|
const newEntities = entities.filter(e => !graph.entities.some(existing => existing.name === e.name));
|
|
61
86
|
graph.entities.push(...newEntities);
|
|
@@ -72,6 +97,9 @@ export class KnowledgeGraphManager {
|
|
|
72
97
|
return newRelations;
|
|
73
98
|
}
|
|
74
99
|
async addObservations(observations) {
|
|
100
|
+
// All of them, before any of them — a batch where one body is malformed is a
|
|
101
|
+
// malformed call, and half of it must not land.
|
|
102
|
+
observations.forEach((o, index) => assertNoUnparsedCallSyntax(o.contents, `observations[${index}].contents`));
|
|
75
103
|
const graph = await this.loadGraph();
|
|
76
104
|
const results = observations.map(o => {
|
|
77
105
|
const entity = graph.entities.find(e => e.name === o.entityName);
|
|
@@ -111,6 +139,20 @@ export class KnowledgeGraphManager {
|
|
|
111
139
|
async readGraph() {
|
|
112
140
|
return this.loadGraph();
|
|
113
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Project a chart (or any entity) into the GitHub Project field shape the
|
|
144
|
+
* coaia-github bridge writes. Returns null when nothing by that name or chartId
|
|
145
|
+
* is in the store.
|
|
146
|
+
*/
|
|
147
|
+
async getGithubProjectFieldProjection(entityNameOrChartId) {
|
|
148
|
+
const graph = await this.loadGraph();
|
|
149
|
+
const entity = graph.entities.find(e => e.name === entityNameOrChartId) ??
|
|
150
|
+
graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
|
|
151
|
+
e.metadata?.chartId === entityNameOrChartId);
|
|
152
|
+
if (!entity)
|
|
153
|
+
return null;
|
|
154
|
+
return createGithubProjectFieldProjection(entity, graph);
|
|
155
|
+
}
|
|
114
156
|
async searchNodes(query) {
|
|
115
157
|
const graph = await this.loadGraph();
|
|
116
158
|
const q = query.toLowerCase();
|
|
@@ -155,10 +197,39 @@ export class KnowledgeGraphManager {
|
|
|
155
197
|
return this.getChartDetails(telescopedOutcomeEntity.metadata.chartId);
|
|
156
198
|
}
|
|
157
199
|
// ─── Structural Tension Charts ──────────────────────────────────────
|
|
158
|
-
async createStructuralTensionChart(desiredOutcome, currentReality, dueDate, actionSteps, elementsOfPerformance) {
|
|
159
|
-
//
|
|
200
|
+
async createStructuralTensionChart(desiredOutcome, currentReality, dueDate, actionSteps, elementsOfPerformance, githubIssue) {
|
|
201
|
+
// A malformed call is diagnosed before its content is judged — there is no
|
|
202
|
+
// point coaching the creative orientation of a fragment of XML.
|
|
203
|
+
assertNoUnparsedCallSyntax(desiredOutcome, 'desiredOutcome');
|
|
204
|
+
assertNoUnparsedCallSyntax(currentReality, 'currentReality');
|
|
205
|
+
assertNoUnparsedCallSyntax(actionSteps, 'actionSteps');
|
|
206
|
+
// Creative orientation validation.
|
|
207
|
+
//
|
|
208
|
+
// MATCHED ON WORD BOUNDARIES, NOT SUBSTRINGS, and the difference is not
|
|
209
|
+
// cosmetic. This filter used `.includes()`, so it read the letters of these
|
|
210
|
+
// words wherever they fell inside a longer one:
|
|
211
|
+
//
|
|
212
|
+
// "a fixed ladder of named rungs" -> "fix" -> refused
|
|
213
|
+
// "carry one to completion or resolve it" -> "solve" -> refused
|
|
214
|
+
// "unstoppable", "prevention", "removal", "avoidance", "dissolve",
|
|
215
|
+
// "stopgap", "reducer", "solvent", "prefix", "suffix", "affix"…
|
|
216
|
+
//
|
|
217
|
+
// Both examples above are real refusals measured in the coaia-narrative
|
|
218
|
+
// lineage (2026-07-31 and 2026-08-01) on action steps already written in
|
|
219
|
+
// creative orientation. The author rephrased around the error both times
|
|
220
|
+
// without asking why, which is the quiet cost: a guard that fires on innocent
|
|
221
|
+
// text teaches the caller to edit for the checker rather than for the reader.
|
|
222
|
+
//
|
|
223
|
+
// Sharper still, the organisation's own guidance prescribes the very word this
|
|
224
|
+
// filter rejected: "GOOD EXAMPLE: Each action step resolve tension between
|
|
225
|
+
// current and desired states." So the check refused the phrasing its own
|
|
226
|
+
// doctrine recommends.
|
|
227
|
+
//
|
|
228
|
+
// The word list is unchanged — a chart whose desired outcome genuinely says
|
|
229
|
+
// "eliminate" or "reduce" should still be met with the teaching below. Only
|
|
230
|
+
// the matching is corrected.
|
|
160
231
|
const problemSolvingWords = ['fix', 'solve', 'eliminate', 'prevent', 'stop', 'avoid', 'reduce', 'remove'];
|
|
161
|
-
const detectedProblemWords = problemSolvingWords.filter(word =>
|
|
232
|
+
const detectedProblemWords = problemSolvingWords.filter(word => new RegExp(`\\b${word}\\b`, 'i').test(desiredOutcome));
|
|
162
233
|
if (detectedProblemWords.length > 0) {
|
|
163
234
|
throw new Error(`🌊 CREATIVE ORIENTATION REQUIRED\n\n` +
|
|
164
235
|
`Desired Outcome: "${desiredOutcome}"\n\n` +
|
|
@@ -193,6 +264,7 @@ export class KnowledgeGraphManager {
|
|
|
193
264
|
}
|
|
194
265
|
const chartId = `chart_${Date.now()}`;
|
|
195
266
|
const timestamp = new Date().toISOString();
|
|
267
|
+
const issueRef = githubIssue ? parseGithubIssueSpec(githubIssue) : undefined;
|
|
196
268
|
const entities = [
|
|
197
269
|
{
|
|
198
270
|
name: `${chartId}_chart`,
|
|
@@ -207,6 +279,7 @@ export class KnowledgeGraphManager {
|
|
|
207
279
|
...(elementsOfPerformance && elementsOfPerformance.length > 0
|
|
208
280
|
? { elementsOfPerformance }
|
|
209
281
|
: {}),
|
|
282
|
+
...(issueRef ? { github: { issue: issueRef } } : {}),
|
|
210
283
|
},
|
|
211
284
|
},
|
|
212
285
|
{
|
|
@@ -358,15 +431,42 @@ export class KnowledgeGraphManager {
|
|
|
358
431
|
async getChartProgress(chartId, preloadedGraph) {
|
|
359
432
|
const graph = preloadedGraph ?? (await this.loadGraph());
|
|
360
433
|
const actionSteps = graph.entities.filter(e => e.entityType === 'action_step' && e.metadata?.chartId === chartId);
|
|
361
|
-
|
|
362
|
-
|
|
434
|
+
// A chart holds its work in two shapes: action_step entities on the chart
|
|
435
|
+
// itself, and telescoped child charts. Counting only the first makes a chart
|
|
436
|
+
// whose steps were telescoped report 0/0 while holding real work.
|
|
437
|
+
const childCharts = graph.entities.filter(e => e.entityType === 'structural_tension_chart' &&
|
|
438
|
+
e.metadata?.parentChart === chartId);
|
|
439
|
+
// A child chart telescoped out of one of this chart's own steps is that same
|
|
440
|
+
// result seen closer up — it is counted through the step, not a second time.
|
|
441
|
+
const stepNames = new Set(actionSteps.map(e => e.name));
|
|
442
|
+
const telescopedChildren = childCharts.filter(c => !(c.metadata?.parentActionStep && stepNames.has(c.metadata.parentActionStep)));
|
|
443
|
+
const units = [
|
|
444
|
+
...actionSteps.map(e => ({
|
|
445
|
+
name: e.name,
|
|
446
|
+
complete: e.metadata?.completionStatus === true,
|
|
447
|
+
dueDate: e.metadata?.dueDate,
|
|
448
|
+
})),
|
|
449
|
+
...telescopedChildren.map(c => {
|
|
450
|
+
const childId = c.metadata?.chartId || c.name.replace('_chart', '');
|
|
451
|
+
// a child chart is completed through its desired outcome — that is the
|
|
452
|
+
// entity mark_action_complete writes to when a telescoped result is achieved
|
|
453
|
+
const outcome = graph.entities.find(e => e.name === `${childId}_desired_outcome` && e.entityType === 'desired_outcome');
|
|
454
|
+
return {
|
|
455
|
+
name: `${childId}_desired_outcome`,
|
|
456
|
+
complete: outcome?.metadata?.completionStatus === true,
|
|
457
|
+
dueDate: c.metadata?.dueDate,
|
|
458
|
+
};
|
|
459
|
+
}),
|
|
460
|
+
];
|
|
461
|
+
const completedActions = units.filter(u => u.complete).length;
|
|
462
|
+
const totalActions = units.length;
|
|
363
463
|
const progress = totalActions > 0 ? completedActions / totalActions : 0;
|
|
364
|
-
// Next incomplete
|
|
365
|
-
const incompleteActions =
|
|
366
|
-
.filter(
|
|
464
|
+
// Next incomplete unit by earliest due date
|
|
465
|
+
const incompleteActions = units
|
|
466
|
+
.filter(u => !u.complete)
|
|
367
467
|
.sort((a, b) => {
|
|
368
|
-
const dateA = new Date(a.
|
|
369
|
-
const dateB = new Date(b.
|
|
468
|
+
const dateA = new Date(a.dueDate || '').getTime();
|
|
469
|
+
const dateB = new Date(b.dueDate || '').getTime();
|
|
370
470
|
return dateA - dateB;
|
|
371
471
|
});
|
|
372
472
|
const chart = graph.entities.find(e => e.name === `${chartId}_chart`);
|
|
@@ -388,6 +488,18 @@ export class KnowledgeGraphManager {
|
|
|
388
488
|
const progress = await this.getChartProgress(chartId, graph);
|
|
389
489
|
const desiredOutcome = graph.entities.find(e => e.name === `${chartId}_desired_outcome` &&
|
|
390
490
|
e.entityType === 'desired_outcome');
|
|
491
|
+
// A chart holds its work in two shapes: action_step entities that live on
|
|
492
|
+
// the chart itself, and telescoped child charts. Callers rendering a
|
|
493
|
+
// hierarchy from parentChart alone see only the second and report the
|
|
494
|
+
// first as absent.
|
|
495
|
+
const actionSteps = graph.entities
|
|
496
|
+
.filter(e => e.entityType === 'action_step' && e.metadata?.chartId === chartId)
|
|
497
|
+
.map(e => ({
|
|
498
|
+
name: e.name,
|
|
499
|
+
title: e.observations[0] || 'Untitled action step',
|
|
500
|
+
complete: Boolean(e.metadata?.completionStatus),
|
|
501
|
+
dueDate: e.metadata?.dueDate,
|
|
502
|
+
}));
|
|
391
503
|
return {
|
|
392
504
|
chartId,
|
|
393
505
|
desiredOutcome: desiredOutcome?.observations[0] || 'Unknown outcome',
|
|
@@ -397,6 +509,8 @@ export class KnowledgeGraphManager {
|
|
|
397
509
|
totalActions: progress.totalActions,
|
|
398
510
|
level: chart.metadata?.level || 0,
|
|
399
511
|
parentChart: chart.metadata?.parentChart,
|
|
512
|
+
parentActionStep: chart.metadata?.parentActionStep,
|
|
513
|
+
actionSteps,
|
|
400
514
|
};
|
|
401
515
|
}));
|
|
402
516
|
return chartSummaries.sort((a, b) => {
|
|
@@ -409,6 +523,7 @@ export class KnowledgeGraphManager {
|
|
|
409
523
|
}
|
|
410
524
|
// ─── Update Action Progress ─────────────────────────────────────────
|
|
411
525
|
async updateActionProgress(actionStepName, progressObservation, updateCurrentReality) {
|
|
526
|
+
assertNoUnparsedCallSyntax(progressObservation, 'progressObservation');
|
|
412
527
|
const graph = await this.loadGraph();
|
|
413
528
|
const actionStep = graph.entities.find(e => e.name === actionStepName &&
|
|
414
529
|
(e.entityType === 'action_step' || e.entityType === 'desired_outcome'));
|
|
@@ -439,6 +554,7 @@ export class KnowledgeGraphManager {
|
|
|
439
554
|
}
|
|
440
555
|
// ─── Update Current Reality ─────────────────────────────────────────
|
|
441
556
|
async updateCurrentReality(chartId, newObservations) {
|
|
557
|
+
assertNoUnparsedCallSyntax(newObservations, 'newObservations');
|
|
442
558
|
const graph = await this.loadGraph();
|
|
443
559
|
const currentReality = graph.entities.find(e => e.name === `${chartId}_current_reality` &&
|
|
444
560
|
e.entityType === 'current_reality');
|
|
@@ -453,7 +569,27 @@ export class KnowledgeGraphManager {
|
|
|
453
569
|
await this.saveGraph(graph);
|
|
454
570
|
}
|
|
455
571
|
// ─── Update Desired Outcome ─────────────────────────────────────────
|
|
572
|
+
/**
|
|
573
|
+
* Record on an existing chart the GitHub issue it was written from.
|
|
574
|
+
* Writes metadata.github.issue, which EntityMetadata already carried and no
|
|
575
|
+
* tool could reach.
|
|
576
|
+
*/
|
|
577
|
+
async linkChartToGithubIssue(chartId, githubIssue) {
|
|
578
|
+
const graph = await this.loadGraph();
|
|
579
|
+
const chartEntity = graph.entities.find(e => e.name === `${chartId}_chart` && e.entityType === 'structural_tension_chart');
|
|
580
|
+
if (!chartEntity) {
|
|
581
|
+
throw new Error(`Chart ${chartId} not found`);
|
|
582
|
+
}
|
|
583
|
+
const issue = parseGithubIssueSpec(githubIssue);
|
|
584
|
+
const timestamp = new Date().toISOString();
|
|
585
|
+
chartEntity.metadata = chartEntity.metadata || {};
|
|
586
|
+
chartEntity.metadata.github = { ...(chartEntity.metadata.github || {}), issue };
|
|
587
|
+
chartEntity.metadata.updatedAt = timestamp;
|
|
588
|
+
await this.saveGraph(graph);
|
|
589
|
+
return { chartId, issue };
|
|
590
|
+
}
|
|
456
591
|
async updateDesiredOutcome(chartId, newDesiredOutcome) {
|
|
592
|
+
assertNoUnparsedCallSyntax(newDesiredOutcome, 'newDesiredOutcome');
|
|
457
593
|
const graph = await this.loadGraph();
|
|
458
594
|
const desiredOutcomeEntity = graph.entities.find(e => e.name === `${chartId}_desired_outcome` &&
|
|
459
595
|
e.entityType === 'desired_outcome');
|
|
@@ -466,8 +602,119 @@ export class KnowledgeGraphManager {
|
|
|
466
602
|
}
|
|
467
603
|
await this.saveGraph(graph);
|
|
468
604
|
}
|
|
605
|
+
// ─── Chart Due Date ─────────────────────────────────────────────────
|
|
606
|
+
/**
|
|
607
|
+
* Move the date a chart is due.
|
|
608
|
+
*
|
|
609
|
+
* `updateDesiredOutcome` and `updateCurrentReality` could already reach both
|
|
610
|
+
* halves of the tension; the date the tension resolves by could only be changed
|
|
611
|
+
* by hand-editing the JSONL — on a store several MCP instances write with no
|
|
612
|
+
* lock, which is how a hand-edit becomes a lost write.
|
|
613
|
+
*
|
|
614
|
+
* The chart and its desired outcome were created carrying the same date and are
|
|
615
|
+
* moved together. Action step dates are left alone unless `redistributeActionSteps`
|
|
616
|
+
* is asked for; the count still standing past the new date is reported either way,
|
|
617
|
+
* so the caller sees what the move left behind.
|
|
618
|
+
*/
|
|
619
|
+
async updateChartDueDate(chartId, newDueDate, redistributeActionSteps = false) {
|
|
620
|
+
const parsed = Date.parse(newDueDate);
|
|
621
|
+
if (Number.isNaN(parsed)) {
|
|
622
|
+
throw new Error(`Invalid due date: "${newDueDate}". Use an ISO date string, e.g. 2026-09-30T12:00:00Z`);
|
|
623
|
+
}
|
|
624
|
+
const normalizedDueDate = new Date(parsed).toISOString();
|
|
625
|
+
const graph = await this.loadGraph();
|
|
626
|
+
const chartEntity = graph.entities.find(e => e.name === `${chartId}_chart` && e.entityType === 'structural_tension_chart');
|
|
627
|
+
if (!chartEntity) {
|
|
628
|
+
throw new Error(`Chart ${chartId} not found`);
|
|
629
|
+
}
|
|
630
|
+
const timestamp = new Date().toISOString();
|
|
631
|
+
chartEntity.metadata = chartEntity.metadata || {};
|
|
632
|
+
const previousDueDate = typeof chartEntity.metadata.dueDate === 'string' ? chartEntity.metadata.dueDate : undefined;
|
|
633
|
+
chartEntity.metadata.dueDate = normalizedDueDate;
|
|
634
|
+
chartEntity.metadata.updatedAt = timestamp;
|
|
635
|
+
const desiredOutcomeEntity = graph.entities.find(e => e.name === `${chartId}_desired_outcome` && e.entityType === 'desired_outcome');
|
|
636
|
+
if (desiredOutcomeEntity) {
|
|
637
|
+
desiredOutcomeEntity.metadata = desiredOutcomeEntity.metadata || {};
|
|
638
|
+
desiredOutcomeEntity.metadata.dueDate = normalizedDueDate;
|
|
639
|
+
desiredOutcomeEntity.metadata.updatedAt = timestamp;
|
|
640
|
+
}
|
|
641
|
+
// The date a chart carried is part of its history. Recording the move on the
|
|
642
|
+
// chart itself keeps it readable later without touching current reality, which
|
|
643
|
+
// belongs to the work rather than to the schedule.
|
|
644
|
+
chartEntity.observations.push(`Due date changed from ${previousDueDate || 'unset'} to ${normalizedDueDate} on ${timestamp}`);
|
|
645
|
+
const units = this.collectChartWorkUnits(graph, chartId);
|
|
646
|
+
const openUnits = units.filter(u => !u.complete);
|
|
647
|
+
let actionStepsRescheduled = 0;
|
|
648
|
+
if (redistributeActionSteps && openUnits.length > 0) {
|
|
649
|
+
openUnits.sort((a, b) => {
|
|
650
|
+
const dateA = new Date(a.dueDate || normalizedDueDate).getTime();
|
|
651
|
+
const dateB = new Date(b.dueDate || normalizedDueDate).getTime();
|
|
652
|
+
return dateA - dateB;
|
|
653
|
+
});
|
|
654
|
+
const dates = this.distributeActionStepDates(new Date(timestamp), new Date(normalizedDueDate), openUnits.length);
|
|
655
|
+
openUnits.forEach((unit, index) => {
|
|
656
|
+
const moved = dates[index].toISOString();
|
|
657
|
+
unit.entities.forEach(entity => {
|
|
658
|
+
entity.metadata = entity.metadata || {};
|
|
659
|
+
entity.metadata.dueDate = moved;
|
|
660
|
+
entity.metadata.updatedAt = timestamp;
|
|
661
|
+
});
|
|
662
|
+
unit.dueDate = moved;
|
|
663
|
+
});
|
|
664
|
+
actionStepsRescheduled = openUnits.length;
|
|
665
|
+
}
|
|
666
|
+
const actionStepsPastDueDate = openUnits.filter(u => u.dueDate ? new Date(u.dueDate).getTime() > parsed : false).length;
|
|
667
|
+
await this.saveGraph(graph);
|
|
668
|
+
return {
|
|
669
|
+
chartId,
|
|
670
|
+
previousDueDate,
|
|
671
|
+
newDueDate: normalizedDueDate,
|
|
672
|
+
actionStepsRescheduled,
|
|
673
|
+
actionStepsPastDueDate,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* A chart's work in the two shapes it takes: action_step entities on the chart
|
|
678
|
+
* itself, and telescoped child charts. A child telescoped out of one of the
|
|
679
|
+
* chart's own steps is that same result seen closer up — it travels with the
|
|
680
|
+
* step rather than counting as a second unit.
|
|
681
|
+
*/
|
|
682
|
+
collectChartWorkUnits(graph, chartId) {
|
|
683
|
+
const ownSteps = graph.entities.filter(e => e.entityType === 'action_step' && e.metadata?.chartId === chartId);
|
|
684
|
+
const childCharts = graph.entities.filter(e => e.entityType === 'structural_tension_chart' && e.metadata?.parentChart === chartId);
|
|
685
|
+
const outcomeOf = (chart) => {
|
|
686
|
+
const childId = chart.metadata?.chartId || chart.name.replace('_chart', '');
|
|
687
|
+
return graph.entities.find(e => e.name === `${childId}_desired_outcome` && e.entityType === 'desired_outcome');
|
|
688
|
+
};
|
|
689
|
+
const stepNames = new Set(ownSteps.map(e => e.name));
|
|
690
|
+
const units = ownSteps.map(step => {
|
|
691
|
+
const grown = childCharts.filter(c => c.metadata?.parentActionStep === step.name);
|
|
692
|
+
const entities = [
|
|
693
|
+
step,
|
|
694
|
+
...grown.flatMap(c => [c, outcomeOf(c)].filter((e) => Boolean(e))),
|
|
695
|
+
];
|
|
696
|
+
return {
|
|
697
|
+
entities,
|
|
698
|
+
complete: step.metadata?.completionStatus === true,
|
|
699
|
+
dueDate: typeof step.metadata?.dueDate === 'string' ? step.metadata.dueDate : undefined,
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
childCharts
|
|
703
|
+
.filter(c => !(c.metadata?.parentActionStep && stepNames.has(c.metadata.parentActionStep)))
|
|
704
|
+
.forEach(chart => {
|
|
705
|
+
const outcome = outcomeOf(chart);
|
|
706
|
+
units.push({
|
|
707
|
+
entities: [chart, outcome].filter((e) => Boolean(e)),
|
|
708
|
+
complete: outcome?.metadata?.completionStatus === true,
|
|
709
|
+
dueDate: typeof chart.metadata?.dueDate === 'string' ? chart.metadata.dueDate : undefined,
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
return units;
|
|
713
|
+
}
|
|
469
714
|
// ─── MMOT Evaluation ────────────────────────────────────────────────
|
|
470
715
|
async performMmotEvaluation(chartId, phase = 'full', assessment, direction, correctiveActions, updateReality = true) {
|
|
716
|
+
assertNoUnparsedCallSyntax(assessment, 'assessment');
|
|
717
|
+
assertNoUnparsedCallSyntax(correctiveActions, 'correctiveActions');
|
|
471
718
|
const graph = await this.loadGraph();
|
|
472
719
|
const chartEntity = graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
|
|
473
720
|
e.metadata?.chartId === chartId);
|
|
@@ -536,11 +783,17 @@ export class KnowledgeGraphManager {
|
|
|
536
783
|
const guidance = phaseGuidance[phase] || phaseGuidance.full || phaseGuidance.acknowledge;
|
|
537
784
|
let evaluationStored = false;
|
|
538
785
|
let beatEmitted = false;
|
|
539
|
-
// Store evaluation observations
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
786
|
+
// Store evaluation observations.
|
|
787
|
+
//
|
|
788
|
+
// updateReality governs ONE record — the append into current reality — which is
|
|
789
|
+
// exactly what the tool declares it to mean ("whether to write evaluation
|
|
790
|
+
// observations into current reality"). The chart's own mmotEvaluations[] trail is
|
|
791
|
+
// a separate record and is written whenever an assessment was made. Gating both on
|
|
792
|
+
// one flag meant a caller who only asked to leave current reality alone lost the
|
|
793
|
+
// evaluation entirely, while still being handed a full phase response.
|
|
794
|
+
if (assessment) {
|
|
795
|
+
if (updateReality && currentReality) {
|
|
796
|
+
currentReality.observations.push(`[MMOT ${phase}${directionLabel}] ${assessment}`);
|
|
544
797
|
if (currentReality.metadata) {
|
|
545
798
|
currentReality.metadata.updatedAt = timestamp;
|
|
546
799
|
}
|
|
@@ -711,6 +964,143 @@ export class KnowledgeGraphManager {
|
|
|
711
964
|
return beats;
|
|
712
965
|
}
|
|
713
966
|
// ─── Add Action Step (telescoped chart) ─────────────────────────────
|
|
967
|
+
// ─── Wampum Belt Sequencing ─────────────────────────────────────────
|
|
968
|
+
// Runs in parallel with linear narrative beats: a grid of beads, each holding a
|
|
969
|
+
// mnemonic and its readings, optionally tied to a chart or beat by ceremony.
|
|
970
|
+
async createWampumBelt(title, purpose, rows = 1, cols = 1) {
|
|
971
|
+
if (!Number.isInteger(rows) || rows <= 0 || !Number.isInteger(cols) || cols <= 0) {
|
|
972
|
+
throw new Error(`rows and cols must be positive integers (received rows=${rows}, cols=${cols})`);
|
|
973
|
+
}
|
|
974
|
+
const beltId = `belt_${Date.now()}`;
|
|
975
|
+
const timestamp = new Date().toISOString();
|
|
976
|
+
const beltMetadata = {
|
|
977
|
+
beltId,
|
|
978
|
+
title,
|
|
979
|
+
purpose,
|
|
980
|
+
rows,
|
|
981
|
+
cols,
|
|
982
|
+
beads: [],
|
|
983
|
+
createdAt: timestamp,
|
|
984
|
+
updatedAt: timestamp,
|
|
985
|
+
};
|
|
986
|
+
const entity = {
|
|
987
|
+
name: `${beltId}_belt`,
|
|
988
|
+
entityType: 'wampum_belt',
|
|
989
|
+
observations: [`Wampum Belt created: ${title}`, `Purpose: ${purpose}`],
|
|
990
|
+
metadata: {
|
|
991
|
+
beltId,
|
|
992
|
+
createdAt: timestamp,
|
|
993
|
+
updatedAt: timestamp,
|
|
994
|
+
wampumBelt: beltMetadata,
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
await this.createEntities([entity]);
|
|
998
|
+
return { beltId, entity };
|
|
999
|
+
}
|
|
1000
|
+
async addWampumBead(beltId, mnemonic, color, position, reading, relationalReadings, ceremonyLink, observations = []) {
|
|
1001
|
+
const graph = await this.loadGraph();
|
|
1002
|
+
const beltEntity = graph.entities.find(e => e.entityType === 'wampum_belt' && e.metadata?.beltId === beltId);
|
|
1003
|
+
if (!beltEntity?.metadata?.wampumBelt) {
|
|
1004
|
+
throw new Error(`Wampum Belt not found: ${beltId}`);
|
|
1005
|
+
}
|
|
1006
|
+
const beltMeta = beltEntity.metadata.wampumBelt;
|
|
1007
|
+
if (!Number.isInteger(position.row) ||
|
|
1008
|
+
!Number.isInteger(position.col) ||
|
|
1009
|
+
position.row < 0 ||
|
|
1010
|
+
position.col < 0 ||
|
|
1011
|
+
position.row >= beltMeta.rows ||
|
|
1012
|
+
position.col >= beltMeta.cols) {
|
|
1013
|
+
throw new Error(`Position (${position.row},${position.col}) out of bounds for belt ${beltId} (${beltMeta.rows}x${beltMeta.cols})`);
|
|
1014
|
+
}
|
|
1015
|
+
const conflict = beltMeta.beads.find(b => b.position.row === position.row && b.position.col === position.col);
|
|
1016
|
+
if (conflict) {
|
|
1017
|
+
throw new Error(`Position (${position.row},${position.col}) already occupied by bead "${conflict.mnemonic}"`);
|
|
1018
|
+
}
|
|
1019
|
+
const timestamp = new Date().toISOString();
|
|
1020
|
+
const bead = {
|
|
1021
|
+
id: `bead_${beltId}_${position.row}_${position.col}`,
|
|
1022
|
+
mnemonic,
|
|
1023
|
+
color,
|
|
1024
|
+
position,
|
|
1025
|
+
reading,
|
|
1026
|
+
...(relationalReadings ? { relationalReadings } : {}),
|
|
1027
|
+
...(ceremonyLink ? { ceremonyLink } : {}),
|
|
1028
|
+
observations,
|
|
1029
|
+
createdAt: timestamp,
|
|
1030
|
+
};
|
|
1031
|
+
beltMeta.beads.push(bead);
|
|
1032
|
+
beltMeta.updatedAt = timestamp;
|
|
1033
|
+
beltEntity.metadata.updatedAt = timestamp;
|
|
1034
|
+
// Ceremony edges are subject to the BEAD, not the belt.
|
|
1035
|
+
//
|
|
1036
|
+
// A relation's identity in the store is (from, to, relationType) — see
|
|
1037
|
+
// jsonl-preservation.ts. With the belt as subject, a second bead linking the
|
|
1038
|
+
// same chart under a different ceremonyType collided on that triple: the
|
|
1039
|
+
// existence check below matched, the push was skipped, and the bead kept a
|
|
1040
|
+
// ceremonyType the graph had no edge for. Silent discard — the bead's own
|
|
1041
|
+
// record disagreed with the graph, and nothing said so.
|
|
1042
|
+
//
|
|
1043
|
+
// bead.id is `bead_${beltId}_${row}_${col}` and a position can be written only
|
|
1044
|
+
// once, so (bead, target, relationType) is unique by construction and every
|
|
1045
|
+
// ceremony link survives with its own ceremonyType.
|
|
1046
|
+
if (ceremonyLink?.chartId) {
|
|
1047
|
+
const from = bead.id;
|
|
1048
|
+
const to = `${ceremonyLink.chartId}_chart`;
|
|
1049
|
+
const exists = graph.relations.some(r => r.from === from && r.to === to && r.relationType === 'wampum_holds_accountable');
|
|
1050
|
+
if (!exists) {
|
|
1051
|
+
graph.relations.push({
|
|
1052
|
+
from,
|
|
1053
|
+
to,
|
|
1054
|
+
relationType: 'wampum_holds_accountable',
|
|
1055
|
+
metadata: { createdAt: timestamp, context: ceremonyLink.ceremonyType, beltId },
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (ceremonyLink?.beatName) {
|
|
1060
|
+
const from = bead.id;
|
|
1061
|
+
const to = ceremonyLink.beatName;
|
|
1062
|
+
const exists = graph.relations.some(r => r.from === from && r.to === to && r.relationType === 'wampum_witnesses');
|
|
1063
|
+
if (!exists) {
|
|
1064
|
+
graph.relations.push({
|
|
1065
|
+
from,
|
|
1066
|
+
to,
|
|
1067
|
+
relationType: 'wampum_witnesses',
|
|
1068
|
+
metadata: { createdAt: timestamp, beltId },
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
await this.saveGraph(graph);
|
|
1073
|
+
return { bead };
|
|
1074
|
+
}
|
|
1075
|
+
async readWampumBelt(beltId, position) {
|
|
1076
|
+
const graph = await this.loadGraph();
|
|
1077
|
+
const beltEntity = graph.entities.find(e => e.entityType === 'wampum_belt' && e.metadata?.beltId === beltId);
|
|
1078
|
+
if (!beltEntity?.metadata?.wampumBelt) {
|
|
1079
|
+
throw new Error(`Wampum Belt not found: ${beltId}`);
|
|
1080
|
+
}
|
|
1081
|
+
const belt = beltEntity.metadata.wampumBelt;
|
|
1082
|
+
if (!position) {
|
|
1083
|
+
return { belt };
|
|
1084
|
+
}
|
|
1085
|
+
if (!Number.isInteger(position.row) ||
|
|
1086
|
+
!Number.isInteger(position.col) ||
|
|
1087
|
+
position.row < 0 ||
|
|
1088
|
+
position.col < 0 ||
|
|
1089
|
+
position.row >= belt.rows ||
|
|
1090
|
+
position.col >= belt.cols) {
|
|
1091
|
+
throw new Error(`Position (${position.row},${position.col}) out of bounds for belt ${beltId} (${belt.rows}x${belt.cols})`);
|
|
1092
|
+
}
|
|
1093
|
+
const bead = belt.beads.find(b => b.position.row === position.row && b.position.col === position.col);
|
|
1094
|
+
if (!bead) {
|
|
1095
|
+
return { belt };
|
|
1096
|
+
}
|
|
1097
|
+
const colLabel = position.col === 0 ? 'left' : position.col === belt.cols - 1 ? 'right' : 'center';
|
|
1098
|
+
const positionalReading = bead.relationalReadings?.[`col:${position.col}`] ??
|
|
1099
|
+
bead.relationalReadings?.[`row:${position.row}`] ??
|
|
1100
|
+
bead.relationalReadings?.[colLabel] ??
|
|
1101
|
+
bead.reading;
|
|
1102
|
+
return { belt, bead, positionalReading };
|
|
1103
|
+
}
|
|
714
1104
|
async addActionStep(parentChartId, actionStepTitle, dueDate, currentReality, performanceElements) {
|
|
715
1105
|
const graph = await this.loadGraph();
|
|
716
1106
|
const parentChart = graph.entities.find(e => e.entityType === 'structural_tension_chart' &&
|