coaia-visualizer 1.6.3 → 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.
Files changed (54) hide show
  1. package/Dockerfile +61 -0
  2. package/Dockerfile.app +50 -0
  3. package/README.md +2 -1
  4. package/cli.ts +11 -5
  5. package/components/asterion-foundation-card.tsx +175 -0
  6. package/components/asterion-session-context-badge.tsx +122 -0
  7. package/components/asterion-session-lineage-card.tsx +119 -0
  8. package/components/chart-detail-editable.tsx +2 -0
  9. package/components/chart-detail.tsx +9 -1
  10. package/components/edit-action-step.tsx +2 -0
  11. package/components/github-provenance.tsx +226 -0
  12. package/components/metadata-projections.tsx +50 -16
  13. package/components/narrative-beats.tsx +16 -3
  14. package/components/relation-graph.tsx +91 -27
  15. package/dist/cli.js +9 -5
  16. package/docker-build-push.sh +20 -0
  17. package/docker-entrypoint.sh +27 -0
  18. package/index.tsx +39 -2
  19. package/lib/asterion-metadata.ts +673 -0
  20. package/lib/chart-editor.ts +8 -3
  21. package/lib/github-provenance.ts +316 -0
  22. package/lib/jsonl-parser.ts +7 -2
  23. package/lib/types.ts +112 -2
  24. package/mcp/test_mcp/.gemini/settings.json +18 -0
  25. package/next-env.d.ts +1 -1
  26. package/package.json +14 -13
  27. package/rispecs/README.md +170 -0
  28. package/rispecs/accountability-responsibility.rispec.md +110 -0
  29. package/rispecs/api-mcp-interface.spec.md +287 -0
  30. package/rispecs/app.spec.md +364 -0
  31. package/rispecs/chart-editing-workflow.spec.md +297 -0
  32. package/rispecs/chart-visualization-hierarchy.spec.md +235 -0
  33. package/rispecs/cli-mode.spec.md +224 -0
  34. package/rispecs/github-project-runtime-memory-integration.spec.md +381 -0
  35. package/rispecs/jsonl-parsing-data-types.spec.md +243 -0
  36. package/rispecs/narrative-beats-display.spec.md +189 -0
  37. package/rispecs/pde-integration.spec.md +280 -0
  38. package/rispecs/planning-integration.spec.md +329 -0
  39. package/rispecs/relation-graph-visualization.spec.md +171 -0
  40. package/rispecs/relation-to-mcp-structural-thinking.kin.md +65 -0
  41. package/rispecs/ui-component-library.spec.md +258 -0
  42. package/mcp/dist/api-client.d.ts +0 -138
  43. package/mcp/dist/api-client.d.ts.map +0 -1
  44. package/mcp/dist/api-client.js +0 -115
  45. package/mcp/dist/api-client.js.map +0 -1
  46. package/mcp/dist/index.d.ts +0 -2
  47. package/mcp/dist/index.d.ts.map +0 -1
  48. package/mcp/dist/index.js +0 -286
  49. package/mcp/dist/index.js.map +0 -1
  50. package/mcp/dist/tools/index.d.ts +0 -18
  51. package/mcp/dist/tools/index.d.ts.map +0 -1
  52. package/mcp/dist/tools/index.js +0 -322
  53. package/mcp/dist/tools/index.js.map +0 -1
  54. package/mcp/package-lock.json +0 -210
