@morit/cli 1.0.0 → 1.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 (33) hide show
  1. package/README.md +53 -18
  2. package/assets/docs/README.md +105 -0
  3. package/assets/docs/ai-response-and-timeline.md +125 -0
  4. package/assets/docs/ai-skill-and-docx-workflow.md +83 -0
  5. package/assets/docs/app-builder.md +56 -0
  6. package/assets/docs/authentication.md +140 -0
  7. package/assets/docs/components.md +159 -0
  8. package/assets/docs/design-tokens-responsive.md +148 -0
  9. package/assets/docs/docs-index.json +93 -0
  10. package/assets/docs/examples-notion.md +83 -0
  11. package/assets/docs/examples-school-life.md +74 -0
  12. package/assets/docs/getting-started.md +132 -0
  13. package/assets/docs/information-hierarchy.md +81 -0
  14. package/assets/docs/instances-and-connectors.md +93 -0
  15. package/assets/docs/lifecycle-and-api.md +158 -0
  16. package/assets/docs/local-cli.md +124 -0
  17. package/assets/docs/manifest.md +208 -0
  18. package/assets/docs/packaging-and-testing.md +115 -0
  19. package/assets/docs/permissions-and-data.md +131 -0
  20. package/assets/docs/platform-compatibility.md +62 -0
  21. package/assets/docs/project-structure.md +90 -0
  22. package/assets/docs/remote-mcp.md +152 -0
  23. package/assets/docs/school-life-privacy.md +49 -0
  24. package/assets/docs/screens-layout-navigation.md +95 -0
  25. package/assets/docs/sdk-and-mcp.md +182 -0
  26. package/assets/docs/tool-and-skill.md +163 -0
  27. package/assets/docs/troubleshooting.md +117 -0
  28. package/assets/docs/ui-extensions.md +70 -0
  29. package/assets/docs/ui-runtime-v2.md +273 -0
  30. package/assets/docs/verification.md +128 -0
  31. package/assets/plugin_contract.json +222 -1
  32. package/package.json +1 -1
  33. package/src/workspace.js +297 -86
