@underdogai/mesh-event-schemas 0.1.0

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/apppacks.d.ts +15652 -0
  4. package/dist/apppacks.d.ts.map +1 -0
  5. package/dist/apppacks.js +71 -0
  6. package/dist/apppacks.js.map +1 -0
  7. package/dist/blocks.d.ts +2320 -0
  8. package/dist/blocks.d.ts.map +1 -0
  9. package/dist/blocks.js +472 -0
  10. package/dist/blocks.js.map +1 -0
  11. package/dist/capabilities.d.ts +35 -0
  12. package/dist/capabilities.d.ts.map +1 -0
  13. package/dist/capabilities.js +37 -0
  14. package/dist/capabilities.js.map +1 -0
  15. package/dist/events.d.ts +4557 -0
  16. package/dist/events.d.ts.map +1 -0
  17. package/dist/events.js +571 -0
  18. package/dist/events.js.map +1 -0
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +16 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/namespace.d.ts +56 -0
  24. package/dist/namespace.d.ts.map +1 -0
  25. package/dist/namespace.js +60 -0
  26. package/dist/namespace.js.map +1 -0
  27. package/dist/plugins.d.ts +131 -0
  28. package/dist/plugins.d.ts.map +1 -0
  29. package/dist/plugins.js +66 -0
  30. package/dist/plugins.js.map +1 -0
  31. package/dist/registry.d.ts +67 -0
  32. package/dist/registry.d.ts.map +1 -0
  33. package/dist/registry.js +212 -0
  34. package/dist/registry.js.map +1 -0
  35. package/dist/surfaces.d.ts +54323 -0
  36. package/dist/surfaces.d.ts.map +1 -0
  37. package/dist/surfaces.js +523 -0
  38. package/dist/surfaces.js.map +1 -0
  39. package/dist/templates.d.ts +390 -0
  40. package/dist/templates.d.ts.map +1 -0
  41. package/dist/templates.js +493 -0
  42. package/dist/templates.js.map +1 -0
  43. package/package.json +50 -0
  44. package/src/apppacks.ts +90 -0
  45. package/src/blocks.ts +544 -0
  46. package/src/capabilities.ts +58 -0
  47. package/src/events.ts +758 -0
  48. package/src/index.ts +15 -0
  49. package/src/namespace.ts +64 -0
  50. package/src/plugins.ts +118 -0
  51. package/src/registry.ts +261 -0
  52. package/src/surfaces.ts +610 -0
  53. package/src/templates.ts +736 -0