package/Dockerfile ADDED
@@ -0,0 +1,61 @@
1
+ # =============================================================================
2
+ # COAIA Visualizer — Docker Image
3
+ # jgwill/coaia:visualizer
4
+ #
5
+ # Usage:
6
+ # docker run --rm -p 4321:4321 \
7
+ # -v /abs/path/to/file.jsonl:/data/memory.jsonl \
8
+ # jgwill/coaia:visualizer
9
+ # =============================================================================
10
+
11
+ # ---- Build stage ----
12
+ FROM node:22-alpine AS builder
13
+
14
+ WORKDIR /app
15
+
16
+ # Install pnpm
17
+ RUN corepack enable && corepack prepare pnpm@latest --activate
18
+
19
+ # Copy lockfile + manifests first (layer cache)
20
+ COPY package.json pnpm-lock.yaml ./
21
+
22
+ # Install all deps — skip prepare hook (cli.ts not copied yet)
23
+ RUN pnpm install --no-frozen-lockfile --ignore-scripts
24
+
25
+ # Copy source
26
+ COPY . .
27
+
28
+ # Build CLI + Next.js
29
+ RUN pnpm run prepare && pnpm build
30
+
31
+ # ---- Runner stage ----
32
+ FROM node:22-alpine AS runner
33
+
34
+ WORKDIR /app
35
+
36
+ RUN corepack enable && corepack prepare pnpm@latest --activate
37
+
38
+ # Copy manifests + node_modules from builder, then prune dev deps
39
+ COPY package.json pnpm-lock.yaml ./
40
+ COPY --from=builder /app/node_modules ./node_modules
41
+ RUN pnpm prune --prod 2>/dev/null || true
42
+
43
+ # Copy built assets from builder
44
+ COPY --from=builder /app/.next ./.next
45
+ COPY --from=builder /app/public ./public
46
+ COPY --from=builder /app/next.config.mjs ./next.config.mjs
47
+
48
+ # Entrypoint script
49
+ COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
50
+ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
51
+
52
+ # Volume for user JSONL files
53
+ VOLUME ["/data"]
54
+
55
+ ENV NODE_ENV=production
56
+ ENV PORT=4321
57
+ ENV COAIAV_MEMORY_PATH=/data/memory.jsonl
58
+
59
+ EXPOSE 4321
60
+
61
+ ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
package/Dockerfile.app ADDED
@@ -0,0 +1,50 @@
1
+ # Build stage
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package.json pnpm-lock.yaml ./
8
+
9
+ # Install pnpm and dependencies
10
+ RUN npm install -g pnpm@latest && \
11
+ pnpm install --frozen-lockfile
12
+
13
+ # Copy application code
14
+ COPY . .
15
+
16
+ # Build Next.js application
17
+ RUN pnpm build
18
+
19
+ # Production stage
20
+ FROM node:20-alpine AS runner
21
+
22
+ WORKDIR /app
23
+
24
+ # Install pnpm
25
+ RUN npm install -g pnpm@latest
26
+
27
+ # Copy package files
28
+ COPY package.json pnpm-lock.yaml ./
29
+
30
+ # Install production dependencies only, skip prepare hook
31
+ RUN pnpm install --prod --frozen-lockfile --ignore-scripts
32
+
33
+ # Copy built application from builder
34
+ COPY --from=builder /app/.next ./.next
35
+ COPY --from=builder /app/public ./public
36
+ COPY --from=builder /app/next.config.mjs ./
37
+ COPY --from=builder /app/app ./app
38
+ COPY --from=builder /app/components ./components
39
+ COPY --from=builder /app/lib ./lib
40
+ COPY --from=builder /app/dist ./dist
41
+
42
+ # Create data directories
43
+ RUN mkdir -p /app/samples /app/test-data
44
+
45
+ ENV NODE_ENV=production
46
+ ENV PORT=4321
47
+
48
+ EXPOSE 4321
49
+
50
+ CMD ["pnpm", "start"]
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
 
package/cli.ts CHANGED
@@ -88,7 +88,7 @@ function loadConfig(args: minimist.ParsedArgs): Config {
88
88
  if (args['port'] || args['p']) {
89
89
  config.port = args['port'] || args['p'];
90
90
  }
91
- if (args['no-open']) {
91
+ if (args['no-open'] || args.open === false) {
92
92
  config.noOpen = true;
93
93
  }
94
94
  if (args['live']) {
@@ -242,11 +242,14 @@ EXAMPLES:
242
242
  }, 4000);
243
243
  }
244
244
 
245
- process.on('SIGINT', () => {
245
+ const shutdownDocker = () => {
246
246
  console.log('\n👋 Shutting down visualizer...');
247
247
  dockerProcess.kill();
248
248
  process.exit(0);
249
- });
249
+ };
250
+
251
+ process.on('SIGINT', shutdownDocker);
252
+ process.on('SIGTERM', shutdownDocker);
250
253
 
