coaia-visualizer 1.6.4 → 1.6.5

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 CHANGED
@@ -17,7 +17,8 @@ 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
+ * **Rich JSONL Metadata Projection:** Separates chart, action-step, and narrative-beat records; exposes nested `metadata.github`, RISE/spec fields, Asterion foundation/session-lineage projections, unknown extension metadata, and preservation warnings aligned with [`avadisabelle/coaia-narrative#35`](https://github.com/avadisabelle/coaia-narrative/issues/35).
21
+ * **Asterion Foundation & Lineage Cards:** Visualizes foundation packet metadata, artifact diffs, issue references, session branch maps, and beat-linked lineage without exposing raw Chronicle content by default.
21
22
  * **Inter-Chart Relation Graph:** Visualizes the relationships between charts and entities, illustrating how different creative projects and their components are interconnected.
22
23
  * **Data Statistics Overview:** Provides summary statistics of the loaded data, including total charts, entities, and relations, for quick comprehension of your creative landscape.
23
24
 
@@ -0,0 +1,175 @@
1
+ "use client"
2
+
3
+ import { AlertTriangle, CheckCircle2, ExternalLink, FileCheck2, FileX2, Github, Shield, UploadCloud } from "lucide-react"
4
+
5
+ import { Badge } from "@/components/ui/badge"
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import type { FoundationProjection, ParsedIssueReference } from "@/lib/types"
8
+
9
+ interface AsterionFoundationCardProps {
10
+ projection: FoundationProjection
11
+ }
12
+
13
+ const evaluationTone: Record<NonNullable<FoundationProjection["foundation"]["evaluationStatus"]>, string> = {
14
+ expected: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
15
+ delegated: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
16
+ produced: "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300",
17
+ evaluated: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
18
+ }
19
+
20
+ const privacyTone: Record<NonNullable<FoundationProjection["foundation"]["privacyClass"]>, string> = {
21
+ "public-safe": "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
22
+ private: "border-destructive/40 bg-destructive/10 text-destructive",
23
+ mixed: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
24
+ }
25
+
26
+ const publicationTone: Record<NonNullable<FoundationProjection["foundation"]["publicationStatus"]>, string> = {
27
+ planned: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
28
+ draft: "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300",
29
+ reviewed: "border-violet-500/40 bg-violet-500/10 text-violet-700 dark:text-violet-300",
30
+ published: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
31
+ }
32
+
33
+ export function AsterionFoundationCard({ projection }: AsterionFoundationCardProps) {
34
+ const foundation = projection.foundation
35
+
36
+ return (
37
+ <Card>
38
+ <CardHeader>
39
+ <CardTitle className="flex items-center gap-2 text-base">
40
+ <Shield className="h-5 w-5" />
41
+ Asterion Foundation
42
+ </CardTitle>
43
+ </CardHeader>
44
+ <CardContent className="space-y-4">
45
+ {projection.warnings.length > 0 && (
46
+ <div className="space-y-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3">
47
+ {projection.warnings.map((warning, index) => (
48
+ <div key={`${warning.recordName || "foundation"}-${index}`} className="flex items-start gap-2 text-sm">
49
+ <AlertTriangle className="mt-0.5 h-4 w-4 text-amber-600" />
50
+ <span>{warning.message}</span>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ )}
55
+
56
+ <div className="grid gap-3 md:grid-cols-2">
57
+ <MetaBlock label="Packet Root" value={foundation.packetRoot} />
58
+ <MetaBlock label="Foundation Type" value={foundation.foundationType} />
59
+ </div>
60
+
61
+ <div className="flex flex-wrap gap-2">
62
+ {foundation.evaluationStatus && (
63
+ <Badge variant="outline" className={evaluationTone[foundation.evaluationStatus]}>
64
+ evaluation: {foundation.evaluationStatus}
65
+ </Badge>
66
+ )}
67
+ {foundation.privacyClass && (
68
+ <Badge variant="outline" className={privacyTone[foundation.privacyClass]}>
69
+ privacy: {foundation.privacyClass}
70
+ </Badge>
71
+ )}
72
+ {foundation.publicationStatus && (
73
+ <Badge variant="outline" className={publicationTone[foundation.publicationStatus]}>
74
+ publication: {foundation.publicationStatus}
75
+ </Badge>
76
+ )}
77
+ </div>
78
+
79
+ {projection.issueRefs.length > 0 && (
80
+ <div>
81
+ <div className="mb-2 text-sm font-medium">Issue References</div>
82
+ <div className="flex flex-wrap gap-2">
83
+ {projection.issueRefs.map((issue) => (
84
+ <IssueBadge key={issue.key} issue={issue} />
85
+ ))}
86
+ </div>
87
+ </div>
88
+ )}
89
+
90
+ {projection.artifactDiff.length > 0 && (
91
+ <div>
92
+ <div className="mb-2 text-sm font-medium">Artifact Checklist</div>
93
+ <div className="space-y-2">
94
+ {projection.artifactDiff.map((artifact) => (
95
+ <div key={artifact.artifact} className="flex items-center justify-between rounded-md border p-3 text-sm">
96
+ <div className="font-mono text-xs md:text-sm">{artifact.artifact}</div>
97
+ <div className="flex items-center gap-2">
98
+ {artifact.status === "matched" ? (
99
+ <Badge variant="outline" className="gap-1 border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300">
100
+ <CheckCircle2 className="h-3 w-3" />
101
+ matched
102
+ </Badge>
103
+ ) : artifact.status === "missing" ? (
104
+ <Badge variant="outline" className="gap-1 border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300">
105
+ <FileX2 className="h-3 w-3" />
106
+ expected only
107
+ </Badge>
108
+ ) : (
109
+ <Badge variant="outline" className="gap-1 border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300">
110
+ <UploadCloud className="h-3 w-3" />
111
+ produced only
112
+ </Badge>
113
+ )}
114
+ </div>
115
+ </div>
116
+ ))}
117
+ </div>
118
+ </div>
119
+ )}
120
+
121
+ {foundation.commitHandles && foundation.commitHandles.length > 0 && (
122
+ <div>
123
+ <div className="mb-2 text-sm font-medium">Commit Handles</div>
124
+ <div className="flex flex-wrap gap-2">
125
+ {foundation.commitHandles.map((handle) => (
126
+ <Badge key={handle} variant="secondary" className="font-mono text-xs">
127
+ {handle}
128
+ </Badge>
129
+ ))}
130
+ </div>
131
+ </div>
132
+ )}
133
+
134
+ {projection.sourceRecords.length > 0 && (
135
+ <div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
136
+ <Badge variant="outline" className="gap-1">
137
+ <FileCheck2 className="h-3 w-3" />
138
+ {projection.sourceRecords.length} source record{projection.sourceRecords.length === 1 ? "" : "s"}
139
+ </Badge>
140
+ </div>
141
+ )}
142
+ </CardContent>
143
+ </Card>
144
+ )
145
+ }
146
+
147
+ function MetaBlock({ label, value }: { label: string; value?: string }) {
148
+ if (!value) return null
149
+
150
+ return (
151
+ <div className="rounded-md border p-3">
152
+ <div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
153
+ <div className="mt-1 text-sm font-medium">{value}</div>
154
+ </div>
155
+ )
156
+ }
157
+
158
+ function IssueBadge({ issue }: { issue: ParsedIssueReference }) {
159
+ const content = (
160
+ <Badge variant="outline" className="gap-1 text-xs">
161
+ <Github className="h-3 w-3" />
162
+ {issue.key}
163
+ <span className="font-normal text-muted-foreground">{issue.label}</span>
164
+ {issue.href && <ExternalLink className="h-3 w-3" />}
165
+ </Badge>
166
+ )
167
+
168
+ if (!issue.href) return content
169
+
170
+ return (
171
+ <a href={issue.href} target="_blank" rel="noreferrer">
172
+ {content}
173
+ </a>
174
+ )
175
+ }
@@ -0,0 +1,122 @@
1
+ "use client"
2
+
3
+ import { AlertTriangle, Clock, FileCheck2, Link2, MapPin, Mic, Monitor, ScrollText, Shield } from "lucide-react"
4
+
5
+ import { Badge } from "@/components/ui/badge"
6
+ import type { SessionContextProjection } from "@/lib/types"
7
+
8
+ interface AsterionSessionContextBadgeProps {
9
+ projection: SessionContextProjection
10
+ }
11
+
12
+ const captureQualityTone: Record<NonNullable<SessionContextProjection["context"]["captureQuality"]>, string> = {
13
+ complete: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
14
+ partial: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
15
+ noisy: "border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300",
16
+ "transcript-only": "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300",
17
+ }
18
+
19
+ const continuationKindTone: Record<NonNullable<SessionContextProjection["context"]["continuationKind"]>, string> = {
20
+ new: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
21
+ resumption: "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300",
22
+ "branch-return": "border-violet-500/40 bg-violet-500/10 text-violet-700 dark:text-violet-300",
23
+ "daily-chronicle": "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
24
+ }
25
+
26
+ const modeIcon: Record<string, React.ElementType> = {
27
+ voice: Mic,
28
+ terminal: Monitor,
29
+ mixed: ScrollText,
30
+ }
31
+
32
+ const settingIcon: Record<string, React.ElementType> = {
33
+ outdoor: MapPin,
34
+ walking: MapPin,
35
+ "land-based": MapPin,
36
+ indoor: Monitor,
37
+ office: Monitor,
38
+ }
39
+
40
+ export function AsterionSessionContextBadge({ projection }: AsterionSessionContextBadgeProps) {
41
+ const context = projection.context
42
+
43
+ if (Object.keys(context).length === 0) {
44
+ return null
45
+ }
46
+
47
+ const hasWarnings = projection.warnings.length > 0
48
+ const captureWarnings = projection.warnings.filter(
49
+ (w) => w.message.includes("Capture quality") || w.message.includes("partial") || w.message.includes("noisy"),
50
+ )
51
+
52
+ const ModeIcon = context.mode ? modeIcon[context.mode.toLowerCase()] || Monitor : Monitor
53
+ const SettingIcon = context.setting ? settingIcon[context.setting.toLowerCase()] || MapPin : MapPin
54
+
55
+ return (
56
+ <div className="flex flex-wrap items-center gap-2 rounded-md border border-sky-500/30 bg-sky-500/5 p-2">
57
+ <Badge variant="outline" className="gap-1 border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300">
58
+ <Clock className="h-3 w-3" />
59
+ Session Context
60
+ </Badge>
61
+
62
+ {context.mode && (
63
+ <Badge variant="outline" className="gap-1">
64
+ <ModeIcon className="h-3 w-3" />
65
+ {context.mode}
66
+ </Badge>
67
+ )}
68
+
69
+ {context.setting && (
70
+ <Badge variant="outline" className="gap-1">
71
+ <SettingIcon className="h-3 w-3" />
72
+ {context.setting}
73
+ </Badge>
74
+ )}
75
+
76
+ {context.captureQuality && (
77
+ <Badge variant="outline" className={captureQualityTone[context.captureQuality]}>
78
+ <FileCheck2 className="h-3 w-3" />
79
+ capture: {context.captureQuality}
80
+ </Badge>
81
+ )}
82
+
83
+ {context.continuationKind && (
84
+ <Badge variant="outline" className={continuationKindTone[context.continuationKind]}>
85
+ {context.continuationKind}
86
+ </Badge>
87
+ )}
88
+
89
+ {context.publicSummaryAllowed !== undefined && (
90
+ <Badge variant="outline" className={context.publicSummaryAllowed ? "border-emerald-500/40" : "border-red-500/40"}>
91
+ <Shield className="h-3 w-3" />
92
+ {context.publicSummaryAllowed ? "public-safe" : "private"}
93
+ </Badge>
94
+ )}
95
+
96
+ {context.handle && (
97
+ <Badge variant="secondary" className="gap-1 font-mono text-xs">
98
+ <Link2 className="h-3 w-3" />
99
+ {context.handle}
100
+ </Badge>
101
+ )}
102
+
103
+ {captureWarnings.length > 0 && (
104
+ <Badge variant="outline" className="gap-1 border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300">
105
+ <AlertTriangle className="h-3 w-3" />
106
+ quality note
107
+ </Badge>
108
+ )}
109
+
110
+ {hasWarnings && (
111
+ <div className="w-full space-y-1 pt-1 text-xs text-muted-foreground">
112
+ {projection.warnings.map((warning, index) => (
113
+ <div key={`${projection.beatName}-${index}`} className="flex items-start gap-1">
114
+ <AlertTriangle className="mt-0.5 h-3 w-3 flex-shrink-0 text-amber-600" />
115
+ <span>{warning.message}</span>
116
+ </div>
117
+ ))}
118
+ </div>
119
+ )}
120
+ </div>
121
+ )
122
+ }
@@ -0,0 +1,119 @@
1
+ "use client"
2
+
3
+ import { AlertTriangle, ExternalLink, Github, GitBranch, Link2, Workflow } from "lucide-react"
4
+
5
+ import { Badge } from "@/components/ui/badge"
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import type { SessionLineageProjection } from "@/lib/types"
8
+
9
+ interface AsterionSessionLineageCardProps {
10
+ projection: SessionLineageProjection
11
+ }
12
+
13
+ const handoffTone: Record<NonNullable<SessionLineageProjection["branches"][number]["metadata"]["handoffState"]>, string> = {
14
+ "requirements-created": "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
15
+ "implementation-ready": "border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300",
16
+ "returned-to-parent": "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
17
+ }
18
+
19
+ export function AsterionSessionLineageCard({ projection }: AsterionSessionLineageCardProps) {
20
+ return (
21
+ <Card>
22
+ <CardHeader>
23
+ <CardTitle className="flex items-center gap-2 text-base">
24
+ <Workflow className="h-5 w-5" />
25
+ Asterion Session Lineage
26
+ </CardTitle>
27
+ </CardHeader>
28
+ <CardContent className="space-y-4">
29
+ {projection.warnings.length > 0 && (
30
+ <div className="space-y-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3">
31
+ {projection.warnings.map((warning, index) => (
32
+ <div key={`${warning.recordName || "lineage"}-${index}`} className="flex items-start gap-2 text-sm">
33
+ <AlertTriangle className="mt-0.5 h-4 w-4 text-amber-600" />
34
+ <span>{warning.message}</span>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ )}
39
+
40
+ {projection.groupedBranches.map((group) => (
41
+ <div key={group.sourceBeat} className="rounded-md border p-4">
42
+ <div className="mb-3 flex flex-wrap items-center gap-2">
43
+ <Badge variant="outline" className="gap-1">
44
+ <GitBranch className="h-3 w-3" />
45
+ {group.branches.length} branch{group.branches.length === 1 ? "" : "es"}
46
+ </Badge>
47
+ {group.sourceBeatExistsInChart && group.sourceBeatAnchor ? (
48
+ <a href={`#${group.sourceBeatAnchor}`} className="inline-flex items-center gap-1 text-sm font-medium underline">
49
+ <Link2 className="h-3.5 w-3.5" />
50
+ {group.sourceBeat}
51
+ </a>
52
+ ) : (
53
+ <div className="text-sm font-medium">{group.sourceBeat}</div>
54
+ )}
55
+ </div>
56
+
57
+ <div className="space-y-3">
58
+ {group.branches.map((branch) => (
59
+ <div key={`${branch.sourceRecordName || branch.metadata.branchSessionId}-${branch.metadata.branchIndex || 0}`} className="rounded-md border bg-muted/20 p-3">
60
+ <div className="flex flex-wrap items-center gap-2">
61
+ <Badge variant="secondary">branch {branch.metadata.branchIndex ?? "?"}</Badge>
62
+ {branch.metadata.handoffState && (
63
+ <Badge variant="outline" className={handoffTone[branch.metadata.handoffState]}>
64
+ {branch.metadata.handoffState}
65
+ </Badge>
66
+ )}
67
+ {branch.metadata.platform && <Badge variant="outline">{branch.metadata.platform}</Badge>}
68
+ </div>
69
+
70
+ <div className="mt-3 grid gap-2 text-sm md:grid-cols-2">
71
+ <Field label="Purpose" value={branch.metadata.branchPurpose} />
72
+ <Field label="Copied Messages" value={branch.metadata.copiedMessageCount?.toString()} />
73
+ <Field label="Parent Chart" value={branch.metadata.parentChartId} />
74
+ <Field label="Original Session" value={branch.metadata.originalSessionId} mono />
75
+ <Field label="Branch Session" value={branch.metadata.branchSessionId} mono />
76
+ <Field label="Source Record" value={branch.sourceRecordName} mono />
77
+ </div>
78
+
79
+ {branch.relatedIssueRefs.length > 0 && (
80
+ <div className="mt-3 flex flex-wrap gap-2">
81
+ {branch.relatedIssueRefs.map((issue) => {
82
+ const badge = (
83
+ <Badge variant="outline" className="gap-1 text-xs">
84
+ <Github className="h-3 w-3" />
85
+ {issue.label}
86
+ {issue.href && <ExternalLink className="h-3 w-3" />}
87
+ </Badge>
88
+ )
89
+
90
+ return issue.href ? (
91
+ <a key={`${issue.key}-${issue.label}`} href={issue.href} target="_blank" rel="noreferrer">
92
+ {badge}
93
+ </a>
94
+ ) : (
95
+ <span key={`${issue.key}-${issue.label}`}>{badge}</span>
96
+ )
97
+ })}
98
+ </div>
99
+ )}
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ ))}
105
+ </CardContent>
106
+ </Card>
107
+ )
108
+ }
109
+
110
+ function Field({ label, value, mono = false }: { label: string; value?: string; mono?: boolean }) {
111
+ if (!value) return null
112
+
113
+ return (
114
+ <div>
115
+ <div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
116
+ <div className={`mt-1 ${mono ? "font-mono text-xs" : "text-sm"}`}>{value}</div>
117
+ </div>
118
+ )
119
+ }
@@ -7,6 +7,7 @@ import { Separator } from "@/components/ui/separator"
7
7
  import type { ParsedData, Chart, EntityRecord } from "@/lib/types"
8
8
  import { Target, MapPin, ListChecks, Network, BookOpen, Calendar, TrendingUp, CheckCircle2, Circle } from "lucide-react"
9
9
  import { NarrativeBeats } from "./narrative-beats"
10
+ import { MetadataProjections } from "./metadata-projections"
10
11
  import { RelationGraph } from "./relation-graph"
11
12
  import { ActionStepIssueBadge, ProjectSourceBadge } from "./github-provenance"
12
13
 
@@ -142,6 +143,8 @@ export function ChartDetail({ chart, data }: ChartDetailProps) {
142
143
  </div>
143
144
  )}
144
145
 
146
+ <MetadataProjections chart={chart} data={data} />
147
+
145
148
  {/* Sub-charts */}
146
149
  {chart.subCharts.length > 0 && (
147
150
  <Card>
@@ -1,9 +1,12 @@
1
1
  "use client"
2
2
 
3
3
  import { AlertTriangle, Blocks, FileJson, Github, ListChecks, Sparkles, Target } from "lucide-react"
4
+
5
+ import { AsterionFoundationCard } from "@/components/asterion-foundation-card"
6
+ import { AsterionSessionLineageCard } from "@/components/asterion-session-lineage-card"
4
7
  import { Badge } from "@/components/ui/badge"
5
8
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
- import type { Chart, EntityRecord, ParsedData } from "@/lib/types"
9
+ import type { Chart, EntityRecord, MetadataWarning, ParsedData } from "@/lib/types"
7
10
 
8
11
  interface MetadataProjectionsProps {
9
12
  chart: Chart
@@ -33,6 +36,17 @@ const KNOWN_METADATA_KEYS = new Set([
33
36
  "isTelescopedChart",
34
37
  "telescopedChartId",
35
38
  "github",
39
+ "asterion",
40
+ "asterionMetadata",
41
+ "asterionFoundation",
42
+ "foundation",
43
+ "foundationMetadata",
44
+ "sessionLineage",
45
+ "sessionLineageMetadata",
46
+ "sessionContext",
47
+ "sessionContextMetadata",
48
+ "livedContext",
49
+ "chronicle",
36
50
  ])
37
51
 
38
52
  function JsonBlock({ value }: { value: unknown }) {
@@ -83,6 +97,16 @@ function ProjectionRecord({ entity, label }: { entity?: EntityRecord; label: str
83
97
  )
84
98
  }
85
99
 
100
+ function dedupeWarnings(warnings: MetadataWarning[]): MetadataWarning[] {
101
+ const seen = new Set<string>()
102
+ return warnings.filter((warning) => {
103
+ const key = `${warning.recordName || ""}|${warning.recordType || ""}|${warning.message}`
104
+ if (seen.has(key)) return false
105
+ seen.add(key)
106
+ return true
107
+ })
108
+ }
109
+
86
110
  export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
87
111
  const riseSpec = (data.riseSpecs || []).find((spec) => spec.chartId === chart.id)
88
112
  const chartRecordNames = new Set(
@@ -94,9 +118,11 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
94
118
  ...chart.narrativeBeats.map((beat) => beat.name),
95
119
  ].filter(Boolean),
96
120
  )
97
- const warnings = (data.preservationWarnings || []).filter(
98
- (warning) => !warning.recordName || chartRecordNames.has(warning.recordName),
99
- )
121
+ const warnings = dedupeWarnings([
122
+ ...(data.preservationWarnings || []).filter((warning) => !warning.recordName || chartRecordNames.has(warning.recordName)),
123
+ ...(chart.foundationProjection?.warnings || []),
124
+ ...(chart.sessionLineageProjection?.warnings || []),
125
+ ])
100
126
 
101
127
  return (
102
128
  <div className="space-y-6">
@@ -105,13 +131,13 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
105
131
  <CardHeader>
106
132
  <CardTitle className="flex items-center gap-2 text-base">
107
133
  <AlertTriangle className="h-5 w-5 text-amber-600" />
108
- Preservation Warnings
134
+ Metadata Warnings
109
135
  </CardTitle>
110
136
  </CardHeader>
111
137
  <CardContent className="space-y-2">
112
138
  {warnings.map((warning, index) => (
113
139
  <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>
140
+ <div className="font-medium">{warning.recordName || warning.recordType || "Chart metadata"}</div>
115
141
  <div className="text-muted-foreground">{warning.message}</div>
116
142
  {warning.upstreamIssue && (
117
143
  <a className="text-xs underline" href={warning.upstreamIssue} target="_blank" rel="noreferrer">
@@ -124,6 +150,9 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
124
150
  </Card>
125
151
  )}
126
152
 
153
+ {chart.foundationProjection && <AsterionFoundationCard projection={chart.foundationProjection} />}
154
+ {chart.sessionLineageProjection && <AsterionSessionLineageCard projection={chart.sessionLineageProjection} />}
155
+
127
156
  <Card>
128
157
  <CardHeader>
129
158
  <CardTitle className="flex items-center gap-2 text-base">
@@ -178,19 +207,24 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
178
207
  <CardHeader>
179
208
  <CardTitle className="flex items-center gap-2 text-base">
180
209
  <FileJson className="h-5 w-5" />
181
- Rich Metadata Projection
210
+ Raw Metadata
182
211
  </CardTitle>
183
212
  </CardHeader>
184
213
  <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
- ))}
214
+ <details>
215
+ <summary className="cursor-pointer text-sm font-medium">Show preserved JSONL metadata</summary>
216
+ <div className="mt-3 space-y-3">
217
+ <ProjectionRecord label="Chart" entity={chart.chartEntity} />
218
+ <ProjectionRecord label="Desired Outcome" entity={chart.desiredOutcome} />
219
+ <ProjectionRecord label="Current Reality" entity={chart.currentReality} />
220
+ {chart.actions.map((action) => (
221
+ <ProjectionRecord key={action.name} label="Action Step" entity={action} />
222
+ ))}
223
+ {chart.narrativeBeats.map((beat) => (
224
+ <ProjectionRecord key={beat.name} label="Narrative Beat" entity={beat} />
225
+ ))}
226
+ </div>
227
+ </details>
194
228
  <div className="flex flex-wrap gap-2 pt-1 text-sm text-muted-foreground">
195
229
  <Badge variant="outline" className="gap-1">
196
230
  <ListChecks className="h-3 w-3" />
@@ -1,9 +1,12 @@
1
1
  "use client"
2
2
 
3
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
3
+ import { Clock, Compass, Drama, Link2, Sparkles } from "lucide-react"
4
+
5
+ import { AsterionSessionContextBadge } from "@/components/asterion-session-context-badge"
4
6
  import { Badge } from "@/components/ui/badge"
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
8
+ import { getNarrativeBeatAnchorId, parseSessionContextMetadata } from "@/lib/asterion-metadata"
5
9
  import type { EntityRecord } from "@/lib/types"
6
- import { Clock, Compass, Drama, Sparkles } from "lucide-react"
7
10
 
8
11
  interface NarrativeBeatsProps {
9
12
  beats: EntityRecord[]
@@ -29,8 +32,11 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
29
32
  const universes = beat.metadata.universes || []
30
33
  const timestamp = beat.metadata.timestamp ? new Date(beat.metadata.timestamp).toLocaleString() : null
31
34
 
35
+ const anchorId = getNarrativeBeatAnchorId(beat, idx)
36
+ const sessionContextProjection = parseSessionContextMetadata(beat, idx)
37
+
32
38
  return (
33
- <Card key={beat.name} className="border-l-4 border-l-chart-3">
39
+ <Card id={anchorId} key={beat.name} className="scroll-mt-24 border-l-4 border-l-chart-3">
34
40
  <CardHeader className="bg-chart-3/5">
35
41
  <div className="flex items-start justify-between gap-4">
36
42
  <div className="flex-1">
@@ -42,6 +48,10 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
42
48
  <Badge variant="outline" className="border-chart-3 text-chart-3">
43
49
  Beat {idx + 1}
44
50
  </Badge>
51
+ <a href={`#${anchorId}`} className="inline-flex items-center gap-1 text-xs text-muted-foreground underline">
52
+ <Link2 className="h-3 w-3" />
53
+ anchor
54
+ </a>
45
55
  </div>
46
56
  {timestamp && (
47
57
  <CardDescription className="flex items-center gap-1">
@@ -53,6 +63,9 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
53
63
  </div>
54
64
  </CardHeader>
55
65
  <CardContent className="space-y-4 pt-4">
66
+ {/* Session Context Badge */}
67
+ {sessionContextProjection && <AsterionSessionContextBadge projection={sessionContextProjection} />}
68
+
56
69
  {/* Universes */}
57
70
  <div className="flex items-center gap-2 flex-wrap">
58
71
  <Compass className="w-4 h-4 text-muted-foreground" />
package/index.tsx CHANGED
@@ -3,15 +3,24 @@
3
3
 
4
4
  // Types
5
5
  export type {
6
+ ArtifactDiffEntry,
7
+ Chart,
6
8
  EntityRecord,
7
- RelationRecord,
9
+ FoundationMetadata,
10
+ FoundationProjection,
8
11
  JSONLRecord,
9
- Chart,
10
12
  ParsedData,
13
+ ParsedIssueReference,
14
+ RelationRecord,
15
+ SessionLineageBranchProjection,
16
+ SessionLineageGroupProjection,
17
+ SessionLineageMetadata,
18
+ SessionLineageProjection,
11
19
  } from "./lib/types"
12
20
 
13
21
  // JSONL Parser
14
22
  export { parseJSONL, organizeData, getChartSummary, getChartProgress } from "./lib/jsonl-parser"
23
+ export { extractAsterionMetadata, getNarrativeBeatAnchorId, parseFoundationMetadata, parseSessionLineageMetadata } from "./lib/asterion-metadata"
15
24
 
16
25
  // GitHub provenance helpers
17
26
  export {
@@ -53,6 +62,8 @@ export { ChartDetail } from "./components/chart-detail"
53
62
  export { DataStats } from "./components/data-stats"
54
63
  export { CreateChartForm } from "./components/create-chart-form"
55
64
  export { NarrativeBeats } from "./components/narrative-beats"
65
+ export { AsterionFoundationCard } from "./components/asterion-foundation-card"
66
+ export { AsterionSessionLineageCard } from "./components/asterion-session-lineage-card"
56
67
  export { ActionStepIssueBadge, ProjectSourceBadge, SyncStatePill } from "./components/github-provenance"
57
68
  export { LiveIndicator } from "./components/live-indicator"
58
69
  export { ThemeToggle } from "./components/theme-toggle"