@cheongfish/bouncer 0.0.0-stage → 0.1.13

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 (156) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/bin/bouncer +26 -0
  4. package/bin/bouncer-root +25 -0
  5. package/bin/build-stamp.js +78 -0
  6. package/bin/register-path.js +71 -0
  7. package/dist/.build-stamp +1 -0
  8. package/dist/approval-snapshot.js +99 -0
  9. package/dist/bouncer-root.js +80 -0
  10. package/dist/canonical-json.js +37 -0
  11. package/dist/cli-commit-guard-command.js +74 -0
  12. package/dist/cli-config-command.js +47 -0
  13. package/dist/cli-context-review-command.js +273 -0
  14. package/dist/cli-current-command.js +374 -0
  15. package/dist/cli-dispatch-command.js +119 -0
  16. package/dist/cli-doc-commands.js +352 -0
  17. package/dist/cli-epic-command.js +130 -0
  18. package/dist/cli-flags.js +50 -0
  19. package/dist/cli-git-commands.js +931 -0
  20. package/dist/cli-intent-command.js +491 -0
  21. package/dist/cli-metrics-command.js +134 -0
  22. package/dist/cli-ontology-command.js +109 -0
  23. package/dist/cli-project-commands.js +580 -0
  24. package/dist/cli-retro-command.js +138 -0
  25. package/dist/cli-review-command.js +251 -0
  26. package/dist/cli-review-dispatch-command.js +338 -0
  27. package/dist/cli-usage.js +72 -0
  28. package/dist/cli.js +70 -0
  29. package/dist/codex-agents.js +189 -0
  30. package/dist/command-argv.js +125 -0
  31. package/dist/commit-guard.js +72 -0
  32. package/dist/commit-hook.js +470 -0
  33. package/dist/commit-message.js +77 -0
  34. package/dist/commit-sha.js +107 -0
  35. package/dist/commit.js +519 -0
  36. package/dist/composite-gates.js +129 -0
  37. package/dist/comprehension.js +127 -0
  38. package/dist/config.js +298 -0
  39. package/dist/context-ids.js +288 -0
  40. package/dist/context-migration-rewrite.js +421 -0
  41. package/dist/context-ontology.js +746 -0
  42. package/dist/context-review-contracts.js +2 -0
  43. package/dist/context-review-projection.js +147 -0
  44. package/dist/context-review-state.js +824 -0
  45. package/dist/context-review-store.js +602 -0
  46. package/dist/context-review-transitions.js +412 -0
  47. package/dist/coordinate-advance.js +253 -0
  48. package/dist/coordinate-next.js +998 -0
  49. package/dist/coordinate-output.js +57 -0
  50. package/dist/coordinator-command-contracts.js +2 -0
  51. package/dist/coordinator-commands.js +1908 -0
  52. package/dist/coordinator-fanin.js +1021 -0
  53. package/dist/coordinator-git.js +121 -0
  54. package/dist/coordinator-input.js +37 -0
  55. package/dist/coordinator-ledger.js +381 -0
  56. package/dist/coordinator-prepare.js +1047 -0
  57. package/dist/coordinator-repair.js +1416 -0
  58. package/dist/coordinator-verification.js +42 -0
  59. package/dist/coordinator-worker.js +388 -0
  60. package/dist/coordinator.js +7 -0
  61. package/dist/cross-drive.js +85 -0
  62. package/dist/current.js +1148 -0
  63. package/dist/dag-shape.js +93 -0
  64. package/dist/doc-shape-contracts.js +241 -0
  65. package/dist/drive-metrics.js +691 -0
  66. package/dist/epic-index.js +347 -0
  67. package/dist/epic-registry.js +453 -0
  68. package/dist/execute-prepare.js +262 -0
  69. package/dist/execution-contracts.js +46 -0
  70. package/dist/execution-dag.js +148 -0
  71. package/dist/finalize-checks.js +177 -0
  72. package/dist/finalize-collect.js +343 -0
  73. package/dist/finalize-comprehension.js +116 -0
  74. package/dist/finalize-digest-collect.js +388 -0
  75. package/dist/finalize-digest-model.js +595 -0
  76. package/dist/finalize-digest.js +164 -0
  77. package/dist/finalize-explain.js +176 -0
  78. package/dist/finalize-pr.js +610 -0
  79. package/dist/finalize-release-main.js +652 -0
  80. package/dist/finalize.js +714 -0
  81. package/dist/frontmatter.js +22 -0
  82. package/dist/git-ops.js +49 -0
  83. package/dist/graph-exec.js +179 -0
  84. package/dist/graph-scope.js +268 -0
  85. package/dist/graphify.js +995 -0
  86. package/dist/guards.js +82 -0
  87. package/dist/import-git.js +78 -0
  88. package/dist/import-history.js +392 -0
  89. package/dist/import-render.js +59 -0
  90. package/dist/import-types.js +2 -0
  91. package/dist/init.js +571 -0
  92. package/dist/intent-bundle.js +936 -0
  93. package/dist/intent-provenance.js +927 -0
  94. package/dist/layout.js +131 -0
  95. package/dist/lease.js +145 -0
  96. package/dist/legacy-comments.js +71 -0
  97. package/dist/migrate-context-slugs.js +262 -0
  98. package/dist/migrate-merge-legacy-epics.js +392 -0
  99. package/dist/migrate-review-scope.js +144 -0
  100. package/dist/migrate-task-layout.js +194 -0
  101. package/dist/path-overlap.js +38 -0
  102. package/dist/paths.js +45 -0
  103. package/dist/plan-approve.js +218 -0
  104. package/dist/plan-inspect.js +575 -0
  105. package/dist/plan-prerequisites.js +197 -0
  106. package/dist/plan-snapshot-contract.js +98 -0
  107. package/dist/plan-snapshot.js +138 -0
  108. package/dist/plugin-root.js +237 -0
  109. package/dist/posix-path.js +14 -0
  110. package/dist/pre-commit-hook.js +170 -0
  111. package/dist/print-dispatch.js +285 -0
  112. package/dist/render.js +8 -0
  113. package/dist/retention-migration.js +595 -0
  114. package/dist/retro-read.js +259 -0
  115. package/dist/retro-sources.js +318 -0
  116. package/dist/review-dispatch.js +1054 -0
  117. package/dist/review-policy.js +28 -0
  118. package/dist/review-record.js +447 -0
  119. package/dist/review-repair-state.js +52 -0
  120. package/dist/review-snapshot.js +659 -0
  121. package/dist/run-detach.js +165 -0
  122. package/dist/run-preflight.js +368 -0
  123. package/dist/runtime-state.js +1038 -0
  124. package/dist/scaffold.js +533 -0
  125. package/dist/schema.js +294 -0
  126. package/dist/scope.js +1073 -0
  127. package/dist/seed-worktree.js +926 -0
  128. package/dist/session-graph.js +337 -0
  129. package/dist/subagents.js +86 -0
  130. package/dist/symbol-index.js +1390 -0
  131. package/dist/task-brief-hash.js +34 -0
  132. package/dist/task-commits.js +124 -0
  133. package/dist/tasks-docs.js +192 -0
  134. package/dist/templates.js +469 -0
  135. package/dist/time.js +21 -0
  136. package/dist/validate-docs.js +302 -0
  137. package/dist/validate-gates-finalize.js +303 -0
  138. package/dist/validate-gates-plan.js +720 -0
  139. package/dist/validate-gates-run.js +420 -0
  140. package/dist/validate-gates-shared.js +124 -0
  141. package/dist/validate-gates.js +87 -0
  142. package/dist/validate-sections.js +654 -0
  143. package/dist/validate-structural.js +347 -0
  144. package/dist/validate.js +417 -0
  145. package/dist/verification-contracts.js +26 -0
  146. package/dist/verification-evidence.js +233 -0
  147. package/dist/verification-execution.js +163 -0
  148. package/dist/verification-identity.js +413 -0
  149. package/dist/verification-target.js +51 -0
  150. package/dist/verification.js +383 -0
  151. package/dist/windows-npm.js +38 -0
  152. package/package.json +32 -4
  153. package/resources/graphify-compat.json +9 -0
  154. package/vendor/README.md +23 -0
  155. package/vendor/js-yaml.LICENSE +21 -0
  156. package/vendor/js-yaml.js +3022 -0