@@ -0,0 +1,736 @@
1
+ /**
2
+ * Composite 템플릿 — 프리미티브 블록을 조합한 고수준 카드 빌더 (작성자 DX).
3
+ *
4
+ * 작성자는 매번 블록 트리를 손으로 쌓지 않고 templates.approval(...) 처럼 의도 단위로 만든다.
5
+ * registerTemplate() 로 작성자가 자기 템플릿을 등록할 수도 있다(클라이언트 릴리스 불필요 —
6
+ * 템플릿은 프리미티브로 "확장"되어 기존 렌더러가 그대로 그린다).
7
+ */
8
+ import { buildUiCardContent, type TaskStatus, type TaskSummary, type UiCardContent } from "./events.js";
9
+ import type { Block, ButtonStyle, Tone } from "./blocks.js";
10
+
11
+ export type Template<P = unknown> = (params: P) => UiCardContent;
12
+
13
+ const templateRegistry = new Map<string, Template>();
14
+
15
+ export function registerTemplate<P>(name: string, fn: Template<P>): void {
16
+ templateRegistry.set(name, fn as Template);
17
+ }
18
+ export function hasTemplate(name: string): boolean {
19
+ return templateRegistry.has(name);
20
+ }
21
+ export function renderTemplate(name: string, params: unknown): UiCardContent {
22
+ const fn = templateRegistry.get(name);
23
+ if (!fn) throw new Error(`Unknown template: ${name}`);
24
+ return fn(params);
25
+ }
26
+
27
+ // --- 내장 템플릿 ---
28
+
29
+ export interface ApprovalParams {
30
+ card_id: string;
31
+ title?: string;
32
+ fields: { label: string; value: string; short?: boolean }[];
33
+ note?: boolean;
34
+ approveText?: string;
35
+ rejectText?: string;
36
+ confirm?: boolean;
37
+ context?: UiCardContent["context"];
38
+ }
39
+
40
+ function approval(p: ApprovalParams): UiCardContent {
41
+ const blocks: Block[] = [{ type: "fields", items: p.fields }];
42
+ if (p.note) blocks.push({ type: "input", name: "note", label: "Note", placeholder: "Optional" });
43
+ blocks.push({
44
+ type: "actions",
45
+ elements: [
46
+ {
47
+ type: "button",
48
+ action_id: "approve",
49
+ text: p.approveText ?? "Approve",
50
+ style: "primary",
51
+ ...(p.confirm ? { confirm: { title: "Approve", text: "Approve this?" } } : {}),
52
+ },
53
+ { type: "button", action_id: "reject", text: p.rejectText ?? "Reject", style: "danger" },
54
+ ],
55
+ });
56
+ return buildUiCardContent({
57
+ card_id: p.card_id,
58
+ title: p.title,
59
+ fallback: p.title ?? "Approval request",
60
+ context: p.context,
61
+ blocks,
62
+ });
63
+ }
64
+
65
+ export interface MultiApprovalParams {
66
+ card_id: string;
67
+ title?: string;
68
+ /** 요청 요약 필드(금액·대상 등). */
69
+ fields?: { label: string; value: string; short?: boolean }[];
70
+ /** 순차 승인자 목록(순서대로). status 미지정 시 pending(아직 결재 전). */
71
+ approvers: { name: string; status?: "approved" | "rejected" | "active" | "pending"; note?: string }[];
72
+ /** 통과에 필요한 최소 승인 수(미지정 시 approvers 전원). */
73
+ required?: number;
74
+ /** 진행 안내/메모(markdown). */
75
+ note?: string;
76
+ /** 액션(미지정 시 Approve/Reject 기본). 종료 상태면 호출부에서 [] 전달해 숨긴다. */
77
+ actions?: ActionDef[];
78
+ state?: UiCardContent["state"];
79
+ context?: UiCardContent["context"];
80
+ }
81
+
82
+ /**
83
+ * 다단계 승인 카드 (HITL) — 0/N 승인 카운터(metric) + 순차 승인자 타임라인(steps) + 액션.
84
+ * approval.request/.decision 이벤트의 표시면. 승인자 status 가 done/active/error/todo 로 매핑되고
85
+ * approved/required 카운터가 진행도를 보여준다(색-only 회피: 숫자 병기).
86
+ */
87
+ function multiApproval(p: MultiApprovalParams): UiCardContent {
88
+ const required = p.required ?? p.approvers.length;
89
+ const approved = p.approvers.filter((a) => a.status === "approved").length;
90
+ const rejected = p.approvers.some((a) => a.status === "rejected");
91
+ const blocks: Block[] = [];
92
+ if (p.fields?.length) blocks.push({ type: "fields", items: p.fields });
93
+ blocks.push({
94
+ type: "metric",
95
+ label: "Approvals",
96
+ value: `${approved}/${required}`,
97
+ tone: rejected ? "danger" : approved >= required ? "success" : "info",
98
+ });
99
+ blocks.push({
100
+ type: "steps",
101
+ items: p.approvers.map((a) => ({
102
+ label: a.name,
103
+ status:
104
+ a.status === "approved"
105
+ ? ("done" as const)
106
+ : a.status === "rejected"
107
+ ? ("error" as const)
108
+ : a.status === "active"
109
+ ? ("active" as const)
110
+ : ("todo" as const),
111
+ note: a.note,
112
+ })),
113
+ });
114
+ if (p.note) blocks.push({ type: "text", text: p.note, markdown: true });
115
+ const row = actionRow(
116
+ p.actions ?? [
117
+ { action_id: "approve", text: "Approve", style: "primary" },
118
+ { action_id: "reject", text: "Reject", style: "danger" },
119
+ ],
120
+ );
121
+ if (row) blocks.push(row);
122
+ return buildUiCardContent({
123
+ card_id: p.card_id,
124
+ title: p.title ?? "Approval workflow",
125
+ fallback: p.title ?? `Approval ${approved}/${required}`,
126
+ state: p.state,
127
+ context: p.context,
128
+ blocks,
129
+ });
130
+ }
131
+
132
+ /** 작업 상태별 텍스트 라벨(색-only 회피 — 단계 note 에 병기). */
133
+ const TASK_STATUS_LABEL: Record<TaskStatus, string> = {
134
+ todo: "To do",
135
+ in_progress: "In progress",
136
+ blocked: "Blocked",
137
+ done: "Done",
138
+ cancelled: "Cancelled",
139
+ };
140
+
141
+ /**
142
+ * 작업 상태 → steps 블록 상태(done|active|todo|error) 매핑.
143
+ * blocked→error(빨강·주의 환기)·cancelled→todo(회색·비강조) — 둘은 색이 달라 구분되고, 정확한 상태는
144
+ * 단계 note 텍스트로 병기한다(steps 블록엔 상태 칩이 없어 색만으로 구분 금지 원칙을 텍스트로 충족).
145
+ */
146
+ const TASK_STEP_STATUS: Record<TaskStatus, "done" | "active" | "todo" | "error"> = {
147
+ done: "done",
148
+ in_progress: "active",
149
+ blocked: "error",
150
+ todo: "todo",
151
+ cancelled: "todo",
152
+ };
153
+
154
+ export interface TaskListParams {
155
+ card_id: string;
156
+ title?: string;
157
+ /** summarizeTasks(creates, updates) 의 결과 — 카운트/공정률을 손으로 박지 않고 데이터로 받는다. */
158
+ summary: TaskSummary;
159
+ actions?: ActionDef[];
160
+ state?: UiCardContent["state"];
161
+ context?: UiCardContent["context"];
162
+ }
163
+
164
+ /**
165
+ * 작업 진척 카드 (C층 프리미티브) — task.* 이벤트에서 파생한 TaskSummary 를 0/N metric + 전체 progress +
166
+ * 작업별 steps 로 렌더. 공정률·카운트는 모두 summary(데이터)에서 오고 카드는 손데이터를 만들지 않는다
167
+ * (done_when "진척도 실데이터"). 상태는 단계 note 에 텍스트로 병기(접근성). multiApproval(0/N)·checklist 와 동형.
168
+ */
169
+ function taskList(p: TaskListParams): UiCardContent {
170
+ const s = p.summary;
171
+ // 완료 카운터의 분모는 공정률과 같은 기준(cancelled 제외) — "1/4"와 25% 가 어긋나지 않게.
172
+ const countable = s.total - s.cancelled;
173
+ const blocks: Block[] = [
174
+ {
175
+ type: "metric",
176
+ label: "Done",
177
+ value: `${s.done}/${countable}`,
178
+ tone: s.percent === 100 && countable > 0 ? "success" : s.byStatus.blocked > 0 ? "warning" : "info",
179
+ },
180
+ { type: "progress", value: s.percent, label: `${s.percent}% complete` },
181
+ {
182
+ type: "steps",
183
+ items: s.tasks.map((t) => {
184
+ // note = 상태 라벨(색-only 회피) + (진행 중이면 %) + 담당자 + 메모 — 한 줄 맥락.
185
+ const parts = [TASK_STATUS_LABEL[t.status]];
186
+ if (t.status === "in_progress" && t.progress != null) parts.push(`${t.progress}%`);
187
+ if (t.assignees?.length) parts.push(t.assignees.join(", "));
188
+ if (t.note) parts.push(t.note);
189
+ return { label: t.title, status: TASK_STEP_STATUS[t.status], note: parts.join(" · ") };
190
+ }),
191
+ },
192
+ ];
193
+ const row = actionRow(p.actions);
194
+ if (row) blocks.push(row);
195
+ return buildUiCardContent({
196
+ card_id: p.card_id,
197
+ title: p.title ?? "Tasks",
198
+ // fallback 도 카드 metric 과 같은 분모(cancelled 제외)를 써 미지원 클라에서도 카운트가 일치한다.
199
+ fallback: p.title ?? `${s.done}/${countable} tasks done`,
200
+ state: p.state,
201
+ context: p.context,
202
+ blocks,
203
+ });
204
+ }
205
+
206
+ export interface WizardParams {
207
+ card_id: string;
208
+ title?: string;
209
+ steps: { title?: string; blocks: Block[] }[];
210
+ submitId?: string;
211
+ submitText?: string;
212
+ }
213
+
214
+ function wizard(p: WizardParams): UiCardContent {
215
+ return buildUiCardContent({
216
+ card_id: p.card_id,
217
+ title: p.title,
218
+ fallback: p.title ?? "Step-by-step form",
219
+ steps: p.steps,
220
+ blocks: [
221
+ {
222
+ type: "actions",
223
+ elements: [
224
+ { type: "button", action_id: p.submitId ?? "submit", text: p.submitText ?? "Submit", style: "primary" },
225
+ ],
226
+ },
227
+ ],
228
+ });
229
+ }
230
+
231
+ export interface ListParams {
232
+ card_id: string;
233
+ title?: string;
234
+ columns: string[];
235
+ rows: string[][];
236
+ }
237
+
238
+ function list(p: ListParams): UiCardContent {
239
+ return buildUiCardContent({
240
+ card_id: p.card_id,
241
+ title: p.title,
242
+ fallback: p.title ?? "List",
243
+ blocks: [{ type: "table", columns: p.columns, rows: p.rows }],
244
+ });
245
+ }
246
+
247
+ export interface ProgressParams {
248
+ card_id: string;
249
+ title?: string;
250
+ value?: number;
251
+ label?: string;
252
+ steps?: { label: string; status?: "done" | "active" | "todo" | "error"; note?: string }[];
253
+ state?: UiCardContent["state"];
254
+ }
255
+
256
+ function progress(p: ProgressParams): UiCardContent {
257
+ const blocks: Block[] = [{ type: "progress", value: p.value, label: p.label }];
258
+ if (p.steps) blocks.push({ type: "steps", items: p.steps });
259
+ return buildUiCardContent({
260
+ card_id: p.card_id,
261
+ title: p.title,
262
+ fallback: p.title ?? "Progress",
263
+ state: p.state,
264
+ blocks,
265
+ });
266
+ }
267
+
268
+ export interface InfoParams {
269
+ card_id: string;
270
+ title?: string;
271
+ badge?: { text: string; tone?: "neutral" | "success" | "warning" | "danger" | "info" };
272
+ fields: { label: string; value: string; short?: boolean }[];
273
+ note?: string;
274
+ context?: UiCardContent["context"];
275
+ }
276
+
277
+ /** 정보/조사 결과 카드 — (선택) badge + 키-값 fields + (선택) 메모 텍스트. */
278
+ function info(p: InfoParams): UiCardContent {
279
+ const blocks: Block[] = [];
280
+ if (p.badge) blocks.push({ type: "badge", text: p.badge.text, tone: p.badge.tone });
281
+ blocks.push({ type: "fields", items: p.fields });
282
+ if (p.note) blocks.push({ type: "text", text: p.note, markdown: true });
283
+ return buildUiCardContent({
284
+ card_id: p.card_id,
285
+ title: p.title,
286
+ fallback: p.title ?? "Info",
287
+ context: p.context,
288
+ blocks,
289
+ });
290
+ }
291
+
292
+ export interface DocParams {
293
+ card_id: string;
294
+ title?: string;
295
+ fileName: string;
296
+ fileMeta?: string;
297
+ summary?: string;
298
+ previewId?: string;
299
+ previewText?: string;
300
+ context?: UiCardContent["context"];
301
+ }
302
+
303
+ /** 문서/파일 카드 — 파일명·메타 fields + (선택) 요약 + 미리보기 액션. */
304
+ function doc(p: DocParams): UiCardContent {
305
+ const items = [{ label: "File", value: p.fileName, short: true }];
306
+ if (p.fileMeta) items.push({ label: "Details", value: p.fileMeta, short: true });
307
+ const blocks: Block[] = [{ type: "fields", items }];
308
+ if (p.summary) blocks.push({ type: "text", text: p.summary });
309
+ blocks.push({
310
+ type: "actions",
311
+ elements: [{ type: "button", action_id: p.previewId ?? "preview", text: p.previewText ?? "Preview" }],
312
+ });
313
+ return buildUiCardContent({
314
+ card_id: p.card_id,
315
+ title: p.title,
316
+ fallback: p.title ?? p.fileName,
317
+ context: p.context,
318
+ blocks,
319
+ });
320
+ }
321
+
322
+ export interface PlanParams {
323
+ card_id: string;
324
+ title?: string;
325
+ value?: number;
326
+ label?: string;
327
+ steps: { label: string; status?: "done" | "active" | "todo" | "error"; note?: string }[];
328
+ context?: UiCardContent["context"];
329
+ }
330
+
331
+ /** 에이전트 실행 계획 카드 — progress + steps 타임라인 (Agent Plan). */
332
+ function plan(p: PlanParams): UiCardContent {
333
+ return buildUiCardContent({
334
+ card_id: p.card_id,
335
+ title: p.title,
336
+ fallback: p.title ?? "Execution plan",
337
+ context: p.context,
338
+ blocks: [
339
+ // tone 미지정 → 기본 브랜드(보라) 채움. info 톤이 필요하면 호출부에서 지정. (DESIGN.md §5.5)
340
+ { type: "progress", value: p.value, label: p.label },
341
+ { type: "steps", items: p.steps },
342
+ ],
343
+ });
344
+ }
345
+
346
+ export interface FormParams {
347
+ card_id: string;
348
+ title?: string;
349
+ /** input/radio/select/checkbox/text 등 임의 블록 — 마지막에 제출 액션이 붙는다. */
350
+ blocks: Block[];
351
+ submitId?: string;
352
+ submitText?: string;
353
+ context?: UiCardContent["context"];
354
+ }
355
+
356
+ /** 단일 단계 입력 폼 카드 — 임의 입력 블록 + 제출 액션(마법사 wizard 와 달리 단일 화면). */
357
+ function form(p: FormParams): UiCardContent {
358
+ return buildUiCardContent({
359
+ card_id: p.card_id,
360
+ title: p.title,
361
+ fallback: p.title ?? "Form",
362
+ context: p.context,
363
+ blocks: [
364
+ ...p.blocks,
365
+ {
366
+ type: "actions",
367
+ elements: [
368
+ { type: "button", action_id: p.submitId ?? "submit", text: p.submitText ?? "Submit", style: "primary" },
369
+ ],
370
+ },
371
+ ],
372
+ });
373
+ }
374
+
375
+ // 도메인색이 강한 SCM(Forgejo) 템플릿(scmRef/prReview/ciRun)은 코어가 아니라 plugin-forgejo 가
376
+ // 소유한다 — register() 에서 registerTemplate 로 등록(packages/plugin-forgejo/src/templates.ts).
377
+ // 코어 templates.* 는 도메인 무관 프리미티브만 유지한다("도메인=플러그인" 가드레일).
378
+
379
+ export interface ScorecardParams {
380
+ card_id: string;
381
+ title?: string;
382
+ progress?: number;
383
+ progressLabel?: string;
384
+ /** 기준(DoD) 체크리스트 */
385
+ criteria?: { label: string; done: boolean }[];
386
+ /** 임팩트 지표 */
387
+ metrics?: { label: string; value: string; target?: string; delta?: string; tone?: Tone }[];
388
+ context?: UiCardContent["context"];
389
+ }
390
+
391
+ /** 진척도·임팩트 스코어카드 — 공정률(progress) + 기준 체크리스트 + 임팩트 KPI(metric). */
392
+ function scorecard(p: ScorecardParams): UiCardContent {
393
+ const blocks: Block[] = [];
394
+ if (p.progress != null || p.progressLabel) {
395
+ blocks.push({ type: "progress", value: p.progress, label: p.progressLabel });
396
+ }
397
+ for (const m of p.metrics ?? []) {
398
+ blocks.push({ type: "metric", label: m.label, value: m.value, target: m.target, delta: m.delta, tone: m.tone });
399
+ }
400
+ if (p.criteria?.length) {
401
+ blocks.push({
402
+ type: "steps",
403
+ items: p.criteria.map((c) => ({ label: c.label, status: c.done ? ("done" as const) : ("todo" as const) })),
404
+ });
405
+ }
406
+ return buildUiCardContent({
407
+ card_id: p.card_id,
408
+ title: p.title ?? "Progress · Impact",
409
+ fallback: p.title ?? "Progress/Impact",
410
+ context: p.context,
411
+ blocks,
412
+ });
413
+ }
414
+
415
+ // --- 도메인 템플릿 1차 (Phase 1 신규 블록 활용) ---
416
+
417
+ /** 액션 버튼 정의(템플릿 공용). */
418
+ export type ActionDef = { action_id: string; text: string; style?: ButtonStyle; value?: string };
419
+
420
+ /** ActionDef[] → actions 블록(없으면 null). */
421
+ function actionRow(actions?: ActionDef[]): Block | null {
422
+ if (!actions?.length) return null;
423
+ return {
424
+ type: "actions",
425
+ elements: actions.map((a) => ({ type: "button", action_id: a.action_id, text: a.text, style: a.style, value: a.value })),
426
+ };
427
+ }
428
+
429
+ export interface ContactCardParams {
430
+ card_id: string;
431
+ title?: string;
432
+ name: string;
433
+ /** 직함/역할(아바타 보조텍스트). */
434
+ role?: string;
435
+ status?: "online" | "away" | "busy" | "offline";
436
+ fields?: { label: string; value: string; short?: boolean }[];
437
+ /** 연결된 엔티티(예: CRM 리드) 칩. */
438
+ ref?: { entity?: string; label: string; url?: string };
439
+ actions?: ActionDef[];
440
+ context?: UiCardContent["context"];
441
+ }
442
+
443
+ /** 인물 카드 — 아바타 + 연락 fields + (선택) 엔티티 칩 + 액션. */
444
+ function contactCard(p: ContactCardParams): UiCardContent {
445
+ const blocks: Block[] = [{ type: "avatar", name: p.name, subtitle: p.role, status: p.status }];
446
+ if (p.fields?.length) blocks.push({ type: "fields", items: p.fields });
447
+ if (p.ref) blocks.push({ type: "entityRef", entity: p.ref.entity, label: p.ref.label, url: p.ref.url, tone: "info" });
448
+ const row = actionRow(p.actions);
449
+ if (row) blocks.push(row);
450
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? p.name, context: p.context, blocks });
451
+ }
452
+
453
+ export interface DigestParams {
454
+ card_id: string;
455
+ title?: string;
456
+ summary: string;
457
+ /** 핵심 포인트(불릿). */
458
+ points?: string[];
459
+ /** 출처 — 칩/링크. */
460
+ sources?: { label: string; url?: string }[];
461
+ actions?: ActionDef[];
462
+ context?: UiCardContent["context"];
463
+ }
464
+
465
+ /** AI 브리핑 카드 — 요약 + 포인트 + 출처 칩 + 액션. */
466
+ function digest(p: DigestParams): UiCardContent {
467
+ const blocks: Block[] = [{ type: "text", text: p.summary, markdown: true }];
468
+ if (p.points?.length) blocks.push({ type: "text", text: p.points.map((pt) => `- ${pt}`).join("\n"), markdown: true });
469
+ if (p.sources?.length) {
470
+ blocks.push({ type: "context", elements: ["Sources"] });
471
+ for (const s of p.sources) blocks.push({ type: "entityRef", entity: "source", label: s.label, url: s.url });
472
+ }
473
+ const row = actionRow(p.actions);
474
+ if (row) blocks.push(row);
475
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Digest", context: p.context, blocks });
476
+ }
477
+
478
+ export interface IncidentParams {
479
+ card_id: string;
480
+ title?: string;
481
+ /** 심각도 라벨(예: "SEV1"). */
482
+ severity?: string;
483
+ /** 심각도 톤(기본 warning). */
484
+ tone?: Tone;
485
+ summary?: string;
486
+ fields?: { label: string; value: string; short?: boolean }[];
487
+ steps?: { label: string; status?: "done" | "active" | "todo" | "error"; note?: string }[];
488
+ actions?: ActionDef[];
489
+ context?: UiCardContent["context"];
490
+ }
491
+
492
+ /** 인시던트 카드 — 심각도 콜아웃 + 메타 fields + 대응 steps + 액션. */
493
+ function incident(p: IncidentParams): UiCardContent {
494
+ const blocks: Block[] = [
495
+ { type: "callout", tone: p.tone ?? "warning", title: p.severity ?? "Incident", text: p.summary ?? p.title ?? "Incident reported" },
496
+ ];
497
+ if (p.fields?.length) blocks.push({ type: "fields", items: p.fields });
498
+ if (p.steps?.length) blocks.push({ type: "steps", items: p.steps });
499
+ const row = actionRow(p.actions);
500
+ if (row) blocks.push(row);
501
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? p.severity ?? "Incident", context: p.context, blocks });
502
+ }
503
+
504
+ export interface KpiGridParams {
505
+ card_id: string;
506
+ title?: string;
507
+ metrics: { label: string; value: string; target?: string; delta?: string; tone?: Tone }[];
508
+ /** 선택 차트(추세). */
509
+ chart?: {
510
+ variant: "sparkline" | "bar" | "line" | "donut";
511
+ data: { label?: string; value: number; tone?: Tone }[];
512
+ caption?: string;
513
+ unit?: string;
514
+ };
515
+ context?: UiCardContent["context"];
516
+ }
517
+
518
+ /** KPI 그리드 카드 — metric 묶음 + (선택) 추세 차트. */
519
+ function kpiGrid(p: KpiGridParams): UiCardContent {
520
+ const blocks: Block[] = p.metrics.map((m) => ({
521
+ type: "metric",
522
+ label: m.label,
523
+ value: m.value,
524
+ target: m.target,
525
+ delta: m.delta,
526
+ tone: m.tone,
527
+ }));
528
+ if (p.chart) {
529
+ blocks.push({ type: "chart", variant: p.chart.variant, data: p.chart.data, caption: p.chart.caption, unit: p.chart.unit });
530
+ }
531
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "KPIs", context: p.context, blocks });
532
+ }
533
+
534
+ // --- 도메인 템플릿 2차 ---
535
+
536
+ export interface InvoiceParams {
537
+ card_id: string;
538
+ title?: string;
539
+ number?: string;
540
+ vendor?: string;
541
+ amount: string;
542
+ due?: string;
543
+ /** 상태 배지(예: {label:"Pending", tone:"warning"}). */
544
+ status?: { label: string; tone?: Tone };
545
+ /** 액션(미지정 시 Approve/Reject 기본). */
546
+ actions?: ActionDef[];
547
+ context?: UiCardContent["context"];
548
+ }
549
+
550
+ /** 송장 카드 — 금액·기한 등 fields + (선택) 상태 배지 + 승인/반려 액션. */
551
+ function invoice(p: InvoiceParams): UiCardContent {
552
+ const blocks: Block[] = [];
553
+ if (p.status) blocks.push({ type: "badge", text: p.status.label, tone: p.status.tone });
554
+ const items: { label: string; value: string; short?: boolean }[] = [];
555
+ if (p.number) items.push({ label: "Invoice", value: p.number, short: true });
556
+ if (p.vendor) items.push({ label: "Vendor", value: p.vendor, short: true });
557
+ items.push({ label: "Amount", value: p.amount, short: true });
558
+ if (p.due) items.push({ label: "Due", value: p.due, short: true });
559
+ blocks.push({ type: "fields", items });
560
+ const row = actionRow(
561
+ p.actions ?? [
562
+ { action_id: "approve", text: "Approve", style: "primary" },
563
+ { action_id: "reject", text: "Reject", style: "danger" },
564
+ ],
565
+ );
566
+ if (row) blocks.push(row);
567
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? `Invoice ${p.number ?? ""}`.trim(), context: p.context, blocks });
568
+ }
569
+
570
+ export interface EstimateParams {
571
+ card_id: string;
572
+ title?: string;
573
+ lines: { label: string; qty?: string; amount: string }[];
574
+ total?: string;
575
+ note?: string;
576
+ actions?: ActionDef[];
577
+ context?: UiCardContent["context"];
578
+ }
579
+
580
+ /** 견적 카드 — 라인아이템 table + (선택) 합계 metric + 액션. */
581
+ function estimate(p: EstimateParams): UiCardContent {
582
+ const blocks: Block[] = [
583
+ { type: "table", columns: ["Item", "Qty", "Amount"], rows: p.lines.map((l) => [l.label, l.qty ?? "", l.amount]) },
584
+ ];
585
+ if (p.total) blocks.push({ type: "metric", label: "Total", value: p.total, tone: "info" });
586
+ if (p.note) blocks.push({ type: "text", text: p.note });
587
+ const row = actionRow(p.actions);
588
+ if (row) blocks.push(row);
589
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Estimate", context: p.context, blocks });
590
+ }
591
+
592
+ export interface ComparisonParams {
593
+ card_id: string;
594
+ title?: string;
595
+ /** 비교 대상 열 제목(예: ["Plan A","Plan B"]). */
596
+ options: string[];
597
+ /** 각 기준 행: label + 옵션 수만큼의 값. */
598
+ rows: { label: string; values: string[] }[];
599
+ context?: UiCardContent["context"];
600
+ }
601
+
602
+ /** 비교 카드 — A vs B 표(첫 열=기준, 나머지=옵션 값). */
603
+ function comparison(p: ComparisonParams): UiCardContent {
604
+ const blocks: Block[] = [
605
+ { type: "table", columns: ["", ...p.options], rows: p.rows.map((r) => [r.label, ...r.values]) },
606
+ ];
607
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Comparison", context: p.context, blocks });
608
+ }
609
+
610
+ export interface ChecklistParams {
611
+ card_id: string;
612
+ title?: string;
613
+ items: { label: string; done?: boolean; note?: string }[];
614
+ /** 담당자(아바타로 표시). */
615
+ owner?: string;
616
+ context?: UiCardContent["context"];
617
+ }
618
+
619
+ /** 체크리스트 카드 — (선택) 담당 아바타 + 진행률 progress + 항목 steps(done/todo). */
620
+ function checklist(p: ChecklistParams): UiCardContent {
621
+ const total = p.items.length;
622
+ const done = p.items.filter((i) => i.done).length;
623
+ const pct = total ? Math.round((done / total) * 100) : 0;
624
+ const blocks: Block[] = [];
625
+ if (p.owner) blocks.push({ type: "avatar", name: p.owner, subtitle: `Owner · ${done}/${total} done` });
626
+ blocks.push({ type: "progress", value: pct, label: `${done}/${total} complete` });
627
+ blocks.push({
628
+ type: "steps",
629
+ items: p.items.map((i) => ({ label: i.label, status: i.done ? ("done" as const) : ("todo" as const), note: i.note })),
630
+ });
631
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Checklist", context: p.context, blocks });
632
+ }
633
+
634
+ export interface MeetingParams {
635
+ card_id: string;
636
+ title?: string;
637
+ agenda?: string;
638
+ when?: string;
639
+ duration?: string;
640
+ attendees?: string[];
641
+ /** 시간 후보(라디오). 미지정 시 즉시 확정 버튼만. */
642
+ options?: { label: string; value: string }[];
643
+ confirmText?: string;
644
+ context?: UiCardContent["context"];
645
+ }
646
+
647
+ /** 회의 카드 — 안건/일시 fields + 참석자 avatarGroup + (선택) 시간 후보 radio + 확정 액션. */
648
+ function meeting(p: MeetingParams): UiCardContent {
649
+ const blocks: Block[] = [];
650
+ const items: { label: string; value: string; short?: boolean }[] = [];
651
+ if (p.agenda) items.push({ label: "Agenda", value: p.agenda });
652
+ if (p.when) items.push({ label: "When", value: p.when, short: true });
653
+ if (p.duration) items.push({ label: "Duration", value: p.duration, short: true });
654
+ if (items.length) blocks.push({ type: "fields", items });
655
+ if (p.attendees?.length) blocks.push({ type: "avatarGroup", label: "Attendees", people: p.attendees.map((a) => ({ name: a })) });
656
+ if (p.options?.length) {
657
+ blocks.push({ type: "radio", name: "slot", label: "Pick a slot", options: p.options, value: p.options[0]?.value });
658
+ }
659
+ blocks.push({
660
+ type: "actions",
661
+ elements: [{ type: "button", action_id: "confirm_schedule", text: p.confirmText ?? "Confirm time", style: "primary" }],
662
+ });
663
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Meeting", context: p.context, blocks });
664
+ }
665
+
666
+ export interface ErrorRecoveryParams {
667
+ card_id: string;
668
+ title?: string;
669
+ message: string;
670
+ /** 기술 상세(코드/로그). */
671
+ detail?: string;
672
+ retryText?: string;
673
+ escalateText?: string;
674
+ context?: UiCardContent["context"];
675
+ }
676
+
677
+ /** 오류 복구 카드 — danger 콜아웃 + (선택) 상세 code + Retry/Escalate 액션. */
678
+ function errorRecovery(p: ErrorRecoveryParams): UiCardContent {
679
+ const blocks: Block[] = [{ type: "callout", tone: "danger", title: p.title ?? "Something went wrong", text: p.message }];
680
+ if (p.detail) blocks.push({ type: "code", code: p.detail });
681
+ blocks.push({
682
+ type: "actions",
683
+ elements: [
684
+ { type: "button", action_id: "retry", text: p.retryText ?? "Retry", style: "primary" },
685
+ { type: "button", action_id: "escalate", text: p.escalateText ?? "Escalate" },
686
+ ],
687
+ });
688
+ return buildUiCardContent({ card_id: p.card_id, title: p.title, fallback: p.title ?? "Error", context: p.context, blocks });
689
+ }
690
+
691
+ /** 내장 템플릿 모음 (타입드 SDK 진입점). */
692
+ export const templates = {
693
+ approval,
694
+ multiApproval,
695
+ taskList,
696
+ wizard,
697
+ list,
698
+ progress,
699
+ info,
700
+ doc,
701
+ plan,
702
+ form,
703
+ scorecard,
704
+ contactCard,
705
+ digest,
706
+ incident,
707
+ kpiGrid,
708
+ invoice,
709
+ estimate,
710
+ comparison,
711
+ checklist,
712
+ meeting,
713
+ errorRecovery,
714
+ };
715
+
716
+ registerTemplate<ApprovalParams>("approval", approval);
717
+ registerTemplate<MultiApprovalParams>("multiApproval", multiApproval);
718
+ registerTemplate<TaskListParams>("taskList", taskList);
719
+ registerTemplate<WizardParams>("wizard", wizard);
720
+ registerTemplate<ListParams>("list", list);
721
+ registerTemplate<ProgressParams>("progress", progress);
722
+ registerTemplate<InfoParams>("info", info);
723
+ registerTemplate<DocParams>("doc", doc);
724
+ registerTemplate<PlanParams>("plan", plan);
725
+ registerTemplate<FormParams>("form", form);
726
+ registerTemplate<ScorecardParams>("scorecard", scorecard);
727
+ registerTemplate<ContactCardParams>("contactCard", contactCard);
728
+ registerTemplate<DigestParams>("digest", digest);
729
+ registerTemplate<IncidentParams>("incident", incident);
730
+ registerTemplate<KpiGridParams>("kpiGrid", kpiGrid);
731
+ registerTemplate<InvoiceParams>("invoice", invoice);
732
+ registerTemplate<EstimateParams>("estimate", estimate);
733
+ registerTemplate<ComparisonParams>("comparison", comparison);
734
+ registerTemplate<ChecklistParams>("checklist", checklist);
735
+ registerTemplate<MeetingParams>("meeting", meeting);
736
+ registerTemplate<ErrorRecoveryParams>("errorRecovery", errorRecovery);