251
254
  dockerProcess.on('exit', (code) => { process.exit(code || 0); });
252
255
  return;
@@ -295,11 +298,14 @@ EXAMPLES:
295
298
  }
296
299
 
297
300
  // Handle shutdown
298
- process.on('SIGINT', () => {
301
+ const shutdownLocal = () => {
299
302
  console.log('\n👋 Shutting down visualizer...');
300
303
  nextProcess.kill();
301
304
  process.exit(0);
302
- });
305
+ };
306
+
307
+ process.on('SIGINT', shutdownLocal);
308
+ process.on('SIGTERM', shutdownLocal);
303
309
 
304
310
  nextProcess.on('exit', (code) => {
305
311
  process.exit(code || 0);
@@ -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
+ }
@@ -17,6 +17,7 @@ import { EditActionStep } from "./edit-action-step"
17
17
  import { AddActionStep } from "./add-action-step"
18
18
  import { EditChartDueDate } from "./edit-chart-due-date"
19
19
  import { ChartEditor } from "@/lib/chart-editor"
20
+ import { ProjectSourceBadge } from "./github-provenance"
20
21
 
21
22
  interface ChartDetailEditableProps {
22
23
  chart: Chart
@@ -89,6 +90,7 @@ export function ChartDetailEditable({
89
90
  <div className="flex items-center gap-4 text-sm text-muted-foreground">
90
91
  <span>Created {createdDate}</span>
91
92
  </div>
93
+ <ProjectSourceBadge entity={chart.chartEntity} className="mt-3" />
92
94
  </div>
93
95
  <div>
94
96
  <EditChartDueDate
@@ -7,7 +7,9 @@ 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"
12
+ import { ActionStepIssueBadge, ProjectSourceBadge } from "./github-provenance"
11
13
 
12
14
  interface ChartDetailProps {
13
15
  chart: Chart
@@ -41,6 +43,7 @@ export function ChartDetail({ chart, data }: ChartDetailProps) {
41
43
  </div>
42
44
  )}
43
45
  </div>
46
+ <ProjectSourceBadge entity={chart.chartEntity} className="mt-3" />
44
47
  </div>
45
48
  </div>
46
49
  </CardHeader>
@@ -140,6 +143,8 @@ export function ChartDetail({ chart, data }: ChartDetailProps) {
140
143
  </div>
141
144
  )}
142
145
 
146
+ <MetadataProjections chart={chart} data={data} />
147
+
143
148
  {/* Sub-charts */}
144
149
  {chart.subCharts.length > 0 && (
145
150
  <Card>
@@ -194,7 +199,10 @@ function ActionItem({ action, index }: ActionItemProps) {
194
199
  </div>
195
200
  <div className="flex-1 min-w-0">
196
201
  <div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-2 mb-1">
197
- <span className="text-xs md:text-sm font-medium">Action {index}</span>
202
+ <div className="flex flex-wrap items-center gap-2">
203
+ <span className="text-xs md:text-sm font-medium">Action {index}</span>
204
+ <ActionStepIssueBadge action={action} />
205
+ </div>
198
206
  {dueDate && (
199
207
  <div className="flex items-center gap-1 text-xs text-muted-foreground">
200
208
  <Calendar className="w-3 h-3" />
@@ -8,6 +8,7 @@ import { Calendar } from "@/components/ui/calendar"
8
8
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
9
9
  import { format } from "date-fns"
10
10
  import type { EntityRecord } from "@/lib/types"
11
+ import { ActionStepIssueBadge } from "@/components/github-provenance"
11
12
 
12
13
  interface EditActionStepProps {
13
14
  action: EntityRecord
@@ -113,6 +114,7 @@ export function EditActionStep({
113
114
  <div className="flex items-start justify-between gap-2">
114
115
  <div className="flex items-center gap-2 flex-wrap">
115
116
  <span className="text-sm font-medium">Action {index}</span>
117
+ <ActionStepIssueBadge action={action} />
116
118
  {isTelescopedChart && (
117
119
  <span className="text-xs bg-primary text-primary-foreground px-2.5 py-1 rounded-md font-semibold shadow-sm">
118
120
  📊 Chart