@@ -0,0 +1,469 @@
1
+ // scripts/lib/templates.js
2
+ // scaffold(및 finalize PR 본문)용 내장 문서 본문. 플러그인이 이 문자열을
3
+ // 소유하며 — 프로젝트에 설치되지 않고 프로젝트 수준 override도 없습니다.
4
+ // 모든 기본값은 게이트가 요구하는 본문 섹션을 충족합니다: tasks는 필수 절 작성 규칙,
5
+ // verification은 G13, review는 G14.
6
+ 'use strict';
7
+ // 리뷰 흐름 본문: 관련 이슈 → 배경·의도 → 변경 → 변경 그림 → 변경 전후 →
8
+ // 리뷰 포인트 → 되돌리기·영향 범위 → 확인 방법. 변경 그림·변경 전후·되돌리기는 필수 절이다. Features/Fixes 체크박스와 Bouncer 메타 절은 두지
9
+ // 않는다 — finalize는 Explain·diff·검증 증적만 조합한다.
10
+ const PR_TEMPLATE = `## 관련 이슈
11
+
12
+ - Explain: [<explain path>](<explain url>)
13
+
14
+ ## 배경 · 변경 의도
15
+
16
+ - 문제
17
+ - <problem>
18
+ - <problem>
19
+ - 의도
20
+ - <intent>
21
+ - <intent>
22
+
23
+ ## 주요 변경 내용
24
+
25
+ | 파일 | 변경 |
26
+ | --- | --- |
27
+ | <changed file or group> | <what changed> |
28
+
29
+ ## 변경 그림
30
+
31
+ \`\`\`mermaid
32
+ flowchart TD
33
+ A[<변경 전>] --> B[<변경 후>]
34
+ \`\`\`
35
+
36
+ ## 변경 전후
37
+
38
+ | 항목 | 변경 전 | 변경 후 |
39
+ | --- | --- | --- |
40
+ | <behavior> | <before; 변경 전 결과 미관측 when not observed> | <after> |
41
+
42
+ ## 리뷰 포인트
43
+
44
+ - <review points>
45
+
46
+ ## 되돌리기 · 영향 범위
47
+
48
+ - <revert method, unrecoverable effects, affected consumers>
49
+
50
+ ## 확인 방법
51
+
52
+ - <verification summary>
53
+ `;
54
+ // 작성 가이드는 더 이상 템플릿 HTML 주석이 아니다. 안내 본문은
55
+ // LEGACY_SCAFFOLD_COMMENT_BODIES에 동결하고, 새 문서는 `<TODO: …>`만 남긴다.
56
+ // plan gate는 섹션이 비었는지 판단하기 전에 주석을 제거하고 남은 `<TODO:`
57
+ // 토큰이 있으면 거부합니다, 따라서 손대지 않은 템플릿은 통과할 수 없습니다.
58
+ // 문서 간 링크는 bundle-relative(§5.1)가 아니라 relative(OKF §5.2)입니다:
59
+ // 둘 다 유효하지만, 선행 `/`를 저장소 루트에 대해 해석하는 웹 git 호스트에서
60
+ // survive하는 것은 relative 형식뿐입니다.
61
+ const TEMPLATES = {
62
+ 'epic.md': `# <EPIC-id> <name>
63
+
64
+ ## Intent
65
+ - 문제: <TODO: 해결하려는 문제>
66
+ - 목표: <TODO: 완료 시 달라지는 것>
67
+
68
+ ## Success criteria
69
+ 1. <TODO: 무엇이 참이 되면 이 에픽이 끝인가>
70
+
71
+ ## Out of scope
72
+ - <TODO: 이 에픽에서 다루지 않을 것>
73
+
74
+ ## Blueprints
75
+ * [<TODO: 00x 제목>](blueprints/<TODO: 00x-slug>/index.md) - <TODO: 한 줄 목적 — what changes + where touched>
76
+ `,
77
+ 'blueprint.md': `# <BP-id> <name>
78
+
79
+ Epic: [<EPIC-id>](../../index.md)
80
+
81
+ ## Intent
82
+ - 문제: <TODO: 이 blueprint가 해결하는 것>
83
+ - 완료 조건: <TODO: 무엇이 되면 끝인가>
84
+
85
+ ## Contract
86
+ - 인터페이스: <TODO: 추가·변경되는 공개 인터페이스>
87
+ - 데이터·상태: <TODO: 스키마 / 상태 변화>
88
+ - 수용 기준: <TODO: 무엇이 되면 이 blueprint가 성공인가>
89
+ - 검증 명령: <TODO: 성공을 증명할 명령 (예: npm test)>
90
+ - 실패 모드·엣지 케이스: <TODO: Happy path 밖 — 실패·경계 조건>
91
+
92
+ ## Out of scope
93
+ - <TODO: 이 blueprint에서 하지 않을 것>
94
+
95
+ ## One-commit justification
96
+ - <TODO: 한 커밋에 들어가는 이유>
97
+ `,
98
+ 'tasks.md': `# Tasks
99
+
100
+ Blueprint: [<BP-id>](../../index.md)
101
+
102
+ ## Goal & intent
103
+ <TODO: 완료 후 시스템이 어떻게 달라지는가>
104
+
105
+ ## Current behavior
106
+ <TODO: 현재 시스템이 이 범위에서 어떻게 동작하는가>
107
+
108
+ ## Target behavior
109
+ <TODO: 완료 후 성공·실패·보존이 어떻게 달라지는가>
110
+
111
+ ## Interface
112
+ - 제공: <TODO: 새로 생기거나 바뀌는 공개 시그니처·산출물>
113
+ - 거부: <TODO: 받아들이지 않는 입력과 그때의 동작>
114
+
115
+ ## Touch
116
+ | 경로 | 심볼 | 변경 | 현재 책임 | 계획한 변경 | 근거 |
117
+ | --- | --- | --- | --- | --- | --- |
118
+ | \`<TODO: 경로>\` | \`<TODO: 심볼>\` | Modify | <TODO: 현재 책임> | <TODO: 계획한 변경> | <TODO: 근거>
119
+
120
+ ## Do not touch
121
+ - \`<TODO: 보호할-경로>\` — <TODO: 왜 건드리면 안 되는가>
122
+
123
+ ## Constraints
124
+ - <TODO: 이번 작업 전체에 걸리는 규칙>
125
+
126
+ ## Checklist
127
+ - [ ] <TODO: 작업 항목>
128
+ `,
129
+ // verification task(execution_kind: verification) 전용 본문. commit 본문의
130
+ // Touch 표는 백틱 경로 자리표시를 담아, 그대로 물려받으면 G20이 source 변경
131
+ // 선언으로 읽는다. 그래서 Touch·Interface·Do not touch는 경로 후보가 나오지
132
+ // 않는 고정 문구로 채우고, 작성자가 쓸 곳(Goal & intent·Checklist)만 TODO로 남긴다.
133
+ // light blueprint도 이 본문을 그대로 쓴다 — `-light` 사본을 두지 않는다(templateNameFor).
134
+ 'verification-tasks.md': `# Tasks
135
+
136
+ Blueprint: [<BP-id>](../../index.md)
137
+
138
+ ## Goal & intent
139
+ <TODO: 이 검증이 증명하는 종단 조건>
140
+
141
+ ## Interface
142
+ - 제공: 구현 task가 모두 통합된 뒤 전체 검증 명령을 한 번 실행해 남기는 종단 검증 증적. 구현 task는 각자 task 전용 명령만 갖는다.
143
+ - 거부: source 변경, review 문서, commit.
144
+
145
+ ## Touch
146
+ Source 변경 경로 없음.
147
+
148
+ ## Do not touch
149
+ - 모든 source 경로 — verification task는 증적만 남기고 파일을 바꾸지 않는다.
150
+
151
+ ## Checklist
152
+ - [ ] <TODO: 검증 항목>
153
+ `,
154
+ 'verification.md': '# Verification\n\n## Command\n<command>\n\n## Evidence\n<result>\n',
155
+ // G14와 기록 검증은 findings[]와 status로 미완성을 거절한다. `- <finding>` 자리표시만
156
+ // 두면 stripComments 후 섹션이 비지 않고, 파서가 예시 finding을 실제 값으로
157
+ // 읽지도 않는다. 허용값 안내는 동결 목록·reference가 정본이다.
158
+ 'review.md': `# Review
159
+
160
+ ## Findings
161
+ - <finding>
162
+ `,
163
+ 'context-review.md': `# Context review
164
+
165
+ ## Findings
166
+ - <finding>
167
+ `,
168
+ // G16는 필수 세 절(Background·Code·Quiz)이 비어 있으면 거절한다. 주석을 지워도
169
+ // 제목만 있는 본문은 손대지 않은 tasks 템플릿이 TODO 검사에 걸리듯 통과할 수 없다.
170
+ // Intuition은 새 문서에 만들지 않는다 — 기존 Explain의 Intuition은 파서가
171
+ // 선택적 legacy 절로 계속 읽는다.
172
+ 'explain.md': `# Explain
173
+
174
+ ## Background
175
+
176
+ ## Code
177
+
178
+ ## Quiz
179
+
180
+ ## Tasks
181
+ `,
182
+ // --- scale: light 전용 본문 ---
183
+ // light scaffold는 네 문서(index + 루트 review + tasks/001 두 문서) 전체 줄 수를
184
+ // 100줄 이하로 묶는 계약이다(rules/planning.md). scoped_terminal light의 종단
185
+ // verification task(`scaffold task --execution-kind verification`, 두 문서)는
186
+ // 이 예산 밖에서 따로 붙는다. 그래서 full 템플릿의 작성
187
+ // 가이드 주석을 옮겨오지 않는다 — 가이드는 skills/spec-authoring이 갖고,
188
+ // 본문에는 게이트가 요구하는 제목과 <TODO:> 자리만 남긴다.
189
+ // full 본문은 바이트 단위로 그대로 두고 여기서만 갈라진다.
190
+ 'blueprint-light.md': `# <BP-id> <name>
191
+ Epic: [<EPIC-id>](../../index.md) · Tasks: [001](tasks/001/tasks.md)
192
+ ## Intent
193
+ - <TODO: 무엇을 바꾸고 무엇이 되면 끝인가>
194
+ `,
195
+ // light task의 필수 절(작성 규칙)은 Goal & intent / Touch / Checklist 셋뿐이다.
196
+ // Interface·Do not touch 제목을 템플릿에 남기면 빈 절이 그대로 남아
197
+ // 사람이 읽을 때 full과 같은 계약으로 오해된다.
198
+ // DAG frontmatter 기본값 3줄을 상쇄하려고 본문 빈 줄을 줄인다(100줄 예산).
199
+ 'tasks-light.md': `# Tasks
200
+ ## Goal & intent
201
+ <TODO: 완료 후 시스템이 어떻게 달라지는가>
202
+ ## Touch
203
+ - Modify \`<TODO: 수정할-파일>\` — <TODO: 왜 만지는가>
204
+ ## Checklist
205
+ - [ ] <TODO: 작업 항목>
206
+ `,
207
+ // verification은 light 전용 본문이 없다. full 본문에 줄일 주석이 없어
208
+ // 사본을 두면 드리프트만 생기므로, scaffold가 공용 `verification.md`로
209
+ // 떨어진다(templateNameFor).
210
+ // G14도 그대로. 허용값 주석만 뺀 최소 본문.
211
+ 'review-light.md': '# Review\n\n## Findings\n- <finding>\n',
212
+ 'pr.md': PR_TEMPLATE,
213
+ };
214
+ /**
215
+ * 템플릿 주석 비교용 공백 정규화.
216
+ * 복사 과정에서 줄 끝과 들여쓰기만 달라진 안내 주석은 같은 표식으로
217
+ * 취급하되, 저자가 쓴 주석의 문장 자체는 임의로 바꾸지 않는다.
218
+ *
219
+ * @param {string} body - HTML 주석 안의 원문
220
+ * @returns {string} 줄 끝·줄별 공백을 접은 본문
221
+ */
222
+ function normalizeCommentBody(body) {
223
+ return body
224
+ .replace(/\r\n?/g, '\n')
225
+ .split('\n')
226
+ .map((line) => line.trim())
227
+ .join('\n')
228
+ .trim();
229
+ }
230
+ /**
231
+ * HTML 주석 본문을 정규화해 등장 순서대로 돌려준다.
232
+ * findLegacyScaffoldComments가 동결 목록과 같은 정규화를 쓰게 한 경로다.
233
+ * TASKS 체크리스트가 동결 주석을 인라인 코드로만 인용하면 leftover가 아니므로
234
+ * 단일 백틱 스팬 안의 일치만 빼고, 본문에 남은 실제 주석은 그대로 반환한다.
235
+ *
236
+ * @param {string} body - HTML 주석이 들어 있을 수 있는 Markdown 본문
237
+ * @returns {string[]} 정규화된 주석 본문. 주석이 없으면 []
238
+ */
239
+ function extractCommentBodies(body) {
240
+ // 1. 단일 백틱 스팬만 공백으로 가린다. 길이를 유지해야 인접 텍스트가
241
+ // `<!--`/`-->`로 이어 붙지 않는다. ``` 펜스는 열지 않는다 — 계약이
242
+ // 인라인 코드 인용만 제외하기 때문이다.
243
+ const withoutInlineCode = body.replace(/`[^`\n]*`/g, (span) => ' '.repeat(span.length));
244
+ // 2. 가린 본문에서만 주석을 모아, 백틱 안 예시는 동결 목록과 맞춰도 빠진다.
245
+ return Array.from(withoutInlineCode.matchAll(/<!--[\s\S]*?-->/g), (match) => normalizeCommentBody(match[0].slice(4, -3)));
246
+ }
247
+ // 템플릿에서 주석을 지운 뒤에도 lint가 옛 초안을 잡으려면 목록이 생성되면 안 된다.
248
+ // 템플릿을 고쳐도 이 배열은 그대로여야 옛 시드 사본이 계속 걸린다.
249
+ // 긴 본문은 + 로만 나눈다. 값을 바꾸면 findLegacyScaffoldComments가 옛 시드를 놓친다.
250
+ const LEGACY_SCAFFOLD_COMMENT_BODIES = Object.freeze([
251
+ '왜 지금 이 에픽인가. 두 문장 이내.',
252
+ 'discovery가 정리한 성공 조건이 남는 자리. 판정 가능한 것만 번호를 붙여 적고,\nblueprint의 수용 기준과 리뷰가 이 번호를 참조합니다.\n' +
253
+ '"개선한다" "정리한다" 처럼 참·거짓을 가릴 수 없는 문장은 조건이 아닙니다.',
254
+ '여기 적은 항목이 blueprint의 Do not touch로 이어집니다.',
255
+ 'OKF §6 인덱스 형식. 새 blueprint를 만드는 기준은 하나 — 한 커밋으로\n리뷰 가능한 단위인가. 더 크면 blueprint를 쪼갠다. 하위 태스크 계층은\n' +
256
+ '만들지 않는다 (rules/planning.md).\n한 줄 목적에는 무엇이 바뀌는지(what)와 어디를 건드리는지(where)를\n함께 적는다. 기존 라인은 소급 수정하지 않는다.',
257
+ 'Contract-First: 계약만. 구현 코드 금지.\n시그니처·타입·의사코드는 블록당 20줄 이하.\n길어지면 구현 상세가 새는 신호이니 tasks.md로 넘기거나 blueprint를 쪼갭니다.\n' +
258
+ '금지: 계약 클래스·메서드 본문, As-Is/To-Be 코드 덤프, 단계별 구현 시퀀스,\n실행 가능한 테스트 본문 → tasks.md로 이연.\n' +
259
+ '본문 분량 예산 ~250줄. 초과는 구현 상세 누출 신호 — 쪼개거나 이연.',
260
+ 'rules/planning.md: blueprint는 한 번에 리뷰 가능한 커밋 하나에 맞춘다.\n이 칸을 못 채우겠으면 blueprint를 쪼갤 신호입니다.',
261
+ 'explain.md는 plan scaffold에 포함되지 않습니다. /bouncer-finalize가 작성합니다.',
262
+ '구현자가 다른 문서 없이 시작할 수 있게.\n수용 기준과 검증 명령도 여기에 적거나 Checklist에 명시한다.',
263
+ 'bouncer.commit_intent와 bouncer.commit_summary는 각각 1~2개의 한국어\n종결 문장으로 작성한다. 두 필드의 합계는 커밋 본문 네 줄을 넘지 않는다.',
264
+ 'DAG frontmatter (author-written):\nexecution_kind: commit | verification. 부재 = commit.\n' +
265
+ 'verification은 source 변경·review.md·commit 없이 종단 CI 증적만 남긴다.\ndepends_on: TASKS-NNN id 배열. 부재·[] = 의존 없음.\n' +
266
+ 'parallel_safe: boolean. false/부재 = 순차 wave 입력.\ndependency_gate: integrated. 부재 = integrated.\n' +
267
+ '실행 순서는 task 번호가 아니라 이 세 필드가 결정한다.',
268
+ '지금 코드·게이트가 하는 일. 재현 경로·관측 결과를 적어 구현자가\n목표를 추론하지 않게 합니다. 절이 있으면 G10이 TODO 자리표시를 거부합니다.',
269
+ '이 task가 끝나면 관측 가능해야 하는 성공·실패·보존 동작.\nCurrent behavior와 같이 선택 절이지만, 쓰면 자리표시를 비워 두면 안 됩니다.',
270
+ '계약이 리뷰에서 검증 가능하도록 제공하는 것과 거부하는 것을 함께 적습니다.',
271
+ 'frontmatter bouncer.affected_paths의 모든 경로가 여기서 정당화되어야 합니다 (G11).\n파일 단위 행으로 적습니다. 디렉터리 하나로 뭉치면 그 안 모든 파일이 열려\n' +
272
+ 'G11이 사실상 통과만 합니다. 경로·심볼은 백틱으로 감쌉니다.\n열: 경로 | 심볼(함수·상수·절 키) | 변경(Create/Modify/Delete/Rename) |\n' +
273
+ '현재 책임 | 계획한 변경 | 근거(왜 이 심볼인가).',
274
+ '여기 적은 경로가 affected_paths와 겹치면 G12가 막습니다.\nepic / blueprint의 Out of scope에서 이어받습니다.',
275
+ '경로로 표현되지 않는, 작업 전체에 걸리는 규칙. 허용된 파일 안에서도 지켜야 합니다.\n예: 하위 호환 별칭을 남기지 않는다 / 기존 게이트 번호와 본문 계약을 유지한다 /\n' +
276
+ '공개 문자열은 한국어를 유지한다.\n막을 대상이 경로뿐이면 Do not touch에 적습니다.',
277
+ '각 항목은 구현자가 순서대로 실행 가능해야 합니다.\n행위를 바꾸는 항목은 실패 테스트 → 실패 확인 → 구현 순서로 적습니다.\n' +
278
+ '기대하는 assertion·상수·명령은 코드블록으로 그대로 적어 해석 여지를 없앱니다.\nblueprint Contract에서 이연된 테스트 본문·구현 시퀀스가 들어올 자리입니다.\n' +
279
+ '수용 기준·검증 명령을 체크 항목으로 포함하세요.',
280
+ '이 verification task가 선행 task 통합 뒤 무엇을 증명하는지 적는다.\n실행 명령은 frontmatter bouncer.verify 한 줄이며 본문에 다시 적지 않는다.',
281
+ 'verification task는 source를 바꾸지 않는다. 이 절에 백틱 토큰이나 경로를\n적으면 G20이 source 변경 선언으로 보고 거절하므로 아래 문구를 그대로 둔다.',
282
+ 'finding: id, severity, status. mode를 쓰는 rounds[]면 category, brief_clause, file,\n' +
283
+ 'symbol, fingerprint, actionability, origin, first_seen_round, last_seen_round도 필수.\n' +
284
+ 'severity: blocker | major | minor | nit\n' +
285
+ 'status: resolved | accepted | deferred | open (open은 해결 전 finding이며 accepted 리뷰에는 남을 수 없음)\n' +
286
+ 'actionability: must_fix | advisory\norigin: discovery | introduced_by_revision | missed_critical\n' +
287
+ 'fingerprint: <category>:<brief_clause>:<file>#<symbol> (앞뒤 공백 제거, category·brief_clause 소문자, file의 ./ 제거)\n' +
288
+ 'accepted note: 권한 있는 위험 수용 근거\ndeferred note: 현재 task와 독립인 후속 planning 항목 근거\n' +
289
+ 'optional bouncer.review.rounds[]: round (양의 정수), mode (discovery | delta | critical_recovery),\n' +
290
+ 'target (blueprint 범위 base·head — task 묶음이 아니라 이 루트 review.md 하나),\n' +
291
+ 'perspectives (combined | spec_scope | correctness_tests | minimality_maintainability | security,\n' +
292
+ 'target_head는 target.head와 동일), previous_finding_ids (문자열 배열), new · resolved · regressed\n' +
293
+ '(0 이상 정수). mode 없는 구문서는 기존 계약으로 통과한다.',
294
+ 'finding: id, severity, status. accepted이면 note 필수.\nseverity: blocker | major | minor | nit\n' +
295
+ 'status: resolved | accepted\n' +
296
+ 'mode를 쓰는 rounds[]면 category, brief_clause, file, symbol, fingerprint, actionability,\n' +
297
+ 'origin, first_seen_round, last_seen_round도 필수.\n' +
298
+ 'category: combined | local | global | consistency | scope_minimality | verifiability | risk_security\n' +
299
+ '(cross_document | scope | korean_quality | success_criteria는 기존 기록 읽기 호환용)\n' +
300
+ 'brief_clause: finding이 걸린 문서 절 (예: tasks/002 Interface)\nfile: 계획 문서의 저장소 상대 경로, symbol: 절 제목 slug (절이 없으면 -)\n' +
301
+ 'fingerprint: context:<category>:<brief_clause>:<file>#<symbol> (앞뒤 공백 제거, category·brief_clause 소문자, file의 ./ ' +
302
+ '제거)\nactionability: must_fix | advisory\norigin: discovery | introduced_by_revision | missed_critical\n' +
303
+ 'optional bouncer.context_review.rounds[]: round (양의 정수), mode (discovery | delta),\ntarget (digest),\n' +
304
+ 'perspectives (combined | local | global | consistency | scope_minimality | verifiability | risk_security 사용.\n' +
305
+ 'cross_document | scope | korean_quality | success_criteria는 기존 기록 읽기 호환용,\n' +
306
+ 'target_digest는 target.digest와 동일), severity_changes. rounds 없는 구문서는 기존 계약으로 통과한다.',
307
+ '이 변경이 생긴 배경. 무엇을 고치려 했는가.',
308
+ '한 줄로 말하면 무엇인가. 비유·그림이 있으면 여기.',
309
+ '핵심 경로와 읽어야 할 파일. 긴 덤프 금지.',
310
+ '이해 확인 질문. 채점·기록 절차는 explain-diff 스킬이 안내한다.',
311
+ '퀴즈 결과와 disposition을 task별 소제목 없이 단일 블록으로.\ncomprehension 프론트매터(BP 엔트리 하나)와 맞춰 적는다.',
312
+ 'finalize가 삭제하기 전에 task의 Goal & intent, Current behavior, Target behavior,\n' +
313
+ 'Interface, Touch, Constraints를 task별 소제목으로 보존한다.\n' +
314
+ '제목은 ### EPIC-ddd/BP-ddd/TASK-ddd · `sha8`(trailer SHA가 있을 때) 또는\n' +
315
+ '### EPIC-ddd/BP-ddd/TASK-ddd(없을 때)이며, stable ID를 만들 수 없으면\n기존 ### Task NNN이다. Do not touch는 보존하지 않는다.\n' +
316
+ '이 절은 선택 사항이며 G16 필수 절이 아니다.',
317
+ ]);
318
+ /**
319
+ * 본문에서 동결된 옛 스캐폴드 안내 주석만 골라 등장 순서대로 돌려준다.
320
+ * 저자 주석은 목록에 없으므로 빠진다. 문자열이 아니면 TypeError로 거절한다.
321
+ *
322
+ * @param {string} body - 검사할 Markdown 본문
323
+ * @returns {string[]} 정규화된 안내 주석 본문. 빈 문자열·일치 없음이면 []
324
+ */
325
+ function findLegacyScaffoldComments(body) {
326
+ if (typeof body !== 'string') {
327
+ throw new TypeError('findLegacyScaffoldComments: body must be a string');
328
+ }
329
+ // 1. 동결 목록을 Set으로 두어 주석마다 선형 탐색하지 않는다.
330
+ const legacy = new Set(LEGACY_SCAFFOLD_COMMENT_BODIES);
331
+ // 2. 문서에 나타난 순서 그대로 걸러, lint 메시지가 파일 안 순서와 맞는다.
332
+ return extractCommentBodies(body).filter((normalized) => legacy.has(normalized));
333
+ }
334
+ function readTemplate(name) {
335
+ // 키 목록을 유니온으로 닫으면 알 수 없는 이름에서 컴파일 오류가 나고,
336
+ // 런타임의 `unknown template` throw 계약을 테스트가 더 이상 칠 수 없다.
337
+ const catalog = TEMPLATES;
338
+ const body = catalog[name];
339
+ if (typeof body !== 'string') {
340
+ throw new Error(`unknown template: ${name}`);
341
+ }
342
+ return body;
343
+ }
344
+ function renderTemplate(body, { epicId, blueprintId, name }) {
345
+ return body
346
+ .replace(/<EPIC-id>/g, epicId || '')
347
+ .replace(/<BP-id>/g, blueprintId || '')
348
+ .replace(/<name>/g, name || '');
349
+ }
350
+ function templateBody(templateName, vars) {
351
+ return renderTemplate(readTemplate(templateName), vars);
352
+ }
353
+ // 커밋 본문은 저자가 쓴 문장만 받아야 한다. 빈 값을 걸러내거나 앞부분만
354
+ // 잘라내면 잘못된 계획이 조용히 다른 메시지로 바뀌므로, 필드 단위로 원자적으로
355
+ // 검증한다. 기존 문서의 필드 부재(undefined)는 호환을 위해 빈 배열로 둔다.
356
+ //
357
+ // 요약: task의 commit_intent/commit_summary 목록을 검증하고 정규화한다.
358
+ // 인자:
359
+ // raw: frontmatter에서 읽은 원시 값(undefined, 배열, 그 외).
360
+ // field: 오류 메시지에 쓰는 필드 이름.
361
+ // 반환: 트림된 1-2개 문장 배열. undefined와 `[]`는 본문 없음을 뜻하는 `[]`.
362
+ // `[]`는 scaffold 기본값이자 plan gate(S32)가 허용하는 값이라 커밋에서도
363
+ // 부재와 같게 취급한다. 빈 목록 거절이 필요한 blueprint Intent는
364
+ // parseIntentBody가 직접 검사한다.
365
+ function normalizeAuthoredLines(raw, field) {
366
+ if (raw === undefined)
367
+ return [];
368
+ if (Array.isArray(raw) && raw.length === 0)
369
+ return [];
370
+ // YAML `field: 한 문장`은 리스트가 아니다. 한 항목으로 접으면 잘못된
371
+ // 작성이 커밋에 그대로 실리므로, plan S32와 같이 리스트 형태만 받는다.
372
+ if (typeof raw === 'string') {
373
+ throw new Error(`${field} must be a YAML list of 1-2 Korean terminal sentences`);
374
+ }
375
+ if (!Array.isArray(raw) || raw.length < 1 || raw.length > 2) {
376
+ throw new Error(`${field} must contain 1-2 Korean terminal sentences`);
377
+ }
378
+ const lines = raw.map((value) => {
379
+ if (typeof value !== 'string') {
380
+ throw new Error(`${field} must contain 1-2 Korean terminal sentences`);
381
+ }
382
+ const line = value.trim();
383
+ // 판정은 형식만 본다: 빈 문장, 줄바꿈, 한글 부재, 한국어 종결형 부재.
384
+ // 영문 식별자·경로·패키지 이름·backtick 인용은 받는다. 계약 용어
385
+ // (예: `integrationBranch 값을 재계산 없이 사용함.`)를 막으면 정확한
386
+ // 한국어 문장까지 커밋 직전에 거절되고, 저자는 용어를 뭉개 쓰게 된다.
387
+ // Epic·Blueprint id를 넣지 말라는 권고는 문서 몫이며 여기서 강제하지 않는다.
388
+ if (!line || line.includes('\n') || !/[가-힣]/u.test(line)
389
+ || !/(?:함|임|음|됨|줌|둠|남|김|씀|듦|림|움|춤|짐|감|앰|냄|꿈|뜀|다|요|죠|까|네|지)[.!?]?$/u.test(line)) {
390
+ throw new Error(`${field} must contain 1-2 Korean terminal sentences (한국어 종결 문장)`);
391
+ }
392
+ return line;
393
+ });
394
+ return lines;
395
+ }
396
+ // blueprint Intent는 문서 본문이 정본이다. heading 밖의 내용을 섞지 않고,
397
+ // bullet 표식만 벗겨 같은 본문 렌더러가 task/finalize 양쪽을 조립하게 한다.
398
+ // mermaid·코드 펜스는 문장이 아니므로 집계에서 뺀다 — 펜스를 문장으로 읽으면
399
+ // 다이어그램이 있는 승인 문서가 finalize 직전에 터진다(S33이 막는 실패 모드).
400
+ // 섹션 경계(`## `)는 펜스보다 먼저 자르므로, 펜스 안의 `## `도 예전처럼
401
+ // Intent를 끝낸다(펜스 인식으로 섹션 길이를 늘리지 않는다).
402
+ //
403
+ // 요약: blueprint 본문의 `## Intent` 섹션에서 1-2개 한국어 문장을 뽑는다.
404
+ // 인자:
405
+ // body: blueprint index 문서 본문(문자열이 아니면 malformed).
406
+ // 반환: bullet 표식·코드 펜스를 벗긴 문장 배열.
407
+ // 빈 섹션은 normalizeAuthoredLines가 `[]`를 허용하게 되었으므로 여기서 직접
408
+ // 거절한다(기존과 같은 메시지 유지).
409
+ function parseIntentBody(body) {
410
+ if (typeof body !== 'string') {
411
+ throw new Error('blueprint Intent is missing or malformed');
412
+ }
413
+ const withoutComments = body.replace(/<!--[\s\S]*?-->/g, '');
414
+ const heading = /^##\s+Intent\s*$/im.exec(withoutComments);
415
+ if (!heading || heading.index == null)
416
+ throw new Error('blueprint Intent is missing or malformed');
417
+ const afterHeading = withoutComments.slice(heading.index + heading[0].length);
418
+ const nextHeading = /^##\s+/m.exec(afterHeading);
419
+ const section = nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading;
420
+ // 펜스 판정은 불릿·공백을 벗긴 뒤 한다. 여는 줄은 ` 또는 ~ 3개 이상이면
421
+ // 언어 태그가 있어도 열고, 닫는 줄은 같은 문자만·개수 ≥ 열림이어야 한다
422
+ // (```x 처럼 뒤에 글자가 있으면 닫히지 않음 — CommonMark에 맞춤).
423
+ const lines = [];
424
+ let fenceChar = null;
425
+ let fenceLen = 0;
426
+ for (const raw of section.split('\n')) {
427
+ const trimmed = raw.trim();
428
+ if (!trimmed)
429
+ continue;
430
+ const content = trimmed.replace(/^[-*+]\s+/, '').trim();
431
+ const fence = /^(?<marker>[`~]{3,})(?<rest>.*)$/.exec(content);
432
+ if (fence && fence.groups) {
433
+ const marker = fence.groups.marker;
434
+ const rest = fence.groups.rest;
435
+ const char = marker[0];
436
+ const len = marker.length;
437
+ if (fenceChar === null) {
438
+ fenceChar = char;
439
+ fenceLen = len;
440
+ continue;
441
+ }
442
+ // 닫힘: 같은 문자·길이 이상·뒤에 다른 글자 없음. 아니면 펜스 안 줄로 건너뛴다.
443
+ if (char === fenceChar && len >= fenceLen && rest === '') {
444
+ fenceChar = null;
445
+ fenceLen = 0;
446
+ }
447
+ continue;
448
+ }
449
+ if (fenceChar !== null)
450
+ continue;
451
+ lines.push(content);
452
+ }
453
+ if (lines.length === 0) {
454
+ throw new Error('blueprint Intent must contain 1-2 Korean terminal sentences');
455
+ }
456
+ return normalizeAuthoredLines(lines, 'blueprint Intent');
457
+ }
458
+ module.exports = {
459
+ TEMPLATES,
460
+ PR_TEMPLATE,
461
+ LEGACY_SCAFFOLD_COMMENT_BODIES,
462
+ findLegacyScaffoldComments,
463
+ normalizeCommentBody,
464
+ readTemplate,
465
+ renderTemplate,
466
+ templateBody,
467
+ normalizeAuthoredLines,
468
+ parseIntentBody,
469
+ };
package/dist/time.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+ /** 고정 KST 오프셋(Asia/Seoul, DST 없음)으로 instant를 ISO-8601 형식으로 포맷한다. */
3
+ function nowIsoKst(date = new Date()) {
4
+ const parts = new Intl.DateTimeFormat('en-CA', {
5
+ timeZone: 'Asia/Seoul',
6
+ year: 'numeric',
7
+ month: '2-digit',
8
+ day: '2-digit',
9
+ hour: '2-digit',
10
+ minute: '2-digit',
11
+ second: '2-digit',
12
+ hourCycle: 'h23',
13
+ }).formatToParts(date);
14
+ const get = (type) => {
15
+ const part = parts.find((p) => p.type === type);
16
+ return part ? part.value : '00';
17
+ };
18
+ const ms = String(date.getUTCMilliseconds()).padStart(3, '0');
19
+ return `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}.${ms}+09:00`;
20
+ }
21
+ module.exports = { nowIsoKst };