@@ -0,0 +1,117 @@
1
+ # 오류 해결
2
+
3
+ 오류를 catch로 숨기기 전에 어느 경계에서 발생했는지 구분합니다.
4
+
5
+ ## 빠른 진단 순서
6
+
7
+ ```text
8
+ source를 읽을 수 있는가
9
+ → validate가 성공하는가
10
+ → preview artifact가 생기는가
11
+ → signed build와 reopen이 성공하는가
12
+ → API 인증과 목록 조회가 성공하는가
13
+ → 설치 상태가 active인가
14
+ → Connection과 권한이 ready인가
15
+ → capability가 실행되는가
16
+ → UI가 결과를 렌더링하는가
17
+ ```
18
+
19
+ 앞 단계가 실패한 상태에서 다음 단계 오류를 우회하지 않습니다.
20
+
21
+ ## Validate 오류
22
+
23
+ | 증상 | 확인 항목 |
24
+ |---|---|
25
+ | unknown field | 문서에 없는 key 제거, v1/v2 필드 혼합 여부 |
26
+ | duplicate ID | manifest 배열과 모든 fragment를 함께 검색 |
27
+ | unknown permission | 지원 목록과 Manifest 전역 권한 확인 |
28
+ | capability reference | UI, Slash Command, child dependency 대상 ID 확인 |
29
+ | state/data binding | `initial_state`, `data_sources`, `item` scope 확인 |
30
+ | source entrypoint | 각 `src/*.py`와 `sandbox_python` 1:1 확인 |
31
+ | image asset | `assets/` 경로·확장자·실제 파일 확인 |
32
+
33
+ 진단에 표시된 첫 오류부터 고치고 다시 validate합니다. 허용 필드를 늘리거나 unknown key를 무시하게
34
+ 바꾸지 않습니다.
35
+
36
+ ## Preview 오류
37
+
38
+ preview 전에 validate가 실행됩니다. artifact가 없으면 출력 디렉터리 권한과 source size를 확인합니다.
39
+ preview가 열려도 실제 앱의 typography, navigation, scroll, async state는 별도 검증 대상입니다.
40
+
41
+ ## Build·signature 오류
42
+
43
+ - output 확장자가 `.mplg`인지 확인합니다.
44
+ - package가 2 MiB, 64 entry 한도 안인지 확인합니다.
45
+ - private key 파일 권한과 publisher identity를 확인합니다.
46
+ - source ZIP을 `.mplg`로 이름만 바꾸지 않았는지 확인합니다.
47
+ - `verify`가 반환한 plugin ID, publisher, version, SHA-256을 기록합니다.
48
+ - trusted publisher에 등록된 key와 embedded public key가 다른 경우 올바른 조직 key로 다시 build합니다.
49
+
50
+ ## 목록 조회와 설치 오류
51
+
52
+ `Not Found`는 URL 이동으로 해결하지 않습니다. API base URL, 배포 route, 인증 header, 사용자 RLS를
53
+ 확인합니다. `401`은 endpoint가 없다는 뜻이 아니라 인증이 필요하다는 뜻입니다.
54
+
55
+ 설치 뒤 목록 조회가 실패했다가 재시도에서 duplicate가 나오면 package file, Installation row,
56
+ default Instance, cache가 하나의 atomic transaction으로 정리되는지 확인합니다. 앱 데이터 삭제만으로
57
+ 서버 DB와 외부 저장소는 삭제되지 않습니다. 사용자별 API 목록과 서버 package store를 함께 확인합니다.
58
+
59
+ 정상 duplicate는 같은 plugin ID·publisher·version 정책으로 판단하고 파일명만 사용하지 않습니다.
60
+
61
+ ## Connection 오류
62
+
63
+ | 상태 | 의미 | 사용자 동작 |
64
+ |---|---|---|
65
+ | `not_configured` | 필수 Connection/Secret 없음 | 계정 연결 또는 Developer Secret 설정 |
66
+ | `unavailable` | provider·암호화·runtime 사용 불가 | 운영 설정 확인 |
67
+ | credential rejected | token 만료·철회 | 한 번 refresh 후 다시 연결 |
68
+ | rate limited | Connector 한도 초과 | 제한된 시간 뒤 재시도 |
69
+
70
+ token을 로그에 출력해 진단하지 않습니다. 연결 metadata와 공개 오류 코드만 사용합니다.
71
+
72
+ ## Capability 오류
73
+
74
+ - permission required: Manifest와 capability 권한, 사용자 grant 확인
75
+ - disabled/incompatible: Instance state와 Morit version 확인
76
+ - timeout: capability timeout과 Connector timeout을 비교하고 작업을 작게 나눔
77
+ - retryable network: Connector의 bounded retry만 사용
78
+ - invalid response: `summary`, `data`, `evidence` 형태와 외부 response mapping 확인
79
+ - code worker unavailable: broker/worker health와 lease 확인; Archive process에서 직접 실행하지 않음
80
+
81
+ Tool 하나의 실패가 AI 요청 전체 실패를 뜻하지 않는 경우 대체 Tool·부분 결과·재연결을 시도하고
82
+ 최종 답변에 남은 제한을 설명합니다.
83
+
84
+ ## UI 오류
85
+
86
+ - 해당 extension만 fallback되고 대화·앱 전체가 계속 동작하는지 확인합니다.
87
+ - 없는 state/data/route를 참조하지 않는지 확인합니다.
88
+ - list/timeline child가 정확히 하나인지 확인합니다.
89
+ - `surface`가 children을 가지는지 확인합니다.
90
+ - `positioned`는 `stack`, `expanded`는 `row`/`column` 안인지 확인합니다.
91
+ - literal image URL 대신 capability result binding을 사용했는지 확인합니다.
92
+ - 원격 이미지가 안 보이면 Manifest와 활성 Instance의 `network` 권한, public HTTPS 여부를 먼저
93
+ 확인합니다. redirect, MIME/magic 불일치, 손상된 파일, 512 KiB 또는 가로·세로 4096 px 초과는
94
+ Host가 의도적으로 차단합니다. 애니메이션은 최대 128 frame이고 `width × height × frame 수`로
95
+ 계산한 frame 합산 pixel이 16,000,000 이하여야 합니다.
96
+ - 원격 이미지 실패를 앱 직접 fetch나 임의 외부 URL widget으로 우회하지 않습니다.
97
+ - Response UI가 원래 AI 메시지 안의 한 container에 붙는지 확인합니다.
98
+
99
+ ## MCP 오류
100
+
101
+ Local MCP:
102
+
103
+ ```bash
104
+ node --version
105
+ npx -y @morit/plugin-mcp
106
+ codex mcp list
107
+ ```
108
+
109
+ Remote MCP:
110
+
111
+ ```bash
112
+ curl -i https://morit-api.moring.co/mcp
113
+ ```
114
+
115
+ 인증 전 `401`과 `WWW-Authenticate`는 정상 OAuth 시작 신호입니다. `404`는 ingress route, `5xx`는
116
+ MCP service health와 server log를 확인합니다. Codex에서는 `codex mcp login <name>`, Tool 목록에서는
117
+ `morit_sdk_contract`와 `morit_docs_search`를 먼저 확인합니다.
@@ -0,0 +1,70 @@
1
+ # UI extension point
2
+
3
+ UI extension은 Host의 노출 위치와 Runtime config를 연결합니다.
4
+
5
+ ```json
6
+ {
7
+ "id": "school_life.home",
8
+ "point": "screen",
9
+ "title": "학교 생활",
10
+ "order": 10,
11
+ "permissions": ["network", "storage"],
12
+ "config": {
13
+ "ui_schema": 2,
14
+ "view": {"type": "text", "props": {"text": "오늘 학교 생활"}}
15
+ }
16
+ }
17
+ ```
18
+
19
+ ## point 선택
20
+
21
+ | point | 노출 위치 | 사용 기준 |
22
+ |---|---|---|
23
+ | `screen` | 독립 플러그인 화면 | 목록·상세·편집 등 주 작업 |
24
+ | `surface` | 기존 일반 surface | 기존 패키지 호환; 새 화면은 `screen` 권장 |
25
+ | `menu` | 홈 플러그인 메뉴 | 독립 화면으로 가는 보조 진입점 |
26
+ | `action` | 홈 extension 영역 | 짧은 즉시 행동 또는 작은 embedded UI |
27
+ | `card` | 홈 extension 영역 | 상태·진행·다음 행동 요약 |
28
+ | `settings` | 플러그인 상세의 설정 영역 | 사용자 설정과 Connection 진입 |
29
+ | `workspace` | Morit AI 작업 공간 선택기 | 대화와 함께 쓰는 독립 작업 surface |
30
+ | `response` | capability를 실행한 AI 메시지 내부 | 읽기 중심 결과와 후속 행동 |
31
+
32
+ 일반 Tool/Skill은 자동으로 `workspace`가 되지 않습니다. AI 작업 공간 UI가 필요할 때만 별도
33
+ `workspace` extension을 선언합니다. Slash Command와 Tool discovery도 별도 계약입니다.
34
+
35
+ ## Home placement
36
+
37
+ Runtime v2의 `card`와 `action`은 고유 section을 만들 수 있습니다.
38
+
39
+ ```json
40
+ {
41
+ "placement": {
42
+ "section_id": "school_life.home",
43
+ "section_title": "학교 생활",
44
+ "section_order": 15,
45
+ "layout": "grid",
46
+ "show_header": true
47
+ }
48
+ }
49
+ ```
50
+
51
+ `layout`은 `stack`, `horizontal`, `grid`입니다. 같은 플러그인에서 같은 `section_id`를 사용한
52
+ extension은 함께 배치됩니다. 다른 플러그인의 ID와 겹치지 않게 package namespace를 붙입니다.
53
+ `placement`는 `card`와 `action`에서만 사용할 수 있습니다.
54
+
55
+ ## 권한과 action
56
+
57
+ UI `permissions`는 Manifest 전역 권한의 부분집합입니다. 화면이 참조하는 capability도 자신의 권한을
58
+ 따로 가집니다. UI가 표시됐다는 사실만으로 capability 권한이 허용되는 것은 아닙니다. `invoke`는
59
+ enabled, generation, granted permission, timeout을 Host 실행 경계에서 다시 확인합니다.
60
+
61
+ ## Runtime v1 호환
62
+
63
+ `ui_schema`가 없는 기존 extension은 `sections`, `actions`, `form`, shorthand `capability`/`label`을
64
+ 사용하는 Runtime v1으로 읽힙니다. v1은 기존 설치 호환용입니다. 새 서비스형 화면에는 state,
65
+ data source, navigation, theme, Response UI를 함께 사용할 수 있는 Runtime v2를 사용합니다.
66
+
67
+ Runtime v1 action style은 `primary` 또는 `secondary`이고, form field는 `text`, `integer`,
68
+ `select`만 지원합니다. v1 JSON에 v2 필드를 섞으면 오류입니다.
69
+
70
+ 다음은 [UI Runtime v2 레퍼런스](ui-runtime-v2.md)입니다.
@@ -0,0 +1,273 @@
1
+ # UI Runtime v2 레퍼런스
2
+
3
+ UI Runtime v2는 서명된 `.mplg`가 화면, 상태, data source, action을 JSON으로 선언하는 Host-rendered
4
+ 계약입니다. Flutter/Dart, JavaScript, HTML, CSS, WebView, native code를 UI로 실행하지 않습니다.
5
+
6
+ ## Config 구조
7
+
8
+ ```json
9
+ {
10
+ "ui_schema": 2,
11
+ "icon": "calendar",
12
+ "description": "오늘과 이번 주 학교 생활을 확인합니다.",
13
+ "theme": {
14
+ "density": "standard",
15
+ "radius": 18,
16
+ "spacing": 12
17
+ },
18
+ "app_bar": {
19
+ "title": "학교 생활",
20
+ "subtitle": "{{data.today.summary}}",
21
+ "pinned": true,
22
+ "actions": [
23
+ {
24
+ "id": "settings",
25
+ "label": "설정",
26
+ "icon": "settings",
27
+ "show_as": "overflow",
28
+ "action": {"type": "navigate", "target": "school_life.settings"}
29
+ }
30
+ ]
31
+ },
32
+ "navigation": {
33
+ "type": "adaptive",
34
+ "selected_state_key": "section",
35
+ "persist": true,
36
+ "label_behavior": "selected",
37
+ "items": [
38
+ {
39
+ "id": "today",
40
+ "label": "오늘",
41
+ "icon": "calendar",
42
+ "value": "today",
43
+ "action": {"type": "set_state", "values": {"section": "today"}}
44
+ },
45
+ {
46
+ "id": "week",
47
+ "label": "이번 주",
48
+ "icon": "clock",
49
+ "value": "week",
50
+ "action": {"type": "navigate", "target": "school_life.week"}
51
+ }
52
+ ]
53
+ },
54
+ "initial_state": {"section": "today"},
55
+ "data_sources": [
56
+ {
57
+ "id": "today",
58
+ "capability": "school_life.today",
59
+ "trigger": "load",
60
+ "query": null,
61
+ "arguments": {},
62
+ "refresh_seconds": 300
63
+ }
64
+ ],
65
+ "view": {
66
+ "type": "column",
67
+ "props": {"spacing": 12},
68
+ "children": [
69
+ {"type": "text", "props": {"text": "{{data.today.summary}}", "style": "heading"}}
70
+ ]
71
+ }
72
+ }
73
+ ```
74
+
75
+ 허용 config 필드는 `ui_schema`, `icon`, `description`, `placement`, `app_bar`, `navigation`,
76
+ `theme`, `initial_state`, `data_sources`, `view`입니다.
77
+
78
+ ## 트리와 크기 제한
79
+
80
+ - tree 최대 160 node, depth 12(root가 depth 0), node당 child 최대 32개
81
+ - data source 최대 8개
82
+ - state key 최대 32개
83
+ - config JSON 최대 32 KiB
84
+ - 자동 refresh 30~86,400초
85
+ - 한 extension의 app bar action 최대 6개
86
+ - navigation item 2~8개
87
+ - binding 깊이 최대 12 segment
88
+ - JSON binding 값 깊이 최대 8, 배열·객체 항목 최대 64개
89
+
90
+ `positioned`, `scroll`, `padding`, `center`, `expanded`, `badge`는 child를 정확히 하나만 가집니다.
91
+ `surface`는 child가 하나 이상이어야 합니다. `positioned`는 `stack`의 직접 child이고 `left`, `top`,
92
+ `right`, `bottom` 중 하나 이상을 지정해야 합니다. `expanded`는 `row` 또는 `column`의 직접
93
+ child입니다.
94
+
95
+ 크기 prop은 0~4096, position offset은 -4096~4096, spacing과 inset은 0~128입니다.
96
+ `border_width` 0~8, `border_radius` 0~64, `elevation` 0~24, `opacity` 0~1,
97
+ `aspect_ratio` 0.1~20, `flex` 1~24를 사용합니다. `padding`과 `margin`의 정확한 입력 형태와
98
+ theme 색상 역할은 [토큰과 반응형](design-tokens-responsive.md)에 있습니다.
99
+
100
+ ## State
101
+
102
+ `initial_state`가 허용 state key와 초기값을 선언합니다. key는 영문자로 시작하고 영문·숫자·밑줄을
103
+ 사용하는 최대 64자입니다.
104
+
105
+ ```json
106
+ {
107
+ "initial_state": {"period": "today", "notifications": true}
108
+ }
109
+ ```
110
+
111
+ `field`, `select`, `switch`, navigation selection, `set_state`는 여기에 선언된 key만 변경합니다.
112
+ `persist: true`는 Instance settings의 extension 전용 namespace에 저장합니다. credential과 민감한
113
+ 사용자 데이터는 state에 저장하지 않습니다.
114
+
115
+ ## Data source와 binding
116
+
117
+ ```json
118
+ {
119
+ "id": "schedule",
120
+ "capability": "school_life.schedule",
121
+ "trigger": "load",
122
+ "query": "{{state.period}}",
123
+ "arguments": {"period": "{{state.period}}"},
124
+ "refresh_seconds": 300
125
+ }
126
+ ```
127
+
128
+ `trigger`는 `load` 또는 `manual`입니다. 결과는 다음 namespace로 읽습니다.
129
+
130
+ ```text
131
+ data.<source_id>.completed
132
+ data.<source_id>.summary
133
+ data.<source_id>.data.<field>
134
+ ```
135
+
136
+ 반복 template 안에서는 `item.<field>`를 사용합니다. 문자열 전체가 binding 하나면 원래 JSON type을
137
+ arguments에 전달하고, 다른 글자와 섞으면 텍스트 template이 됩니다.
138
+
139
+ ```json
140
+ {"limit": "{{state.limit}}"}
141
+ ```
142
+
143
+ ```json
144
+ {"label": "{{state.period}} 일정"}
145
+ ```
146
+
147
+ ### Capability 결과 이미지
148
+
149
+ `image`와 `avatar`의 `url`은 literal URL이 아니라 capability 결과를 가리키는 전체
150
+ `{{data.<source>...}}` 또는 `{{item...}}` binding입니다. Manifest가 `network`를 요청하고 활성
151
+ Instance가 `network`를 grant한 경우에만 표시할 수 있습니다.
152
+
153
+ Host renderer는 해석된 URL을 앱에서 직접 fetch하지 않고 인증된
154
+ `POST /v1/plugin-instances/{instance_id}/ui-images:fetch`를 사용합니다. 요청 body는 다음과 같습니다.
155
+
156
+ ```json
157
+ {"url": "https://images.example.edu/today.png"}
158
+ ```
159
+
160
+ Host proxy는 공개 HTTPS와 SSRF 정책을 적용하고 redirect를 따르지 않습니다. PNG/JPEG/GIF/WebP만
161
+ 허용하며 응답 MIME type, magic byte, 실제 이미지 포맷이 모두 일치해야 합니다. 응답 본문은 최대
162
+ 512 KiB, 가로·세로는 각각 최대 4096 px입니다. 정적 이미지는 1 frame, 애니메이션은 최대 128
163
+ frame이며 `width × height × frame 수`로 계산한 frame 합산 pixel은 최대 16,000,000입니다. 검증
164
+ 실패는 해당 이미지 fallback으로 격리하며 원본 URL이나 실패 응답을 직접 렌더링하지 않습니다.
165
+
166
+ ## App bar
167
+
168
+ `app_bar`는 Host 화면 chrome을 구성합니다. tree 안에 앱 바를 다시 만들지 않습니다.
169
+
170
+ | 필드 | 규칙 |
171
+ |---|---|
172
+ | `title` | 필수, binding 가능, 최대 120자 |
173
+ | `subtitle` | 선택, binding 가능, 최대 160자 |
174
+ | `center_title`, `pinned` | boolean |
175
+ | `leading` | action item 하나 |
176
+ | `actions` | 최대 6개 |
177
+
178
+ action item은 `id`, `label`, 선택적 `icon`, `show_as`, 필수 `action`을 가집니다. `show_as`는
179
+ `auto`, `always`, `overflow`입니다. overflow에는 드문 행동을 두고 주요 행동은 화면 본문에 둡니다.
180
+
181
+ ## Navigation
182
+
183
+ `navigation.type`은 `tabs`, `bar`, `rail`, `drawer`, `adaptive`입니다. 각 item은 `id`, `label`,
184
+ 선택적 `icon`, `selected_icon`, `value`, 필수 `action`을 가집니다.
185
+
186
+ `selected_state_key`를 사용하면 해당 key가 `initial_state`에 있어야 하고 모든 item에 `value`가
187
+ 필요합니다. `label_behavior`는 `auto`, `always`, `selected`, `never`입니다. `rail_breakpoint`는
188
+ 480~1600입니다.
189
+
190
+ ## Component tree
191
+
192
+ 지원 node:
193
+
194
+ ```text
195
+ column row wrap grid stack positioned scroll padding center expanded
196
+ surface card section text icon image avatar badge divider spacer
197
+ button chip metric progress list timeline calendar chart
198
+ form dialog sheet field select switch empty
199
+ ```
200
+
201
+ 각 node의 목적과 props는 [컴포넌트 레퍼런스](components.md), 색·크기 범위는
202
+ [토큰과 반응형](design-tokens-responsive.md)을 참고하세요.
203
+
204
+ ## Action
205
+
206
+ ### Capability 실행
207
+
208
+ ```json
209
+ {
210
+ "type": "invoke",
211
+ "capability": "school_life.lookup",
212
+ "query": "{{state.query}}",
213
+ "arguments": {"limit": 10},
214
+ "store": "results"
215
+ }
216
+ ```
217
+
218
+ `store`는 선언된 data source ID이며 성공 결과로 해당 source를 교체합니다.
219
+
220
+ ### State와 refresh
221
+
222
+ ```json
223
+ {"type": "set_state", "values": {"period": "tomorrow"}, "persist": true}
224
+ ```
225
+
226
+ ```json
227
+ {"type": "refresh", "source": "schedule"}
228
+ ```
229
+
230
+ ### Navigation
231
+
232
+ ```json
233
+ {
234
+ "type": "navigate",
235
+ "target": "school_life.details",
236
+ "transition": "platform",
237
+ "replace": false
238
+ }
239
+ ```
240
+
241
+ `transition`은 `platform`, `fade`, `slide`, `none`입니다. `replace: true`는 현재 route를 뒤로가기
242
+ stack에서 교체합니다. 같은 manifest의 UI extension ID만 target이 될 수 있습니다.
243
+
244
+ ```json
245
+ {"type": "back"}
246
+ ```
247
+
248
+ ## 조건부 표시
249
+
250
+ ```json
251
+ {
252
+ "visible_when": {
253
+ "path": "data.schedule.data.items",
254
+ "exists": true
255
+ }
256
+ }
257
+ ```
258
+
259
+ `equals`, `not_equals`, `exists` 중 정확히 하나를 사용합니다. 조건은 state·data·현재 `item` 값만
260
+ 읽고 임의 expression을 실행하지 않습니다.
261
+
262
+ ## 로딩과 오류 복구
263
+
264
+ Host는 source별 실행을 격리하고 중복 실행을 합칩니다. 화면이 background로 가면 자동 refresh를
265
+ 멈추고 다시 foreground가 될 때 최신 source를 확인합니다. 갱신 실패 시 기존 정상 데이터가 있으면
266
+ 유지하고, 첫 로드도 실패한 경우에만 해당 영역에 retry 상태를 표시합니다. package generation이나
267
+ 화면이 바뀐 뒤 도착한 오래된 응답은 버립니다.
268
+
269
+ ## Response UI
270
+
271
+ `point: "response"`도 같은 `theme`, `app_bar`, `navigation`, data/state/node/action 계약을
272
+ 재사용합니다. 단, 결과는 capability를 실행한 AI 메시지의 한 response container 안에 렌더링됩니다.
273
+ 표현 세부는 [Response UI와 Agent Timeline](ai-response-and-timeline.md)을 참고하세요.
@@ -0,0 +1,128 @@
1
+ # 예제 검증 방법과 확인 경계
2
+
3
+ 자동 테스트, 외부 서비스 live test, Android 실기기 확인은 서로 다른 증거입니다. 하나의 성공을
4
+ 다른 단계의 성공으로 기록하지 않습니다. 실행 시각, 대상 version, 명령, 종료 코드, artifact
5
+ SHA-256을 함께 남기면 나중에 같은 결과를 재현할 수 있습니다.
6
+
7
+ ## 1. 패키지 무결성
8
+
9
+ 현재 저장소의 예제 artifact는 다음과 같습니다.
10
+
11
+ | 패키지 | 크기 | SHA-256 |
12
+ |---|---:|---|
13
+ | `school-life-1.4.0.mplg` | 262758 bytes | `f246d3abff4eac85e1a991d8f436c5381b394858843c95d6d4d88620a1b760b7` |
14
+ | `notion-1.1.0.mplg` | 3044 bytes | `eba57c440b305343c5cf917259e30e50b322272cfadb0ed64d39ecfc47539c4e` |
15
+
16
+ ```powershell
17
+ python tools/morit_plugin.py verify `
18
+ examples/plugins/school-life/dist/school-life-1.4.0.mplg
19
+ python tools/morit_plugin.py verify `
20
+ examples/plugins/notion/dist/notion-1.1.0.mplg
21
+ Get-FileHash -Algorithm SHA256 @(
22
+ 'examples/plugins/school-life/dist/school-life-1.4.0.mplg',
23
+ 'examples/plugins/notion/dist/notion-1.1.0.mplg'
24
+ )
25
+ ```
26
+
27
+ `verify`는 ZIP 안전성, canonical payload, embedded Ed25519 public key와 signature를 검사합니다.
28
+ 검증 결과에 publisher key가 포함되어 있어야 하며, trusted publisher 사전 등록 여부와 무관하게
29
+ 서명 자체가 유효해야 합니다. Host의 신뢰 정책은 설치·실행 단계에서 별도로 적용됩니다.
30
+
31
+ 문서의 hash는 source나 build 규칙이 바뀌면 달라집니다. 새 artifact를 만들었으면 위 명령으로
32
+ 실제 파일을 다시 계산한 뒤 문서와 배포 metadata를 함께 갱신합니다.
33
+
34
+ ## 2. 자동 회귀 테스트
35
+
36
+ 저장소 root에서 다음 검사를 먼저 실행합니다.
37
+
38
+ ```powershell
39
+ npx -y @morit/cli plugin validate .\examples\plugins\school-life
40
+ npx -y @morit/cli plugin validate .\examples\plugins\notion
41
+ python -m unittest discover -s backend/archive_processing -p "test_*.py"
42
+ python -m unittest discover -s backend/plugin_mcp -p "test_*.py"
43
+ dart analyze
44
+ flutter test
45
+ ```
46
+
47
+ 관련 backend 테스트는 정상 package 설치·복원·삭제, actor 격리, permission/readiness gate,
48
+ Connection OAuth state와 replay 차단, credential 암호화·갱신·해제, host-action outbox,
49
+ Slash·Search·Tool routing, UI/Response 계약을 검사합니다. transport fixture 테스트는 외부 서비스가
50
+ 응답하지 않아도 request/response parser 계약을 확인하지만 실제 사용자 계정의 성공을 증명하지는
51
+ 않습니다.
52
+
53
+ ## 3. Backend live E2E
54
+
55
+ `tools/plugin_live_e2e.py`는 임시 Supabase 사용자를 만들고 공개 API를 호출합니다. 관리자 key를
56
+ 사용하므로 승인된 운영자 환경에서만 실행하며 Secret, OAuth code, access token을 로그에 남기지
57
+ 않습니다.
58
+
59
+ ```powershell
60
+ $env:MORIT_E2E_ENV = 'C:\secure\morit-backend.env'
61
+ $env:MORIT_E2E_API = 'https://morit-api.moring.co'
62
+ $env:MORIT_SCHOOL_PLUGIN = 'examples/plugins/school-life/dist/school-life-1.4.0.mplg'
63
+ $env:MORIT_NOTION_PLUGIN = 'examples/plugins/notion/dist/notion-1.1.0.mplg'
64
+
65
+ # 실제 Notion 승인을 할 때만 0보다 큰 값을 사용합니다.
66
+ $env:MORIT_E2E_WAIT_NOTION_OAUTH_SECONDS = '300'
67
+ $env:MORIT_E2E_NOTION_QUERY = 'Morit 프로젝트'
68
+ $env:MORIT_E2E_NOTION_PAGE_QUERY = 'Morit 프로젝트 안내'
69
+ $env:MORIT_E2E_NOTION_DATA_SOURCE_QUERY = 'Morit 작업 목록'
70
+
71
+ python tools/plugin_live_e2e.py
72
+ ```
73
+
74
+ live E2E에서 확인할 핵심 순서는 다음과 같습니다.
75
+
76
+ 1. 신규 사용자의 설치·Instance 목록이 비어 있고 traversal package가 부분 상태 없이 차단됨
77
+ 2. 학교와 Notion package를 함께 설치하면 각 설치의 기본 Instance가 정확히 하나 생성됨
78
+ 3. 권한 승인 전 capability가 노출되지 않고, 승인 후 학교 Tool·Search·Slash·background가 등록됨
79
+ 4. 실제 NEIS 조회, 검색, 알림 outbox lease/ACK, 재시작 후 registry 복원이 동작함
80
+ 5. Notion 승인 시 기본 Instance의 Connection이 활성화되고 실제 page/data source를 읽음
81
+ 6. Connection 삭제 후 credential을 다시 쓰지 않으며 capability가 즉시 제거됨
82
+ 7. disable/delete/purge 뒤 cache, 설정, credential, pending action이 남지 않음
83
+
84
+ 같은 외부 서비스를 여러 계정으로 쓰는 경우 설치나 root Instance를 복제하지 않습니다. 하나의
85
+ 기본 Instance 아래 여러 Connection을 만들고 `allow_multiple` 정책과 계정별 격리를 검사합니다.
86
+
87
+ 스크립트 결과는 다음처럼 해석합니다.
88
+
89
+ | 출력 | 종료 코드 | 의미 |
90
+ |---|---:|---|
91
+ | `backend_live_plugin_e2e=passed` | 0 | 요청한 외부 단계까지 모두 확인 |
92
+ | `backend_live_plugin_e2e=partial reasons=...` | 2 | 일부 외부 승인·재시작 단계가 빠짐 |
93
+ | `backend_live_plugin_e2e=failed ...` | 1 | 구현·배포·API 검증 실패 |
94
+
95
+ Notion 사용자 승인을 생략했거나 운영 process 재시작 명령을 주지 않은 실행은 `partial`입니다.
96
+ `state=completed`나 검색 결과 수만으로 page/data source 본문 조회 성공을 추정하지 않습니다.
97
+
98
+ ## 4. Android 실기기 확인
99
+
100
+ Backend live test는 앱 화면과 Android 알림을 누르지 않습니다. release APK를 실제 기기에 설치해
101
+ 다음을 별도로 확인합니다.
102
+
103
+ 1. `.mplg` 파일 선택 후 설치·권한·초기 설정 흐름
104
+ 2. 학교 후보 선택, 오늘·주간·급식 화면과 빈 상태·오류 상태
105
+ 3. Notion 공식 승인, Connection 추가·전환·해제와 실제 page/data source 표시
106
+ 4. AI 자연어·Slash·Search와 해당 AI 메시지 안의 Response UI
107
+ 5. 알림 권한 승인 후 실제 알림 표시
108
+ 6. 강제 종료·재실행 뒤 설치·연결·화면 상태 복원
109
+ 7. 비활성화·삭제 뒤 화면·capability·알림 제거
110
+
111
+ 실기기에서 확인하지 않은 항목은 자동 테스트가 통과했더라도 `미검증`으로 남깁니다.
112
+
113
+ ## 5. 결과 기록 형식
114
+
115
+ ```text
116
+ 시각(KST):
117
+ source revision:
118
+ Plugin/Host version:
119
+ 실행 명령과 종료 코드:
120
+ artifact 경로·크기·SHA-256:
121
+ 자동 테스트:
122
+ live E2E:
123
+ Android/iOS/Desktop 실기기:
124
+ 생략 또는 실패한 단계와 이유:
125
+ ```
126
+
127
+ 성공 기록에는 실행하지 않은 항목을 넣지 않습니다. iOS·Desktop은 현재 Host 지원 범위를 먼저
128
+ 확인하고, 지원이 확정되지 않은 플랫폼을 Android 결과로 대신하지 않습니다.