coaia-visualizer 1.6.1 → 1.6.3

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/app/page.tsx +4 -23
  3. package/components/chart-detail-editable.tsx +3 -0
  4. package/components/metadata-projections.tsx +208 -0
  5. package/lib/chart-editor.ts +13 -13
  6. package/lib/jsonl-parser.ts +55 -8
  7. package/lib/jsonl-preservation.ts +405 -0
  8. package/lib/types.ts +30 -0
  9. package/package.json +31 -1
  10. package/.dockerignore +0 -9
  11. package/COMPLETE_IMPLEMENTATION_REPORT.md +0 -215
  12. package/Dockerfile +0 -61
  13. package/Dockerfile.app +0 -50
  14. package/QUICK_START_MCP_TESTING.md +0 -236
  15. package/STC.md +0 -18
  16. package/STCGOAL.md +0 -0
  17. package/STCISSUE.md +0 -36
  18. package/STCKIN.md +0 -0
  19. package/STCMASTERY.md +0 -0
  20. package/STUDY_REPORT.md +0 -510
  21. package/direct-test.sh +0 -180
  22. package/docker-build-push.sh +0 -20
  23. package/docker-compose.test.yml +0 -69
  24. package/docker-entrypoint.sh +0 -27
  25. package/jgwill.coaia-visualizer-8--496dca71-d476-4ac9-ba9f-376add118dd8--260208.txt +0 -2612
  26. package/mcp/test_mcp/.gemini/settings.json +0 -18
  27. package/rispecs/README.md +0 -170
  28. package/rispecs/accountability-responsibility.rispec.md +0 -110
  29. package/rispecs/api-mcp-interface.spec.md +0 -287
  30. package/rispecs/app.spec.md +0 -364
  31. package/rispecs/chart-editing-workflow.spec.md +0 -297
  32. package/rispecs/chart-visualization-hierarchy.spec.md +0 -235
  33. package/rispecs/cli-mode.spec.md +0 -224
  34. package/rispecs/github-project-runtime-memory-integration.spec.md +0 -381
  35. package/rispecs/jsonl-parsing-data-types.spec.md +0 -243
  36. package/rispecs/narrative-beats-display.spec.md +0 -189
  37. package/rispecs/pde-integration.spec.md +0 -280
  38. package/rispecs/planning-integration.spec.md +0 -329
  39. package/rispecs/relation-graph-visualization.spec.md +0 -171
  40. package/rispecs/relation-to-mcp-structural-thinking.kin.md +0 -65
  41. package/rispecs/ui-component-library.spec.md +0 -258
  42. package/test-data/test-master.jsonl +0 -11
  43. package/test-scripts/README.md +0 -325
  44. package/test-scripts/run-all-tests.sh +0 -38
  45. package/test-scripts/test-01-basic-operations.sh +0 -87
  46. package/test-scripts/test-02-telescope-creation.sh +0 -91
  47. package/test-scripts/test-03-navigation.sh +0 -87
  48. package/test-scripts/test-global-cli.spec.ts +0 -220
  49. package/test-scripts/verify-global-cli.sh +0 -87
