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 +2 -1
- package/components/asterion-foundation-card.tsx +175 -0
- package/components/asterion-session-context-badge.tsx +122 -0
- package/components/asterion-session-lineage-card.tsx +119 -0
- package/components/chart-detail.tsx +3 -0
- package/components/metadata-projections.tsx +50 -16
- package/components/narrative-beats.tsx +16 -3
- package/index.tsx +13 -2
- package/lib/asterion-metadata.ts +673 -0
- package/lib/chart-editor.ts +5 -0
- package/lib/jsonl-parser.ts +5 -0
- package/lib/types.ts +97 -0
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
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 = (
|
|
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
|
-
|
|
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 || "
|
|
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
|
-
|
|
210
|
+
Raw Metadata
|
|
182
211
|
</CardTitle>
|
|
183
212
|
</CardHeader>
|
|
184
213
|
<CardContent className="space-y-3">
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 {
|
|
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
|
-
|
|
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"
|