@underdogai/mesh-event-schemas 0.1.0 → 0.3.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.
package/src/surfaces.ts CHANGED
@@ -9,7 +9,15 @@
9
9
  * 데이터 바인딩: 문자열 안의 `{{entity.path}}` 를 resolveBindings 로 룸 엔티티 스냅샷에서 채운다.
10
10
  */
11
11
  import { z } from "zod";
12
- import { ToneSchema } from "./blocks.js";
12
+ import {
13
+ CheckboxBlockSchema,
14
+ InputBlockSchema,
15
+ RadioBlockSchema,
16
+ SelectBlockSchema,
17
+ SliderBlockSchema,
18
+ ToggleBlockSchema,
19
+ ToneSchema,
20
+ } from "./blocks.js";
13
21
  import {
14
22
  AgentPlanContentSchema,
15
23
  AgentStepStatusSchema,
@@ -408,7 +416,29 @@ export const ModalConfirmSchema = z.object({
408
416
  });
409
417
  export type ModalConfirm = z.infer<typeof ModalConfirmSchema>;
410
418
 
411
- /** 모달/시트 스펙 — 제목 + 설명 + 톤 + 확인/취소. title/description 은 `{{entity.path}}` 바인딩 가능. */
419
+ /**
420
+ * 모달 폼 블록 — 카드 블록 어휘의 **입력 서브셋**만 허용한 닫힌 유니온(D1). 버튼/액션 블록은 넣지
421
+ * 않는다 — 모달의 실행 어포던스는 confirm 하나다(파괴적 액션 확인 게이트라는 정체성 유지).
422
+ * 렌더러(ModalRenderer)가 name 별 값을 수집해 confirm 발행 시 values 로 동봉한다.
423
+ */
424
+ export const ModalFormBlockSchema = z.discriminatedUnion("type", [
425
+ InputBlockSchema,
426
+ SelectBlockSchema,
427
+ RadioBlockSchema,
428
+ CheckboxBlockSchema,
429
+ ToggleBlockSchema,
430
+ SliderBlockSchema,
431
+ ]);
432
+ export type ModalFormBlock = z.infer<typeof ModalFormBlockSchema>;
433
+
434
+ /** 모달 폼 스텝(G3) — 제목 + 폼 블록 묶음. 카드 마법사(UiCardContent.steps)의 모달 짝. */
435
+ export const ModalStepSchema = z.object({
436
+ title: z.string().optional(),
437
+ blocks: ModalFormBlockSchema.array(),
438
+ });
439
+ export type ModalStep = z.infer<typeof ModalStepSchema>;
440
+
441
+ /** 모달/시트 스펙 — 제목 + 설명 + 톤 + (선택) 폼 블록 + 확인/취소. title/description 은 `{{entity.path}}` 바인딩 가능. */
412
442
  export const ModalSpecSchema = z.object({
413
443
  /** 이 모달의 안정적 id — 같은 id 의 셸 액션이 이 모달을 연다. */
414
444
  id: z.string(),
@@ -417,6 +447,20 @@ export const ModalSpecSchema = z.object({
417
447
  title: z.string(),
418
448
  description: z.string().optional(),
419
449
  tone: ToneSchema.optional(),
450
+ /**
451
+ * 폼 블록(선택, D1) — 확인 전에 사람이 채우는 입력들. confirm 발행 시 수집값이 values 로 동봉된다.
452
+ * 이 필드를 모르는 옛 클라는 zod strip 으로 조용히 떨궈 확인문구만 렌더한다(graceful degrade).
453
+ * 미래 블록 종류는 dropUnknownSurfaceSections 가 블록 단위로 걸러낸다(서피스 붕괴 방지).
454
+ */
455
+ blocks: ModalFormBlockSchema.array().optional(),
456
+ /**
457
+ * 스텝 폼(G3) — 있으면 렌더러가 마법사(이전/다음)로 그리고, 마지막 스텝에서만 confirm 을 활성한다.
458
+ * 이때 blocks 는 무시된다(전방호환 트릭: 발행 측 buildAppSurface 가 blocks 에 **평탄화 사본**을 자동
459
+ * 동봉하므로, steps 를 모르는 옛 클라는 strip 후 평면 폼(blocks)으로 렌더 — 기능 축소일 뿐 파손 없음).
460
+ */
461
+ steps: ModalStepSchema.array().optional(),
462
+ /** 자동 확인(리뷰) 스텝(G3) — steps 가 있을 때 마지막에 수집값 요약(라벨→값) 스텝을 붙인다. 기본 true. */
463
+ review: z.boolean().optional(),
420
464
  /** 확인 버튼(있으면) — 누르면 confirm.action_id 를 발행하고 닫는다. */
421
465
  confirm: ModalConfirmSchema.optional(),
422
466
  /** 취소/닫기 라벨(기본 "Cancel"). */
@@ -456,6 +500,112 @@ export const ObjectMapSpecSchema = z.object({
456
500
  });
457
501
  export type ObjectMapSpec = z.infer<typeof ObjectMapSpecSchema>;
458
502
 
503
+ // --- 우측 셸 패널 기여 (ShellPanelTabContribution) — PANEL-SDK-1 ---
504
+ // SDK 앱이 우측 패널에 Files 와 동급의 **독립 탭**을 선언형으로 기여한다. 기존 PanelTabSchema 는
505
+ // PanelSpec *내부의 서브탭*이라 이름을 겹치지 않는다(티켓 §3.1). 배치(panel_tabs)와 콘텐츠(kind)를
506
+ // 분리한다: 파일 관리는 콘텐츠 종류 중 하나(assetBrowser)일 뿐, 탭 배치 API 가 아니다(잠금 결정 1).
507
+ // 호스트가 실제 탭 id 를 `app:<app>:<tab.id>` 로 네임스페이스해 코어 id(panel/files/preview:*)와
508
+ // 충돌할 수 없게 한다(client-web core/panelContributions.ts).
509
+
510
+ /** 앱당 panel_tabs 상한 — 스키마(max)와 인바운드 degrade(초과분 자르기)가 같은 값을 본다. */
511
+ export const MAX_PANEL_TAB_CONTRIBUTIONS = 8;
512
+ /** 앱당 preview_providers 상한 — 위와 같은 쌍. */
513
+ export const MAX_PREVIEW_PROVIDERS = 32;
514
+
515
+ /** 필수 파일 슬롯 — "이 종류의 파일이 최소 몇 개 있어야 한다"(레퍼런스/기획설계 등). */
516
+ export const AssetBrowserSlotSchema = z.object({
517
+ id: z.string().min(1),
518
+ label: z.string().min(1),
519
+ /** 허용 형식 — ".pdf"(확장자) 또는 "image/*"(MIME) 문자열. 업로드 input accept 와 같은 어휘. */
520
+ accept: z.string().array().optional(),
521
+ /** 충족으로 판정할 최소 개수(기본 1). */
522
+ min: z.number().int().min(0).optional(),
523
+ });
524
+ export type AssetBrowserSlot = z.infer<typeof AssetBrowserSlotSchema>;
525
+
526
+ /**
527
+ * 파일 브라우저 스펙 — 폴더/현재 버전/버전 이력을 **앱 네임스페이스 이벤트**로 연결한다(티켓 §6).
528
+ * mxc(바이트 주소)와 앱 도메인 메타데이터(asset/folder/version)를 분리한다(잠금 결정 7).
529
+ * 렌더러(AssetBrowserPanel)는 Phase D — 스키마/계약을 먼저 고정한다.
530
+ */
531
+ export const AssetBrowserSpecSchema = z.object({
532
+ title: z.string().optional(),
533
+ /** 데이터 이벤트 타입 — folder/current 는 state, version 은 timeline(append-only). */
534
+ events: z.object({
535
+ folder: z.string().min(1),
536
+ current: z.string().min(1),
537
+ version: z.string().min(1),
538
+ }),
539
+ /** 조작 액션 id(org.corp.ui.action 왕복) — 없으면 읽기 전용 브라우저. */
540
+ actions: z
541
+ .object({
542
+ upload: z.string().optional(),
543
+ createFolder: z.string().optional(),
544
+ rename: z.string().optional(),
545
+ })
546
+ .optional(),
547
+ requiredSlots: AssetBrowserSlotSchema.array().optional(),
548
+ });
549
+ export type AssetBrowserSpec = z.infer<typeof AssetBrowserSpecSchema>;
550
+
551
+ /**
552
+ * 기여 탭의 콘텐츠 — 닫힌 판별 유니온(잠금 결정 3: React/임의 CSS 주입 없음).
553
+ * - surface: 기존 닫힌 SurfaceBody 조합(PanelSpec)을 그대로 렌더(SurfaceRenderer 재사용).
554
+ * - assetBrowser: 파일 관리 명세(위). 미래 kind 는 dropUnknownSurfaceSections 가 탭 단위로 거른다.
555
+ */
556
+ export const ShellPanelContentSchema = z.discriminatedUnion("kind", [
557
+ z.object({ kind: z.literal("surface"), panel: PanelSpecSchema }),
558
+ AssetBrowserSpecSchema.extend({ kind: z.literal("assetBrowser") }),
559
+ ]);
560
+ export type ShellPanelContent = z.infer<typeof ShellPanelContentSchema>;
561
+
562
+ /** 우측 셸 패널 기여 탭 하나 — id 는 앱 내부 고유(호스트가 `app:<app>:` 접두사로 네임스페이스). */
563
+ export const ShellPanelTabContributionSchema = z.object({
564
+ id: z.string().min(1),
565
+ label: z.string().min(1),
566
+ /** 호스트 아이콘 집합(components/icons) 이름 — 미지 값은 중립 폴백(빈 원). */
567
+ icon: z.string().optional(),
568
+ /**
569
+ * lazy(기본) — 활성일 때만 마운트. keep-alive — **한 번 열린 뒤**에는 전환해도 살려 둔다
570
+ * (WorkspaceShell 의 기존 규칙 보존: 열지도 않은 탭을 미리 마운트하지 않는다).
571
+ */
572
+ lifecycle: z.enum(["lazy", "keep-alive"]).optional(),
573
+ content: ShellPanelContentSchema,
574
+ });
575
+ export type ShellPanelTabContribution = z.infer<typeof ShellPanelTabContributionSchema>;
576
+
577
+ // --- 동적 미리보기 제공자 (PreviewProviderSpec) — PANEL-SDK-1 ---
578
+ // 앱이 코어가 모르는 확장자/MIME 을 **호스트 내장 안전 렌더러**에 매핑한다(잠금 결정 4: 매칭과
579
+ // 렌더링 분리 — 임의 실행 코드는 0.4 의 별도 샌드박스 경로만). 매칭 규칙은 순수 함수
580
+ // resolvePreviewProvider(client-web core/previewRegistry.ts)가 고정한다: 코어 아는 형식 우선,
581
+ // 앱 간은 room.apps 순서, provider 안에서는 extension/MIME OR.
582
+
583
+ /** 호스트 내장 안전 renderer id — Phase C 에서 renderer registry 의 키가 된다. */
584
+ export const NativePreviewRendererIdSchema = z.enum([
585
+ "image",
586
+ "video",
587
+ "audio",
588
+ "pdf",
589
+ "text",
590
+ "markdown",
591
+ "html-sandbox",
592
+ ]);
593
+ export type NativePreviewRendererId = z.infer<typeof NativePreviewRendererIdSchema>;
594
+
595
+ /** 미리보기 제공자 하나 — 확장자/MIME 매칭 → 내장 renderer. */
596
+ export const PreviewProviderSpecSchema = z.object({
597
+ id: z.string().min(1),
598
+ match: z.object({
599
+ /** 점 포함 확장자(".dxf") 권장 — 클라가 소문자·점 포함으로 정규화해 매칭한다. */
600
+ extensions: z.string().array().optional(),
601
+ /** exact("text/plain") 또는 wildcard("image/*"). */
602
+ mimeTypes: z.string().array().optional(),
603
+ }),
604
+ /** 렌더러 — 0.3 은 native 만(닫힌 enum). widget/converter 는 0.4 별도 보안 경로(티켓 §5.4). */
605
+ renderer: z.object({ kind: z.literal("native"), id: NativePreviewRendererIdSchema }),
606
+ });
607
+ export type PreviewProviderSpec = z.infer<typeof PreviewProviderSpecSchema>;
608
+
459
609
  // --- 앱 서피스 와이어 이벤트 (org.corp.app.surface) — "서피스를 데이터로" ---
460
610
  // 클라에 팩을 컴파일해 넣는 대신(registerAppPack=클라 평면), 앱(App Service)이 셸 서피스(패널/헤더)를
461
611
  // 상태 이벤트로 룸에 실어 보낸다 → "Matrix App Service 만 추가해도" 회사별 UI 가 등록·렌더된다.
@@ -473,8 +623,13 @@ export const AppSurfaceContentSchema = z.object({
473
623
  display_name: z.string().optional(),
474
624
  /** 이 서피스가 다루는 엔티티 타입(표시/진단용). */
475
625
  entity_type: z.string().optional(),
476
- /** 우측 컨텍스트 패널(닫힌 SurfaceBody 조합). {{entity.path}} 바인딩은 클라가 resolveBindings 로 해석. */
477
- panel: PanelSpecSchema,
626
+ /**
627
+ * 우측 컨텍스트 패널(닫힌 SurfaceBody 조합) — PANEL-SDK-1 부터 **선택**. panel_tabs 만 기여하는
628
+ * 앱은 panel 없이도 유효한 서피스다. 이 필드를 모르는 구 클라에서는 "신규 탭만 가진 앱이 안 보임"
629
+ * 으로 축소될 뿐 기존 서피스는 그대로 동작한다(하위호환 UI 가 필요한 앱은 0.3 동안 sidebar 병행).
630
+ * {{entity.path}} 바인딩은 클라가 resolveBindings 로 해석.
631
+ */
632
+ panel: PanelSpecSchema.optional(),
478
633
  /**
479
634
  * 메인 영역 서피스(닫힌 SurfaceBody 조합) — 선택. 우측 panel 의 짝으로, 룸 본문(타임라인 대신)에
480
635
  * 탭으로 그릴 중앙 콘텐츠를 데이터로 기술한다. 없으면 이 앱은 사이드바 탭만 제공한다.
@@ -498,6 +653,13 @@ export const AppSurfaceContentSchema = z.object({
498
653
  objectMap: ObjectMapSpecSchema.optional(),
499
654
  /** 선언형 모달/시트(확인 다이얼로그) — 선택(AppPack.modals 동일). 같은 id 의 셸 액션이 연다. */
500
655
  modals: ModalSpecSchema.array().optional(),
656
+ /**
657
+ * 우측 셸 패널 기여 탭(PANEL-SDK-1) — Files 옆에 서는 독립 탭 목록. 순서 = 선언 순서.
658
+ * 이 필드를 모르는 옛 클라는 zod strip 으로 조용히 무시한다(기존 M3 필드와 같은 자세).
659
+ */
660
+ panel_tabs: ShellPanelTabContributionSchema.array().max(MAX_PANEL_TAB_CONTRIBUTIONS).optional(),
661
+ /** 동적 미리보기 제공자(PANEL-SDK-1) — 미지 확장자/MIME → 내장 renderer 매핑. 순서 = 선언 순서. */
662
+ preview_providers: PreviewProviderSpecSchema.array().max(MAX_PREVIEW_PROVIDERS).optional(),
501
663
  /**
502
664
  * 회사별 브랜드 토큰 오버라이드(--cob-color-* → 값). 클라가 룩(디자인 토큰)을 소유하되,
503
665
  * 테넌트는 토큰만 갈아끼워 자기 브랜드를 입힌다(임의 CSS 아님 = 닫힌 토큰 키). 클라가 inline CSS 변수로 적용.
@@ -510,7 +672,13 @@ export type AppSurfaceContent = z.infer<typeof AppSurfaceContentSchema>;
510
672
  export function buildAppSurface(
511
673
  input: Omit<AppSurfaceContent, "v"> & { v?: typeof SCHEMA_VERSION },
512
674
  ): AppSurfaceContent {
513
- return AppSurfaceContentSchema.parse({ ...input, v: SCHEMA_VERSION });
675
+ // 스텝 모달(G3) 전방호환 — steps 선언 시 blocks 에 **평탄화 사본**을 자동 동봉한다(작성자가 blocks 를
676
+ // 직접 줬으면 존중). 신 클라는 steps 를 그리며 blocks 를 무시하고, steps 를 모르는 옛 클라는 zod strip
677
+ // 후 평면 폼(blocks)으로 렌더한다 — 기능 축소일 뿐 파손 없음.
678
+ const modals = input.modals?.map((m) =>
679
+ m.steps?.length && !m.blocks?.length ? { ...m, blocks: m.steps.flatMap((s) => s.blocks) } : m,
680
+ );
681
+ return AppSurfaceContentSchema.parse({ ...input, ...(modals ? { modals } : {}), v: SCHEMA_VERSION });
514
682
  }
515
683
 
516
684
  /**
@@ -529,10 +697,60 @@ export function dropUnknownSurfaceSections(raw: unknown): unknown {
529
697
  if (!Array.isArray(p.sections)) return panel;
530
698
  return { ...p, sections: p.sections.filter((s) => SurfaceSectionSchema.safeParse(s).success) };
531
699
  };
700
+ // 모달 폼 블록(D1)도 같은 자세 — blocks 는 닫힌 유니온이라 미래 블록 하나가 modals 배열 전체
701
+ // (나아가 서피스 전체)를 붕괴시킨다 → 블록 단위로 걸러 아는 블록 + 확인문구는 그린다.
702
+ // 스텝 폼(G3)의 steps[].blocks 도 동일 — 스텝 안 미래 블록 하나가 모달을 붕괴시키지 않게 거른다.
703
+ const keepModalBlocks = (modals: unknown): unknown => {
704
+ if (!Array.isArray(modals)) return modals;
705
+ const keepBlocks = (blocks: unknown): unknown =>
706
+ Array.isArray(blocks) ? blocks.filter((b) => ModalFormBlockSchema.safeParse(b).success) : blocks;
707
+ return modals.map((m) => {
708
+ if (!m || typeof m !== "object") return m;
709
+ const mm = m as Record<string, unknown>;
710
+ const out: Record<string, unknown> = { ...mm };
711
+ if (Array.isArray(mm.blocks)) out.blocks = keepBlocks(mm.blocks);
712
+ if (Array.isArray(mm.steps)) {
713
+ out.steps = mm.steps.map((s) => {
714
+ if (!s || typeof s !== "object") return s;
715
+ const ss = s as Record<string, unknown>;
716
+ return Array.isArray(ss.blocks) ? { ...ss, blocks: keepBlocks(ss.blocks) } : s;
717
+ });
718
+ }
719
+ return out;
720
+ });
721
+ };
722
+ // SDK 기여 탭(PANEL-SDK-1)도 같은 자세 — ① kind:"surface" 탭의 내부 panel 은 섹션 단위로 거르고,
723
+ // ② 그래도 미지(미래 kind)거나 깨진 contribution 은 **그 탭만** 드롭한다(탭 하나 때문에 배열
724
+ // 전체 → 서피스 전체가 붕괴하지 않게). ③ 미래 스키마가 상한을 올려 보내도 옛 클라의 max 검증이
725
+ // 전체를 죽이지 않도록 상한으로 자른다(발신 빌더는 strict 라 정상 경로에선 안 걸린다).
726
+ const keepPanelTabs = (tabs: unknown): unknown => {
727
+ if (!Array.isArray(tabs)) return tabs;
728
+ return tabs
729
+ .map((t) => {
730
+ if (!t || typeof t !== "object") return t;
731
+ const tt = t as Record<string, unknown>;
732
+ const content = tt.content;
733
+ if (content && typeof content === "object" && (content as Record<string, unknown>).kind === "surface") {
734
+ const cc = content as Record<string, unknown>;
735
+ return { ...tt, content: { ...cc, panel: keepSections(cc.panel) } };
736
+ }
737
+ return t;
738
+ })
739
+ .filter((t) => ShellPanelTabContributionSchema.safeParse(t).success)
740
+ .slice(0, MAX_PANEL_TAB_CONTRIBUTIONS);
741
+ };
742
+ // 미리보기 제공자 — 미래 renderer kind("widget", 0.4)가 섞여 와도 provider 단위로만 거른다.
743
+ const keepPreviewProviders = (providers: unknown): unknown =>
744
+ Array.isArray(providers)
745
+ ? providers.filter((p) => PreviewProviderSpecSchema.safeParse(p).success).slice(0, MAX_PREVIEW_PROVIDERS)
746
+ : providers;
532
747
  return {
533
748
  ...c,
534
749
  ...(c.panel !== undefined ? { panel: keepSections(c.panel) } : {}),
535
750
  ...(c.main !== undefined ? { main: keepSections(c.main) } : {}),
751
+ ...(c.modals !== undefined ? { modals: keepModalBlocks(c.modals) } : {}),
752
+ ...(c.panel_tabs !== undefined ? { panel_tabs: keepPanelTabs(c.panel_tabs) } : {}),
753
+ ...(c.preview_providers !== undefined ? { preview_providers: keepPreviewProviders(c.preview_providers) } : {}),
536
754
  };
537
755
  }
538
756
 
package/src/templates.ts CHANGED
@@ -373,7 +373,7 @@ function form(p: FormParams): UiCardContent {
373
373
  }
374
374
 
375
375
  // 도메인색이 강한 SCM(Forgejo) 템플릿(scmRef/prReview/ciRun)은 코어가 아니라 plugin-forgejo 가
376
- // 소유한다 — register() 에서 registerTemplate 로 등록(packages/plugin-forgejo/src/templates.ts).
376
+ // 소유한다 — register() 에서 registerTemplate 로 등록(packages/domain/plugin-forgejo/src/templates.ts).
377
377
  // 코어 templates.* 는 도메인 무관 프리미티브만 유지한다("도메인=플러그인" 가드레일).
378
378
 
379
379
  export interface ScorecardParams {