@@ -0,0 +1,405 @@
1
+ import type {
2
+ EntityRecord,
3
+ MetadataWarning,
4
+ ParsedData,
5
+ RawJSONLRecord,
6
+ RecordProjections,
7
+ RelationRecord,
8
+ } from "./types"
9
+
10
+ const UPSTREAM_ISSUE = "https://github.com/avadisabelle/coaia-narrative/issues/35"
11
+
12
+ const ENTITY_TOP_LEVEL_KEYS = new Set(["type", "name", "entityType", "observations", "metadata"])
13
+ const RELATION_TOP_LEVEL_KEYS = new Set(["type", "from", "to", "relationType", "metadata"])
14
+ const NARRATIVE_BEAT_TOP_LEVEL_KEYS = new Set([
15
+ ...ENTITY_TOP_LEVEL_KEYS,
16
+ "narrative",
17
+ "relational_alignment",
18
+ "four_directions",
19
+ ])
20
+ const MUTABLE_METADATA_KEYS = new Set([
21
+ "dueDate",
22
+ "chartId",
23
+ "phase",
24
+ "completionStatus",
25
+ "completedAt",
26
+ "parentChart",
27
+ "parentActionStep",
28
+ "level",
29
+ "createdAt",
30
+ "updatedAt",
31
+ "act",
32
+ "type_dramatic",
33
+ "universes",
34
+ "timestamp",
35
+ "elementsOfPerformance",
36
+ "mmotEvaluations",
37
+ "relationalAlignment",
38
+ "fourDirections",
39
+ "narrative",
40
+ "isTelescopedChart",
41
+ "telescopedChartId",
42
+ ])
43
+
44
+ function isObject(value: unknown): value is Record<string, any> {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value)
46
+ }
47
+
48
+ function cloneJson<T>(value: T): T {
49
+ return value === undefined ? value : JSON.parse(JSON.stringify(value))
50
+ }
51
+
52
+ function deepEqual(left: unknown, right: unknown): boolean {
53
+ return JSON.stringify(left) === JSON.stringify(right)
54
+ }
55
+
56
+ function removeUndefined<T extends Record<string, any>>(record: T): T {
57
+ for (const key of Object.keys(record)) {
58
+ if (record[key] === undefined) {
59
+ delete record[key]
60
+ }
61
+ }
62
+ return record
63
+ }
64
+
65
+ function relationKey(relation: Pick<RelationRecord, "from" | "to" | "relationType">): string {
66
+ return `${relation.from}\u0000${relation.to}\u0000${relation.relationType}`
67
+ }
68
+
69
+ export function isEntityLikeRecord(record: RawJSONLRecord): boolean {
70
+ return (record.type === "entity" || record.type === "narrative_beat") && typeof record.name === "string"
71
+ }
72
+
73
+ export function isRelationLikeRecord(record: RawJSONLRecord): boolean {
74
+ return (
75
+ record.type === "relation" &&
76
+ typeof record.from === "string" &&
77
+ typeof record.to === "string" &&
78
+ typeof record.relationType === "string"
79
+ )
80
+ }
81
+
82
+ export function normalizeJsonlRecord(record: RawJSONLRecord): EntityRecord | RelationRecord | null {
83
+ if (record.type === "entity" && typeof record.name === "string") {
84
+ return {
85
+ ...record,
86
+ type: "entity",
87
+ observations: Array.isArray(record.observations) ? record.observations : [],
88
+ metadata: isObject(record.metadata) ? record.metadata : {},
89
+ } as EntityRecord
90
+ }
91
+
92
+ if (record.type === "narrative_beat" && typeof record.name === "string") {
93
+ const metadata = {
94
+ ...(isObject(record.metadata) ? record.metadata : {}),
95
+ narrative: isObject(record.metadata) && record.metadata.narrative !== undefined
96
+ ? record.metadata.narrative
97
+ : record.narrative,
98
+ relationalAlignment: isObject(record.metadata) && record.metadata.relationalAlignment !== undefined
99
+ ? record.metadata.relationalAlignment
100
+ : record.relational_alignment,
101
+ fourDirections: isObject(record.metadata) && record.metadata.fourDirections !== undefined
102
+ ? record.metadata.fourDirections
103
+ : record.four_directions,
104
+ }
105
+
106
+ return removeUndefined({
107
+ ...record,
108
+ type: "entity",
109
+ name: record.name as string,
110
+ entityType: "narrative_beat",
111
+ observations: Array.isArray(record.observations) ? record.observations : [],
112
+ metadata: removeUndefined(metadata),
113
+ }) as EntityRecord
114
+ }
115
+
116
+ if (isRelationLikeRecord(record)) {
117
+ return {
118
+ ...record,
119
+ type: "relation",
120
+ metadata: isObject(record.metadata) ? record.metadata : {},
121
+ } as RelationRecord
122
+ }
123
+
124
+ return null
125
+ }
126
+
127
+ function mergePreservedMetadata(originalMetadata: unknown, currentMetadata: unknown): Record<string, any> {
128
+ const merged = {
129
+ ...(isObject(currentMetadata) ? currentMetadata : {}),
130
+ }
131
+
132
+ if (isObject(originalMetadata)) {
133
+ for (const [key, value] of Object.entries(originalMetadata)) {
134
+ if (!(key in merged) && (!MUTABLE_METADATA_KEYS.has(key) || key === "github")) {
135
+ merged[key] = cloneJson(value)
136
+ }
137
+ }
138
+ }
139
+
140
+ return merged
141
+ }
142
+
143
+ function serializeEntity(entity: EntityRecord, original?: RawJSONLRecord): RawJSONLRecord {
144
+ const metadata = mergePreservedMetadata(original?.metadata, entity.metadata)
145
+
146
+ if (original?.type === "narrative_beat") {
147
+ const record: RawJSONLRecord = {
148
+ ...original,
149
+ type: "narrative_beat",
150
+ name: entity.name,
151
+ observations: entity.observations,
152
+ metadata,
153
+ }
154
+
155
+ if ("entityType" in original) {
156
+ record.entityType = entity.entityType
157
+ }
158
+ if ("narrative" in original && entity.metadata?.narrative !== undefined) {
159
+ record.narrative = cloneJson(entity.metadata.narrative)
160
+ }
161
+ if ("relational_alignment" in original && entity.metadata?.relationalAlignment !== undefined) {
162
+ record.relational_alignment = cloneJson(entity.metadata.relationalAlignment)
163
+ }
164
+ if ("four_directions" in original && entity.metadata?.fourDirections !== undefined) {
165
+ record.four_directions = cloneJson(entity.metadata.fourDirections)
166
+ }
167
+
168
+ return removeUndefined(record)
169
+ }
170
+
171
+ return removeUndefined({
172
+ ...original,
173
+ ...entity,
174
+ type: "entity",
175
+ metadata,
176
+ })
177
+ }
178
+
179
+ function serializeRelation(relation: RelationRecord, original?: RawJSONLRecord): RawJSONLRecord {
180
+ return removeUndefined({
181
+ ...original,
182
+ ...relation,
183
+ type: "relation",
184
+ metadata: mergePreservedMetadata(original?.metadata, relation.metadata),
185
+ })
186
+ }
187
+
188
+ function findRecord(records: RawJSONLRecord[], key: string): RawJSONLRecord | undefined {
189
+ return records.find((record) => {
190
+ if (isEntityLikeRecord(record) && record.name === key) return true
191
+ if (isRelationLikeRecord(record)) return relationKey(record as RelationRecord) === key
192
+ return false
193
+ })
194
+ }
195
+
196
+ function validateMetadata(label: string, beforeMetadata: unknown, afterMetadata: unknown, failures: string[]): void {
197
+ if (!isObject(beforeMetadata)) return
198
+ if (!isObject(afterMetadata)) {
199
+ failures.push(`${label}: metadata object missing after write`)
200
+ return
201
+ }
202
+
203
+ for (const [key, value] of Object.entries(beforeMetadata)) {
204
+ if (!(key in afterMetadata)) {
205
+ failures.push(`${label}: metadata.${key} missing after write`)
206
+ continue
207
+ }
208
+
209
+ if ((key === "github" || !MUTABLE_METADATA_KEYS.has(key)) && !deepEqual(value, afterMetadata[key])) {
210
+ failures.push(`${label}: metadata.${key} changed unexpectedly`)
211
+ }
212
+ }
213
+ }
214
+
215
+ function validateRecordPreservation(
216
+ beforeRecords: RawJSONLRecord[],
217
+ afterRecords: RawJSONLRecord[],
218
+ entities: Map<string, EntityRecord>,
219
+ relations: RelationRecord[],
220
+ ): void {
221
+ const relationKeys = new Set(relations.map((relation) => relationKey(relation)))
222
+ const failures: string[] = []
223
+
224
+ for (const before of beforeRecords) {
225
+ if (isEntityLikeRecord(before)) {
226
+ const name = before.name as string
227
+ if (!entities.has(name)) continue
228
+
229
+ const after = findRecord(afterRecords, name)
230
+ if (!after) {
231
+ failures.push(`${name}: record missing after write`)
232
+ continue
233
+ }
234
+
235
+ if (before.type === "narrative_beat" && after.type !== "narrative_beat") {
236
+ failures.push(`${name}: legacy narrative_beat record type was flattened`)
237
+ }
238
+
239
+ const knownKeys = before.type === "narrative_beat" ? NARRATIVE_BEAT_TOP_LEVEL_KEYS : ENTITY_TOP_LEVEL_KEYS
240
+ for (const key of Object.keys(before)) {
241
+ if (!knownKeys.has(key) && !deepEqual(before[key], after[key])) {
242
+ failures.push(`${name}: top-level extension field "${key}" was not preserved`)
243
+ }
244
+ }
245
+ validateMetadata(name, before.metadata, after.metadata, failures)
246
+ }
247
+
248
+ if (isRelationLikeRecord(before)) {
249
+ const key = relationKey(before as RelationRecord)
250
+ if (!relationKeys.has(key)) continue
251
+
252
+ const after = findRecord(afterRecords, key)
253
+ if (!after) {
254
+ failures.push(`${key}: relation missing after write`)
255
+ continue
256
+ }
257
+
258
+ for (const topLevelKey of Object.keys(before)) {
259
+ if (!RELATION_TOP_LEVEL_KEYS.has(topLevelKey) && !deepEqual(before[topLevelKey], after[topLevelKey])) {
260
+ failures.push(`${key}: relation extension field "${topLevelKey}" was not preserved`)
261
+ }
262
+ }
263
+ validateMetadata(key, before.metadata, after.metadata, failures)
264
+ }
265
+ }
266
+
267
+ if (failures.length > 0) {
268
+ throw new Error(`COAIA JSONL metadata preservation failed:\n- ${failures.join("\n- ")}`)
269
+ }
270
+ }
271
+
272
+ export function buildRecordProjections(entities: Map<string, EntityRecord>): RecordProjections {
273
+ const allEntities = Array.from(entities.values())
274
+ return {
275
+ chartRecords: allEntities.filter((entity) => entity.entityType === "structural_tension_chart"),
276
+ actionStepRecords: allEntities.filter((entity) => entity.entityType === "action_step" || entity.metadata?.isTelescopedChart),
277
+ narrativeBeatRecords: allEntities.filter((entity) => entity.entityType === "narrative_beat"),
278
+ }
279
+ }
280
+
281
+ export function buildMetadataWarnings(records: RawJSONLRecord[]): MetadataWarning[] {
282
+ const warnings: MetadataWarning[] = []
283
+
284
+ for (const record of records) {
285
+ if (!isEntityLikeRecord(record) && !isRelationLikeRecord(record)) continue
286
+
287
+ const recordName = record.name || [record.from, record.to, record.relationType].filter(Boolean).join(" -> ")
288
+ const metadata = record.metadata
289
+
290
+ if (!isObject(metadata)) {
291
+ warnings.push({
292
+ severity: "warning",
293
+ recordName,
294
+ recordType: record.type,
295
+ message: "Record has no metadata object; provenance and extension fields may already be flattened.",
296
+ upstreamIssue: UPSTREAM_ISSUE,
297
+ })
298
+ continue
299
+ }
300
+
301
+ if (record.github && !metadata.github) {
302
+ warnings.push({
303
+ severity: "warning",
304
+ recordName,
305
+ recordType: record.type,
306
+ message: "GitHub provenance exists outside metadata.github; this shape is suspicious for preservation-aware tools.",
307
+ upstreamIssue: UPSTREAM_ISSUE,
308
+ })
309
+ }
310
+
311
+ const normalized = normalizeJsonlRecord(record)
312
+ if (normalized && "entityType" in normalized) {
313
+ if (
314
+ ["structural_tension_chart", "action_step", "desired_outcome", "current_reality", "narrative_beat"].includes(
315
+ normalized.entityType,
316
+ ) &&
317
+ !normalized.metadata?.chartId
318
+ ) {
319
+ warnings.push({
320
+ severity: "warning",
321
+ recordName: normalized.name,
322
+ recordType: normalized.entityType,
323
+ message: "COAIA entity is missing metadata.chartId, so chart/action projection may be incomplete.",
324
+ upstreamIssue: UPSTREAM_ISSUE,
325
+ })
326
+ }
327
+
328
+ if (
329
+ normalized.entityType === "narrative_beat" &&
330
+ !normalized.metadata?.narrative &&
331
+ !record.narrative &&
332
+ normalized.observations.length <= 1
333
+ ) {
334
+ warnings.push({
335
+ severity: "warning",
336
+ recordName: normalized.name,
337
+ recordType: normalized.entityType,
338
+ message: "Narrative beat appears flattened: no narrative metadata or rich beat payload is visible.",
339
+ upstreamIssue: UPSTREAM_ISSUE,
340
+ })
341
+ }
342
+ }
343
+
344
+ if (record.type === "narrative_beat") {
345
+ warnings.push({
346
+ severity: "warning",
347
+ recordName,
348
+ recordType: "narrative_beat",
349
+ message: "Legacy narrative_beat shape detected. It is rendered separately and preserved on save.",
350
+ upstreamIssue: UPSTREAM_ISSUE,
351
+ })
352
+ }
353
+ }
354
+
355
+ return warnings
356
+ }
357
+
358
+ export function serializeParsedData(data: Pick<ParsedData, "entities" | "relations" | "rawRecords">): string {
359
+ const rawRecords = data.rawRecords || []
360
+ const emittedEntities = new Set<string>()
361
+ const emittedRelations = new Set<string>()
362
+ const records: RawJSONLRecord[] = []
363
+
364
+ for (const original of rawRecords) {
365
+ if (isEntityLikeRecord(original)) {
366
+ const entity = data.entities.get(original.name)
367
+ if (entity) {
368
+ records.push(serializeEntity(entity, original))
369
+ emittedEntities.add(entity.name)
370
+ }
371
+ continue
372
+ }
373
+
374
+ if (isRelationLikeRecord(original)) {
375
+ const key = relationKey(original as RelationRecord)
376
+ const relation = data.relations.find((candidate) => relationKey(candidate) === key)
377
+ if (relation) {
378
+ records.push(serializeRelation(relation, original))
379
+ emittedRelations.add(key)
380
+ }
381
+ continue
382
+ }
383
+
384
+ records.push(original)
385
+ }
386
+
387
+ for (const entity of data.entities.values()) {
388
+ if (!emittedEntities.has(entity.name)) {
389
+ records.push(serializeEntity(entity))
390
+ }
391
+ }
392
+
393
+ for (const relation of data.relations) {
394
+ const key = relationKey(relation)
395
+ if (!emittedRelations.has(key)) {
396
+ records.push(serializeRelation(relation))
397
+ }
398
+ }
399
+
400
+ validateRecordPreservation(rawRecords, records, data.entities, data.relations)
401
+
402
+ return `${records.map((record) => JSON.stringify(record)).join("\n")}\n`
403
+ }
404
+
405
+ export { UPSTREAM_ISSUE }
package/lib/types.ts CHANGED
@@ -6,6 +6,7 @@ export interface EntityRecord {
6
6
  entityType: "structural_tension_chart" | "desired_outcome" | "current_reality" | "action_step" | "narrative_beat"
7
7
  observations: string[]
8
8
  metadata: Record<string, any>
9
+ [key: string]: any
9
10
  }
