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
package/README.md CHANGED
@@ -17,6 +17,7 @@ This tool allows users to upload their `coaia-narrative` memory files and gain c
17
17
  * **Dynamic Structural Tension Display:** Clearly distinguishes and presents the core Desired Outcome and Current Reality observations, making the generative force of structural tension visible.
18
18
  * **Action Step Tracking:** View all Action Steps for a selected chart, complete with their descriptions, completion status, and due dates, facilitating a clear understanding of strategic advancement.
19
19
  * **Narrative Beat Integration:** Displays associated Narrative Beats, offering multi-universe perspectives (Engineer-World, Ceremony-World, Story-Engine-World) to enrich the context of your creative efforts.
20
+ * **Rich JSONL Metadata Projection:** Separates chart, action-step, and narrative-beat records; exposes nested `metadata.github`, RISE/spec fields, unknown extension metadata, and preservation warnings aligned with [`avadisabelle/coaia-narrative#35`](https://github.com/avadisabelle/coaia-narrative/issues/35).
20
21
  * **Inter-Chart Relation Graph:** Visualizes the relationships between charts and entities, illustrating how different creative projects and their components are interconnected.
21
22
  * **Data Statistics Overview:** Provides summary statistics of the loaded data, including total charts, entities, and relations, for quick comprehension of your creative landscape.
22
23
 
@@ -113,4 +114,3 @@ Continue building your app on:
113
114
  ## Alternative
114
115
  * You can run it locally
115
116
 
116
-
package/app/page.tsx CHANGED
@@ -8,6 +8,7 @@ import { DataStats } from "@/components/data-stats"
8
8
  import { CreateChartForm } from "@/components/create-chart-form"
9
9
  import type { ParsedData, Chart } from "@/lib/types"
10
10
  import { parseJSONL, organizeData } from "@/lib/jsonl-parser"
11
+ import { serializeParsedData } from "@/lib/jsonl-preservation"
11
12
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
12
13
  import { Download, Upload, RefreshCw, Save, Edit3, Plus } from "lucide-react"
13
14
  import { Button } from "@/components/ui/button"
@@ -120,20 +121,10 @@ export default function Page() {
120
121
 
121
122
  setIsLoading(true)
122
123
  try {
123
- const jsonlLines: string[] = []
124
-
125
- for (const entity of data.entities.values()) {
126
- jsonlLines.push(JSON.stringify(entity))
127
- }
128
-
129
- for (const relation of data.relations) {
130
- jsonlLines.push(JSON.stringify(relation))
131
- }
132
-
133
124
  const response = await fetch("/api/jsonl", {
134
125
  method: "POST",
135
126
  headers: { "Content-Type": "application/json" },
136
- body: JSON.stringify({ content: jsonlLines.join("\n") + "\n" }),
127
+ body: JSON.stringify({ content: serializeParsedData(data) }),
137
128
  })
138
129
 
139
130
  if (response.ok) {
@@ -160,17 +151,7 @@ export default function Page() {
160
151
  const handleExportData = () => {
161
152
  if (!data) return
162
153
 
163
- const jsonlLines: string[] = []
164
-
165
- for (const entity of data.entities.values()) {
166
- jsonlLines.push(JSON.stringify(entity))
167
- }
168
-
169
- for (const relation of data.relations) {
170
- jsonlLines.push(JSON.stringify(relation))
171
- }
172
-
173
- const blob = new Blob([jsonlLines.join("\n") + "\n"], { type: "application/jsonl" })
154
+ const blob = new Blob([serializeParsedData(data)], { type: "application/jsonl" })
174
155
  const url = URL.createObjectURL(blob)
175
156
  const a = document.createElement("a")
176
157
  a.href = url
@@ -206,7 +187,7 @@ export default function Page() {
206
187
  const previousChartId = newStack.pop()!.id
207
188
  setChartNavigationStack(newStack)
208
189
  // Find the refreshed parent chart from current data (with populated subCharts)
209
- const refreshedParent = data.charts.find((c) => c.id === previousChartId)
190
+ const refreshedParent = data?.charts.find((c) => c.id === previousChartId)
210
191
  if (refreshedParent) {
211
192
  setSelectedChart(refreshedParent)
212
193
  }
@@ -9,6 +9,7 @@ import { Separator } from "@/components/ui/separator"
9
9
  import type { ParsedData, Chart } from "@/lib/types"
10
10
  import { Target, MapPin, ListChecks, Network, BookOpen, TrendingUp, ArrowLeft } from "lucide-react"
11
11
  import { NarrativeBeats } from "@/components/narrative-beats"
12
+ import { MetadataProjections } from "@/components/metadata-projections"
12
13
  import { RelationGraph } from "@/components/relation-graph"
13
14
  import { EditDesiredOutcome } from "./edit-desired-outcome"
14
15
  import { EditCurrentReality } from "./edit-current-reality"
@@ -218,6 +219,8 @@ export function ChartDetailEditable({
218
219
  </div>
219
220
  )}
220
221
 
222
+ <MetadataProjections chart={chart} data={data} />
223
+
221
224
  {/* Sub-charts */}
222
225
  {chart.subCharts.length > 0 && (
223
226
  <Card>
@@ -0,0 +1,208 @@
1
+ "use client"
2
+
3
+ import { AlertTriangle, Blocks, FileJson, Github, ListChecks, Sparkles, Target } from "lucide-react"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
+ import type { Chart, EntityRecord, ParsedData } from "@/lib/types"
7
+
8
+ interface MetadataProjectionsProps {
9
+ chart: Chart
10
+ data: ParsedData
11
+ }
12
+
13
+ const KNOWN_METADATA_KEYS = new Set([
14
+ "dueDate",
15
+ "chartId",
16
+ "phase",
17
+ "completionStatus",
18
+ "completedAt",
19
+ "parentChart",
20
+ "parentActionStep",
21
+ "level",
22
+ "createdAt",
23
+ "updatedAt",
24
+ "act",
25
+ "type_dramatic",
26
+ "universes",
27
+ "timestamp",
28
+ "elementsOfPerformance",
29
+ "mmotEvaluations",
30
+ "relationalAlignment",
31
+ "fourDirections",
32
+ "narrative",
33
+ "isTelescopedChart",
34
+ "telescopedChartId",
35
+ "github",
36
+ ])
37
+
38
+ function JsonBlock({ value }: { value: unknown }) {
39
+ return (
40
+ <pre className="max-h-64 overflow-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed">
41
+ {JSON.stringify(value, null, 2)}
42
+ </pre>
43
+ )
44
+ }
45
+
46
+ function ProjectionRecord({ entity, label }: { entity?: EntityRecord; label: string }) {
47
+ if (!entity) return null
48
+
49
+ const metadata = entity.metadata || {}
50
+ const unknownKeys = Object.keys(metadata).filter((key) => !KNOWN_METADATA_KEYS.has(key))
51
+
52
+ return (
53
+ <div className="rounded-md border p-3">
54
+ <div className="flex flex-wrap items-center gap-2">
55
+ <Badge variant="outline">{label}</Badge>
56
+ <span className="font-mono text-xs text-muted-foreground">{entity.name}</span>
57
+ {metadata.github && (
58
+ <Badge variant="secondary" className="gap-1">
59
+ <Github className="h-3 w-3" />
60
+ GitHub
61
+ </Badge>
62
+ )}
63
+ {unknownKeys.length > 0 && <Badge variant="secondary">{unknownKeys.length} extensions</Badge>}
64
+ </div>
65
+
66
+ {metadata.github && (
67
+ <div className="mt-3">
68
+ <div className="mb-1 flex items-center gap-2 text-sm font-medium">
69
+ <Github className="h-4 w-4" />
70
+ GitHub Provenance
71
+ </div>
72
+ <JsonBlock value={metadata.github} />
73
+ </div>
74
+ )}
75
+
76
+ <details className="mt-3">
77
+ <summary className="cursor-pointer text-sm font-medium">Full Metadata</summary>
78
+ <div className="mt-2">
79
+ <JsonBlock value={metadata} />
80
+ </div>
81
+ </details>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
87
+ const riseSpec = (data.riseSpecs || []).find((spec) => spec.chartId === chart.id)
88
+ const chartRecordNames = new Set(
89
+ [
90
+ chart.chartEntity.name,
91
+ chart.desiredOutcome?.name,
92
+ chart.currentReality?.name,
93
+ ...chart.actions.map((action) => action.name),
94
+ ...chart.narrativeBeats.map((beat) => beat.name),
95
+ ].filter(Boolean),
96
+ )
97
+ const warnings = (data.preservationWarnings || []).filter(
98
+ (warning) => !warning.recordName || chartRecordNames.has(warning.recordName),
99
+ )
100
+
101
+ return (
102
+ <div className="space-y-6">
103
+ {warnings.length > 0 && (
104
+ <Card className="border-amber-500/40">
105
+ <CardHeader>
106
+ <CardTitle className="flex items-center gap-2 text-base">
107
+ <AlertTriangle className="h-5 w-5 text-amber-600" />
108
+ Preservation Warnings
109
+ </CardTitle>
110
+ </CardHeader>
111
+ <CardContent className="space-y-2">
112
+ {warnings.map((warning, index) => (
113
+ <div key={`${warning.recordName}-${index}`} className="rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
114
+ <div className="font-medium">{warning.recordName || warning.recordType || "JSONL record"}</div>
115
+ <div className="text-muted-foreground">{warning.message}</div>
116
+ {warning.upstreamIssue && (
117
+ <a className="text-xs underline" href={warning.upstreamIssue} target="_blank" rel="noreferrer">
118
+ avadisabelle/coaia-narrative#35
119
+ </a>
120
+ )}
121
+ </div>
122
+ ))}
123
+ </CardContent>
124
+ </Card>
125
+ )}
126
+
127
+ <Card>
128
+ <CardHeader>
129
+ <CardTitle className="flex items-center gap-2 text-base">
130
+ <Blocks className="h-5 w-5" />
131
+ RISE Projection
132
+ </CardTitle>
133
+ </CardHeader>
134
+ <CardContent className="grid gap-4 md:grid-cols-2">
135
+ <div>
136
+ <div className="mb-1 flex items-center gap-2 text-sm font-medium">
137
+ <Target className="h-4 w-4" />
138
+ Desired Outcome
139
+ </div>
140
+ <p className="text-sm text-muted-foreground">{riseSpec?.desiredOutcome || "No desired outcome"}</p>
141
+ </div>
142
+ <div>
143
+ <div className="mb-1 text-sm font-medium">Current Reality</div>
144
+ <ul className="space-y-1 text-sm text-muted-foreground">
145
+ {(riseSpec?.currentReality.length ? riseSpec.currentReality : ["No current reality"]).map((item, index) => (
146
+ <li key={index}>{item}</li>
147
+ ))}
148
+ </ul>
149
+ </div>
150
+ <div>
151
+ <div className="mb-1 text-sm font-medium">Natural Progression</div>
152
+ <ul className="space-y-1 text-sm text-muted-foreground">
153
+ {(riseSpec?.naturalProgression.length ? riseSpec.naturalProgression : ["No projected progression"]).map((item, index) => (
154
+ <li key={index}>{item}</li>
155
+ ))}
156
+ </ul>
157
+ </div>
158
+ <div>
159
+ <div className="mb-1 text-sm font-medium">Remaining Blockers</div>
160
+ <ul className="space-y-1 text-sm text-muted-foreground">
161
+ {(riseSpec?.remainingBlockers.length ? riseSpec.remainingBlockers : ["No blockers recorded"]).map((item, index) => (
162
+ <li key={index}>{item}</li>
163
+ ))}
164
+ </ul>
165
+ </div>
166
+ <div className="md:col-span-2">
167
+ <div className="mb-1 text-sm font-medium">Next Chart Operations</div>
168
+ <ul className="space-y-1 text-sm text-muted-foreground">
169
+ {(riseSpec?.nextOperations.length ? riseSpec.nextOperations : ["No next operations recorded"]).map((item, index) => (
170
+ <li key={index}>{item}</li>
171
+ ))}
172
+ </ul>
173
+ </div>
174
+ </CardContent>
175
+ </Card>
176
+
177
+ <Card>
178
+ <CardHeader>
179
+ <CardTitle className="flex items-center gap-2 text-base">
180
+ <FileJson className="h-5 w-5" />
181
+ Rich Metadata Projection
182
+ </CardTitle>
183
+ </CardHeader>
184
+ <CardContent className="space-y-3">
185
+ <ProjectionRecord label="Chart" entity={chart.chartEntity} />
186
+ <ProjectionRecord label="Desired Outcome" entity={chart.desiredOutcome} />
187
+ <ProjectionRecord label="Current Reality" entity={chart.currentReality} />
188
+ {chart.actions.map((action) => (
189
+ <ProjectionRecord key={action.name} label="Action Step" entity={action} />
190
+ ))}
191
+ {chart.narrativeBeats.map((beat) => (
192
+ <ProjectionRecord key={beat.name} label="Narrative Beat" entity={beat} />
193
+ ))}
194
+ <div className="flex flex-wrap gap-2 pt-1 text-sm text-muted-foreground">
195
+ <Badge variant="outline" className="gap-1">
196
+ <ListChecks className="h-3 w-3" />
197
+ {data.projections?.actionStepRecords.length || 0} actions
198
+ </Badge>
199
+ <Badge variant="outline" className="gap-1">
200
+ <Sparkles className="h-3 w-3" />
201
+ {data.projections?.narrativeBeatRecords.length || 0} beats
202
+ </Badge>
203
+ </div>
204
+ </CardContent>
205
+ </Card>
206
+ </div>
207
+ )
208
+ }
@@ -1,6 +1,8 @@
1
1
  // lib/chart-editor.ts - Chart editing logic
2
2
 
3
3
  import type { EntityRecord, RelationRecord, ParsedData } from "./types"
4
+ import { buildMetadataWarnings, buildRecordProjections, serializeParsedData } from "./jsonl-preservation"
5
+ import { buildRiseSpecs } from "./jsonl-parser"
4
6
 
5
7
  export interface ChartUpdate {
6
8
  type:
@@ -20,11 +22,13 @@ export interface ChartUpdate {
20
22
  export class ChartEditor {
21
23
  private entities: Map<string, EntityRecord>
22
24
  private relations: RelationRecord[]
25
+ private rawRecords: NonNullable<ParsedData["rawRecords"]>
23
26
 
24
27
  constructor(parsedData: ParsedData) {
25
28
  // Clone data to avoid mutations
26
29
  this.entities = new Map(parsedData.entities)
27
30
  this.relations = [...parsedData.relations]
31
+ this.rawRecords = [...(parsedData.rawRecords || [])]
28
32
  }
29
33
 
30
34
  private resolveStructuralChart(chartRef: string): { entityName: string; chartId: string; entity: EntityRecord } | null {
@@ -520,19 +524,11 @@ export class ChartEditor {
520
524
  }
521
525
 
522
526
  exportToJSONL(): string {
523
- const lines: string[] = []
524
-
525
- // Export entities
526
- for (const entity of this.entities.values()) {
527
- lines.push(JSON.stringify(entity))
528
- }
529
-
530
- // Export relations
531
- for (const relation of this.relations) {
532
- lines.push(JSON.stringify(relation))
533
- }
534
-
535
- return lines.join("\n") + "\n"
527
+ return serializeParsedData({
528
+ entities: this.entities,
529
+ relations: this.relations,
530
+ rawRecords: this.rawRecords,
531
+ })
536
532
  }
537
533
 
538
534
  getUpdatedData(): ParsedData {
@@ -615,6 +611,10 @@ export class ChartEditor {
615
611
  relations: this.relations,
616
612
  charts: chartsArray,
617
613
  rootCharts: rootCharts,
614
+ rawRecords: this.rawRecords,
615
+ projections: buildRecordProjections(this.entities),
616
+ preservationWarnings: buildMetadataWarnings(this.rawRecords),
617
+ riseSpecs: buildRiseSpecs(chartsArray),
618
618
  }
619
619
  }
620
620
  }
@@ -1,8 +1,9 @@
1
- import type { EntityRecord, RelationRecord, JSONLRecord, Chart, ParsedData } from "./types"
1
+ import type { Chart, EntityRecord, JSONLRecord, ParsedData, RawJSONLRecord, RelationRecord, RiseSpecProjection } from "./types"
2
+ import { buildMetadataWarnings, buildRecordProjections, normalizeJsonlRecord } from "./jsonl-preservation"
2
3
 
3
- export function parseJSONL(content: string): JSONLRecord[] {
4
+ export function parseJSONL(content: string): RawJSONLRecord[] {
4
5
  const lines = content.trim().split("\n")
5
- const records: JSONLRecord[] = []
6
+ const records: RawJSONLRecord[] = []
6
7
 
7
8
  for (const line of lines) {
8
9
  if (!line.trim()) continue
@@ -17,7 +18,44 @@ export function parseJSONL(content: string): JSONLRecord[] {
17
18
  return records
18
19
  }
19
20
 
20
- export function organizeData(records: JSONLRecord[]): ParsedData {
21
+ function firstObservation(entity?: EntityRecord): string {
22
+ return entity?.observations?.[0] || ""
23
+ }
24
+
25
+ function asStringList(value: unknown): string[] {
26
+ if (Array.isArray(value)) {
27
+ return value.map((item) => typeof item === "string" ? item : JSON.stringify(item))
28
+ }
29
+ if (typeof value === "string" && value.trim()) {
30
+ return [value]
31
+ }
32
+ return []
33
+ }
34
+
35
+ export function buildRiseSpecs(charts: Chart[]): RiseSpecProjection[] {
36
+ return charts.map((chart) => {
37
+ const metadata = chart.chartEntity.metadata || {}
38
+ const rise = metadata.rise || metadata.riseSpec || metadata.spec || {}
39
+ const incompleteActions = chart.actions.filter((action) => action.metadata?.completionStatus !== true)
40
+
41
+ return {
42
+ chartId: chart.id,
43
+ desiredOutcome: firstObservation(chart.desiredOutcome),
44
+ currentReality: chart.currentReality?.observations || [],
45
+ naturalProgression: [
46
+ ...asStringList(rise.naturalProgression || metadata.naturalProgression),
47
+ ...incompleteActions.slice(0, 3).map((action) => action.observations?.[0]).filter(Boolean),
48
+ ],
49
+ remainingBlockers: asStringList(rise.remainingBlockers || rise.blockers || metadata.remainingBlockers || metadata.blockers),
50
+ nextOperations: [
51
+ ...asStringList(rise.nextOperations || metadata.nextOperations),
52
+ ...incompleteActions.slice(0, 5).map((action) => action.observations?.[0]).filter(Boolean),
53
+ ],
54
+ }
55
+ })
56
+ }
57
+
58
+ export function organizeData(records: RawJSONLRecord[]): ParsedData {
21
59
  const entities = new Map<string, EntityRecord>()
22
60
  const relations: RelationRecord[] = []
23
61
  const charts = new Map<string, Chart>()
@@ -26,10 +64,15 @@ export function organizeData(records: JSONLRecord[]): ParsedData {
26
64
 
27
65
  // First pass: separate entities and relations
28
66
  for (const record of records) {
29
- if (record.type === "entity") {
30
- entities.set(record.name, record)
31
- } else if (record.type === "relation") {
32
- relations.push(record)
67
+ const normalized = normalizeJsonlRecord(record)
68
+ if (!normalized) {
69
+ continue
70
+ }
71
+
72
+ if (normalized.type === "entity") {
73
+ entities.set(normalized.name, normalized)
74
+ } else if (normalized.type === "relation") {
75
+ relations.push(normalized)
33
76
  }
34
77
  }
35
78
 
@@ -163,6 +206,10 @@ export function organizeData(records: JSONLRecord[]): ParsedData {
163
206
  entities,
164
207
  relations,
165
208
  rootCharts,
209
+ rawRecords: records,
210
+ projections: buildRecordProjections(entities),
211
+ preservationWarnings: buildMetadataWarnings(records),
212
+ riseSpecs: buildRiseSpecs(Array.from(charts.values())),
166
213
  }
167
214
  }
168
215