@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,288 @@
1
+ 'use strict';
2
+ const posixPath = require("./posix-path");
3
+ const { toPosix } = posixPath;
4
+ const SLUG_MAX = 40;
5
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+ // 구형 번호 세그먼트는 base `paths.ts`처럼 나머지 문자를 제한하지 않는다(공백 든 구형 이름도 번호 형식).
7
+ const NUMERIC_SEG_RE = /^(\d{3})-([^/]+)$/;
8
+ const THREE_DIGITS_RE = /^\d{3}$/;
9
+ // 번호 epic 디렉터리 이름. 접두 번호만 읽는 형태와 번호·slug로 나누는 형태가 호출부마다 달라 둘을 둔다.
10
+ const NUMERIC_PREFIX_RE = /^(\d{3})-/;
11
+ const NUMERIC_DIR_RE = /^(\d{3})-(.+)$/;
12
+ // 본문 어디서든 구형 stable ID를 찾는 전역 형태(앵커 없음). 전역 regex는 lastIndex 상태가 있어 호출마다 새로 만든다.
13
+ const OLD_STABLE_SCAN_SRC = 'EPIC-\\d{3}\\/BP-\\d{3}(?:\\/TASK-\\d{3})?';
14
+ const OLD_STABLE_RE = /^EPIC-(\d{3})\/BP-(\d{3})(?:\/TASK-(\d{3}))?$/;
15
+ // epics/<seg> 뒤에 선택적으로 blueprints/<seg>가 바로 이어진다.
16
+ const PATH_RE = /(?:^|\/)epics\/([^/]+)(?:\/blueprints\/([^/]+))?/;
17
+ /**
18
+ * slug 규칙을 검사한다. 호출부(등록부 parser, epic add-*)가 같은 이유 문자열을 쓰도록 한 곳에 둔다.
19
+ * 숫자만이거나 `NNN-`로 시작하는 값은 번호 형식 세그먼트와 구분되지 않아 거절한다.
20
+ *
21
+ * @param {unknown} s - 검사할 값
22
+ * @returns {string | null} null이면 통과, 문자열이면 거절 이유
23
+ */
24
+ function validateSlug(s) {
25
+ if (typeof s !== 'string' || s === '')
26
+ return 'slug must be a non-empty string';
27
+ if (s.length > SLUG_MAX)
28
+ return `slug is longer than ${SLUG_MAX} characters: ${s}`;
29
+ if (/^\d+$/.test(s))
30
+ return `slug must not be digits only: ${s}`;
31
+ if (/^\d{3}-/.test(s))
32
+ return `slug must not start with a three-digit prefix: ${s}`;
33
+ if (!SLUG_RE.test(s))
34
+ return `slug is not lowercase kebab-case: ${s}`;
35
+ return null;
36
+ }
37
+ /** 세그먼트 하나를 두 형식 중 하나로 읽는다. 어느 쪽도 아니면 null. */
38
+ function readSegment(seg) {
39
+ const numeric = NUMERIC_SEG_RE.exec(seg);
40
+ if (numeric)
41
+ return { format: 'numeric', key: numeric[1], slug: numeric[2] };
42
+ if (validateSlug(seg) === null)
43
+ return { format: 'slug', key: seg, slug: seg };
44
+ return null;
45
+ }
46
+ /**
47
+ * 경로의 `epics/<seg>[/blueprints/<seg>]` 구간을 해석한다.
48
+ * epic과 blueprint 세그먼트의 형식이 다르면 한 문서가 두 체계에 걸치므로 null이다.
49
+ *
50
+ * @param {unknown} p - 저장소 상대 경로(백슬래시 허용)
51
+ * @returns {ContextIds | null} 해석 결과. 해석 불가면 null
52
+ */
53
+ function parseContextPath(p) {
54
+ const match = PATH_RE.exec(toPosix(p));
55
+ if (!match)
56
+ return null;
57
+ const epic = readSegment(match[1]);
58
+ if (!epic)
59
+ return null;
60
+ if (match[2] === undefined) {
61
+ return { format: epic.format, epicKey: epic.key, blueprintKey: null, epicSlug: epic.slug, blueprintSlug: null };
62
+ }
63
+ const bp = readSegment(match[2]);
64
+ if (!bp || bp.format !== epic.format)
65
+ return null;
66
+ return {
67
+ format: epic.format, epicKey: epic.key, blueprintKey: bp.key, epicSlug: epic.slug, blueprintSlug: bp.slug,
68
+ };
69
+ }
70
+ /** key가 형식에 맞지 않으면 throw한다. 잘못된 key로 만든 ID가 조용히 저장되는 것을 막는다. */
71
+ function assertKey(format, key, label) {
72
+ const ok = format === 'numeric' ? THREE_DIGITS_RE.test(key) : validateSlug(key) === null;
73
+ if (!ok)
74
+ throw new Error(`${label} does not match ${format} format: ${String(key)}`);
75
+ }
76
+ function assertPair(ids) {
77
+ assertKey(ids.format, ids.epicKey, 'epicKey');
78
+ assertKey(ids.format, ids.blueprintKey, 'blueprintKey');
79
+ }
80
+ /**
81
+ * blueprint 단위 stable ID(intent ID)를 만든다.
82
+ *
83
+ * @param {KeyPair} ids - 형식과 두 key
84
+ * @returns {string} numeric은 `EPIC-ddd/BP-ddd`, slug는 `<epic>/<bp>`
85
+ * @throws {Error} key가 형식에 맞지 않을 때
86
+ */
87
+ function formatStableIntentId(ids) {
88
+ assertPair(ids);
89
+ return ids.format === 'numeric' ? `EPIC-${ids.epicKey}/BP-${ids.blueprintKey}` : `${ids.epicKey}/${ids.blueprintKey}`;
90
+ }
91
+ /**
92
+ * task 단위 stable ID를 만든다.
93
+ *
94
+ * @param {KeyPair & {task: string}} ids - 형식, 두 key, 세 자리 task 번호
95
+ * @returns {string} numeric은 `EPIC-ddd/BP-ddd/TASK-ddd`, slug는 `<epic>/<bp>/ddd`
96
+ * @throws {Error} key가 형식에 맞지 않거나 task가 세 자리가 아닐 때
97
+ */
98
+ function formatStableTaskId(ids) {
99
+ assertPair(ids);
100
+ if (!THREE_DIGITS_RE.test(ids.task))
101
+ throw new Error(`task must be three digits: ${String(ids.task)}`);
102
+ return ids.format === 'numeric'
103
+ ? `EPIC-${ids.epicKey}/BP-${ids.blueprintKey}/TASK-${ids.task}`
104
+ : `${ids.epicKey}/${ids.blueprintKey}/${ids.task}`;
105
+ }
106
+ /**
107
+ * stable ID 문자열을 두 형식 모두에서 해석한다.
108
+ *
109
+ * @param {unknown} v - 문자열이 아니면 null
110
+ * @returns {StableId | null} 해석 결과(task 없는 intent ID는 task null). 해석 불가면 null
111
+ */
112
+ function parseStableId(v) {
113
+ if (typeof v !== 'string')
114
+ return null;
115
+ const old = OLD_STABLE_RE.exec(v);
116
+ if (old)
117
+ return { format: 'numeric', epicKey: old[1], blueprintKey: old[2], task: old[3] ?? null };
118
+ const parts = v.split('/');
119
+ if (parts.length !== 2 && parts.length !== 3)
120
+ return null;
121
+ if (validateSlug(parts[0]) !== null || validateSlug(parts[1]) !== null)
122
+ return null;
123
+ if (parts.length === 3 && !THREE_DIGITS_RE.test(parts[2]))
124
+ return null;
125
+ return { format: 'slug', epicKey: parts[0], blueprintKey: parts[1], task: parts[2] ?? null };
126
+ }
127
+ /**
128
+ * stable ID를 slug 형식의 현재 위치로 바꾼다. 구형 번호 ID는 `legacy_id`·`blueprint_aliases`로 해석하고,
129
+ * legacy epic 항목의 `moved_to`에 blueprint가 있으면 번호·slug 두 형식 모두 이동한 도메인 epic 아래로
130
+ * 옮긴다. 그 밖의 slug 형식 ID는 값을 그대로 돌려주고, 별칭 표에 없는 구형 ID는 null이다.
131
+ *
132
+ * @param {unknown} v - stable ID 후보
133
+ * @param {RegistryLike | null} registry - 등록부(없으면 구형 ID는 해석 불가, slug ID는 그대로)
134
+ * @returns {string | null} slug 형식 ID 또는 null
135
+ */
136
+ function normalizeStableId(v, registry) {
137
+ const parsed = parseStableId(v);
138
+ if (!parsed)
139
+ return null;
140
+ let epicSlug;
141
+ let bpSlug;
142
+ if (parsed.format === 'slug') {
143
+ if (!registry)
144
+ return v;
145
+ epicSlug = parsed.epicKey;
146
+ bpSlug = parsed.blueprintKey;
147
+ }
148
+ else {
149
+ if (!registry)
150
+ return null;
151
+ // v1 등록부는 id가 곧 legacy_id다.
152
+ const epic = registry.epics.find((e) => (e.legacy_id ?? e.id) === parsed.epicKey);
153
+ const alias = epic?.blueprint_aliases?.[parsed.blueprintKey];
154
+ if (!epic || alias === undefined)
155
+ return null;
156
+ epicSlug = epic.slug;
157
+ bpSlug = alias;
158
+ }
159
+ // 상속 키(`constructor` 등)가 이동 대상으로 새지 않도록 own property만 본다.
160
+ const movedTo = registry.epics.find((e) => e.slug === epicSlug)?.moved_to;
161
+ if (movedTo && Object.prototype.hasOwnProperty.call(movedTo, bpSlug))
162
+ epicSlug = movedTo[bpSlug];
163
+ const ids = { format: 'slug', epicKey: epicSlug, blueprintKey: bpSlug };
164
+ if (parsed.format === 'slug' && epicSlug === parsed.epicKey)
165
+ return v;
166
+ return parsed.task === null ? formatStableIntentId(ids) : formatStableTaskId({ ...ids, task: parsed.task });
167
+ }
168
+ /**
169
+ * pointer 키(`<epicKey>/<blueprintKey>`)를 만든다.
170
+ *
171
+ * @param {KeyPair} ids - 형식과 두 key
172
+ * @returns {string} pointer 키
173
+ * @throws {Error} key가 형식에 맞지 않을 때
174
+ */
175
+ function formatPointerKey(ids) {
176
+ assertPair(ids);
177
+ return `${ids.epicKey}/${ids.blueprintKey}`;
178
+ }
179
+ /**
180
+ * pointer 키를 해석한다. 두 key의 형식이 다르면 null이다.
181
+ *
182
+ * @param {unknown} k - pointer 키 후보
183
+ * @returns {KeyPair | null} 해석 결과. 해석 불가면 null
184
+ */
185
+ function parsePointerKey(k) {
186
+ if (typeof k !== 'string')
187
+ return null;
188
+ const parts = k.split('/');
189
+ if (parts.length !== 2)
190
+ return null;
191
+ for (const format of ['numeric', 'slug']) {
192
+ const ok = format === 'numeric'
193
+ ? THREE_DIGITS_RE.test(parts[0]) && THREE_DIGITS_RE.test(parts[1])
194
+ : validateSlug(parts[0]) === null && validateSlug(parts[1]) === null;
195
+ if (ok)
196
+ return { format, epicKey: parts[0], blueprintKey: parts[1] };
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * 번호 epic의 `bouncer.id`(`EPIC-ddd`)를 만든다. key 형식은 검증하지 않는다 — 읽기 쪽 비교용이다.
202
+ *
203
+ * @param {string} epicKey - 번호 epic key
204
+ * @returns {string} `EPIC-<epicKey>`
205
+ */
206
+ function formatLegacyEpicId(epicKey) {
207
+ return `EPIC-${epicKey}`;
208
+ }
209
+ /**
210
+ * 번호 blueprint의 구형 intent ID(`EPIC-ddd/BP-ddd`)를 key 검증 없이 조립한다.
211
+ * 호출부가 slug 규칙 불통과 값(번호 외 임의 문자열 포함)을 그대로 넘기므로 formatStableIntentId처럼 던지지 않는다.
212
+ *
213
+ * @param {string} epicKey - epic key
214
+ * @param {string} blueprintKey - blueprint key
215
+ * @returns {string} `EPIC-<epicKey>/BP-<blueprintKey>`
216
+ */
217
+ function formatLegacyBlueprintId(epicKey, blueprintKey) {
218
+ return `EPIC-${epicKey}/BP-${blueprintKey}`;
219
+ }
220
+ /**
221
+ * 디렉터리 이름 앞의 세 자리 번호(`ddd-`)만 읽는다.
222
+ *
223
+ * @param {string} name - 디렉터리 이름
224
+ * @returns {string | null} 번호 문자열, 접두가 없으면 null
225
+ */
226
+ function parseNumericPrefix(name) {
227
+ const match = NUMERIC_PREFIX_RE.exec(name);
228
+ return match ? match[1] : null;
229
+ }
230
+ /**
231
+ * 번호 epic 디렉터리 이름 `ddd-<rest>`를 번호와 나머지로 나눈다. rest는 slug 규칙을 검사하지 않는다.
232
+ *
233
+ * @param {string} name - 디렉터리 이름
234
+ * @returns {{id: string, slug: string} | null} 나눈 결과, 형식이 아니면 null
235
+ */
236
+ function parseNumericEpicDir(name) {
237
+ const match = NUMERIC_DIR_RE.exec(name);
238
+ return match ? { id: match[1], slug: match[2] } : null;
239
+ }
240
+ /**
241
+ * 텍스트 안의 구형 stable ID(`EPIC-ddd/BP-ddd[/TASK-ddd]`)를 모두 `replacer` 결과로 바꾼다.
242
+ *
243
+ * @param {string} text - 원문
244
+ * @param {(hit: string) => string} replacer - 일치한 구형 ID를 받아 바꿀 문자열을 돌려준다
245
+ * @returns {string} 치환된 텍스트
246
+ */
247
+ function replaceOldStableIds(text, replacer) {
248
+ return text.replace(new RegExp(OLD_STABLE_SCAN_SRC, 'g'), replacer);
249
+ }
250
+ /**
251
+ * 텍스트 안에서 등록부 `moved_to`에 올라 있는 slug 형식 stable ID(`<legacy>/<bp>[/<ddd>]`)를 모두
252
+ * `replacer` 결과로 바꾼다. 번호 형식 ID는 `replaceOldStableIds`가 맡고, 이 함수는 legacy epic이 도메인
253
+ * epic으로 옮겨진 뒤에도 옛 epic 이름을 가리키는 slug 형식만 찾는다. 두 단계를 나눈 이유는 번호 형식이
254
+ * 먼저 `<domain>/<bp>/<ddd>`로 바뀐 뒤에는 epic이 legacy slug가 아니라서 이 함수가 다시 건드리지 않기
255
+ * 때문이다. 같은 입력에 두 번 적용해도 결과가 같다.
256
+ *
257
+ * 경계 검사는 본문 속 부분 문자열 오탐을 막는다. 앞이 영숫자·`-`·`_`·`/`·`.`이면 다른 이름이나 경로의
258
+ * 일부(`data/x` 안의 `a/x`, `epics/a/blueprints/x`)로 보고 건드리지 않는다. 뒤가 영숫자·`-`·`_`로
259
+ * 이어지면(`a/x-y`) 다른 blueprint 이름이므로 건드리지 않는다. fs·git을 읽지 않는 순수 함수다.
260
+ *
261
+ * @param {string} text - 원문
262
+ * @param {RegistryLike | null} registry - 등록부. null이거나 `moved_to`가 하나도 없으면 원문을 그대로 돌려준다
263
+ * @param {(hit: string) => string} replacer - 일치한 slug ID를 받아 바꿀 문자열을 돌려준다
264
+ * @returns {string} 치환된 텍스트
265
+ */
266
+ function replaceMovedSlugStableIds(text, registry, replacer) {
267
+ if (!registry)
268
+ return text;
269
+ const pairs = [];
270
+ for (const epic of registry.epics) {
271
+ // Object.keys는 own enumerable만 돌려주므로 상속 키(`constructor` 등)가 대상 쌍으로 새지 않는다.
272
+ for (const bp of Object.keys(epic.moved_to ?? {}))
273
+ pairs.push(`${epic.slug}/${bp}`);
274
+ }
275
+ if (pairs.length === 0)
276
+ return text;
277
+ // 긴 쌍이 먼저 맞아야 접두가 같은 다른 쌍에 가려지지 않는다.
278
+ const alternation = pairs.sort((a, b) => b.length - a.length)
279
+ .map((pair) => pair.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
280
+ const re = new RegExp(`(?<![A-Za-z0-9\\-_/.])(?:${alternation})(?:\\/\\d{3}(?![A-Za-z0-9_-]))?(?![A-Za-z0-9_-])`, 'g');
281
+ return text.replace(re, replacer);
282
+ }
283
+ module.exports = {
284
+ formatLegacyEpicId, formatLegacyBlueprintId, parseNumericPrefix, parseNumericEpicDir, replaceOldStableIds,
285
+ replaceMovedSlugStableIds,
286
+ SLUG_MAX, validateSlug, parseContextPath, formatStableIntentId, formatStableTaskId,
287
+ parseStableId, normalizeStableId, formatPointerKey, parsePointerKey,
288
+ };