10
11
 
11
12
  export interface RelationRecord {
@@ -14,9 +15,34 @@ export interface RelationRecord {
14
15
  to: string
15
16
  relationType: "contains" | "creates_tension_with" | "advances_toward" | "documents"
16
17
  metadata: Record<string, any>
18
+ [key: string]: any
17
19
  }
18
20
 
19
21
  export type JSONLRecord = EntityRecord | RelationRecord
22
+ export type RawJSONLRecord = Record<string, any>
23
+
24
+ export interface MetadataWarning {
25
+ severity: "warning" | "error"
26
+ recordName?: string
27
+ recordType?: string
28
+ message: string
29
+ upstreamIssue?: string
30
+ }
31
+
32
+ export interface RecordProjections {
33
+ chartRecords: EntityRecord[]
34
+ actionStepRecords: EntityRecord[]
35
+ narrativeBeatRecords: EntityRecord[]
36
+ }
37
+
38
+ export interface RiseSpecProjection {
39
+ chartId: string
40
+ desiredOutcome: string
41
+ currentReality: string[]
42
+ naturalProgression: string[]
43
+ remainingBlockers: string[]
44
+ nextOperations: string[]
45
+ }
20
46
 
21
47
  export interface Chart {
22
48
  id: string
@@ -36,4 +62,8 @@ export interface ParsedData {
36
62
  entities: Map<string, EntityRecord>
37
63
  relations: RelationRecord[]
38
64
  rootCharts: Chart[]
65
+ rawRecords?: RawJSONLRecord[]
66
+ projections?: RecordProjections
67
+ preservationWarnings?: MetadataWarning[]
68
+ riseSpecs?: RiseSpecProjection[]
39
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coaia-visualizer",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./index.tsx",
@@ -18,6 +18,35 @@
18
18
  "bin": {
19
19
  "coaia-visualizer": "dist/cli.js"
20
20
  },
21
+ "files": [
22
+ "app/**/*",
23
+ "components/**/*",
24
+ "hooks/**/*",
25
+ "lib/**/*",
26
+ "styles/**/*",
27
+ "public/**/*",
28
+ "dist/**/*",
29
+ "mcp/dist/**/*",
30
+ "mcp/src/**/*",
31
+ "mcp/package.json",
32
+ "mcp/package-lock.json",
33
+ "mcp/README.md",
34
+ "mcp/Dockerfile",
35
+ "mcp/tsconfig.json",
36
+ "index.tsx",
37
+ "cli.ts",
38
+ "skill.ts",
39
+ "components.json",
40
+ "mcp-config.json",
41
+ "next-env.d.ts",
42
+ "next.config.mjs",
43
+ "postcss.config.mjs",
44
+ "tsconfig.json",
45
+ "README.md",
46
+ "README_TELESCOPED_NAVIGATION.md",
47
+ "KINSHIP.md",
48
+ "NAMING.md"
49
+ ],
21
50
  "scripts": {
22
51
  "build": "next build",
23
52
  "build:cli": "tsc cli.ts --outDir dist --module esnext --target es2022 --moduleResolution bundler --esModuleInterop --skipLibCheck",
@@ -26,6 +55,7 @@
26
55
  "prepare": "npm run build:cli",
27
56
  "start": "next start",
28
57
  "test": "playwright test",
58
+ "test:rich-jsonl": "playwright test test-scripts/rich-jsonl-preservation.spec.ts",
29
59
  "test:global-cli": "playwright test test-scripts/test-global-cli.spec.ts",
30
60
  "docker:build": "docker build -t jgwill/coaia:visualizer .",
31
61
  "docker:push": "docker push jgwill/coaia:visualizer",
package/.dockerignore DELETED
@@ -1,9 +0,0 @@
1
- node_modules
2
- dist
3
- .next
4
- .git
5
- .env.local
6
- .env*.local
7
- test-results
8
- *.log
9
- .DS_Store