commitgate 0.9.8 โ 0.9.9
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/CHANGELOG.md +14 -0
- package/bin/dispatch.mjs +3 -0
- package/bin/init.ts +15 -2
- package/bin/migrate.ts +29 -11
- package/bin/sync.ts +200 -17
- package/bin/uninstall.ts +8 -2
- package/package.json +1 -1
- package/scripts/req/lib/adapters.ts +99 -7
- package/scripts/req/lib/close-migrate.ts +135 -0
- package/scripts/req/lib/close-proof.ts +323 -0
- package/scripts/req/lib/config.ts +9 -0
- package/scripts/req/lib/evidence.ts +158 -3
- package/scripts/req/lib/intake.ts +175 -0
- package/scripts/req/lib/lockfile-diff.ts +127 -0
- package/scripts/req/lib/reconstruct.ts +118 -0
- package/scripts/req/lib/review-exception.ts +233 -0
- package/scripts/req/lib/review-ledger.ts +257 -0
- package/scripts/req/lib/review-target.ts +61 -0
- package/scripts/req/lib/scratch.ts +14 -1
- package/scripts/req/req-close.ts +222 -0
- package/scripts/req/req-commit.ts +765 -623
- package/scripts/req/req-doctor.ts +58 -1
- package/scripts/req/req-new.ts +304 -255
- package/scripts/req/req-next.ts +829 -813
- package/scripts/req/req-reconstruct.ts +217 -0
- package/scripts/req/req-review-exception.ts +197 -0
- package/scripts/req/review-codex.ts +2526 -2146
- package/workflow/req.config.schema.json +3 -0
- package/workflow/review-persona.md +8 -0
|
@@ -91,6 +91,16 @@ export interface ManifestEntry {
|
|
|
91
91
|
review_base_sha: string
|
|
92
92
|
approved_tree?: string
|
|
93
93
|
design_hash?: string
|
|
94
|
+
/**
|
|
95
|
+
* ๐ด REQ-2026-052 DEC-B5(phase-3a2): **phase ์ ์ฉ** โ ์ด phase๊ฐ ์น์ธ ์์ ์ ๊ฒฐ์๋ committed design ์ฐธ์กฐ
|
|
96
|
+
* (= `captureDesignBinding().designHash`, design ํ `design_hash`์ ๋์ผ ๊ณ์ฐยท๊ฐ). dev-complete ์์ ์ฑ์ด
|
|
97
|
+
* ์ด ๊ฐ == ํ์ฌ committed design_ref์ธ phase ํ๋ง ์ฐ์
ํ๋ค(design-blind๋ฉด D1 ๊ฒํ ๋ถ์ด D2 ์๋ฃ์ ์๋ค).
|
|
98
|
+
*
|
|
99
|
+
* **์ ํ ํ๋**๋ค: ๋ถ์ฌ(๋ ๊ฑฐ์ยท์ด ๋ณด์ ์ด์ ์ปค๋ฐ๋ถ)ํด๋ ๋งค๋ํ์คํธ ๊ฒ์ฆ์ ํต๊ณผํ๋ค(๋ฌดํ๊ท). ๋จ durable ์๋ฃ
|
|
100
|
+
* ํ์ ์์๋ ๋ถ์ฌ๋ฅผ **๋ถ์ฐ์
(fail-closed)** โ ๊ฒ์ฆ์ ๊ด๋ํจ๊ณผ ์๋ฃ์ ์๊ฒฉํจ์ ๋ถ๋ฆฌ(`archive_inventory`์ ๋ํ).
|
|
101
|
+
* design ํ์๋ **๊ธ์ง**(kind ๊ฒฉ๋ฆฌ). ๋ฏธ์ง์ ์ ํค ์์ฒด๋ฅผ ๋ฃ์ง ์์ ๊ธฐ์กด phase ํ๊ณผ ๋ฐ์ดํธ ๋์ผ.
|
|
102
|
+
*/
|
|
103
|
+
phase_design_ref?: string
|
|
94
104
|
approved_at: string
|
|
95
105
|
consumed_at: string
|
|
96
106
|
consumed_by_commit_sha: string
|
|
@@ -118,6 +128,7 @@ const MANIFEST_KEYS = new Set([
|
|
|
118
128
|
'review_base_sha',
|
|
119
129
|
'approved_tree',
|
|
120
130
|
'design_hash',
|
|
131
|
+
'phase_design_ref', // REQ-2026-052 DEC-B5(์ ํ โ phase ์ ์ฉยท๋ถ์ฌํด๋ ์ ํจ)
|
|
121
132
|
'approved_at',
|
|
122
133
|
'consumed_at',
|
|
123
134
|
'consumed_by_commit_sha',
|
|
@@ -165,12 +176,20 @@ export function designEvidenceStagePaths(
|
|
|
165
176
|
inventory: readonly ArchiveInventoryItem[],
|
|
166
177
|
responsePath: string,
|
|
167
178
|
ticketRel: string,
|
|
179
|
+
/**
|
|
180
|
+
* ๋ฆฌ๋ทฐ ์์ฅ์ด ๋์คํฌ์ **์กด์ฌํ๋๊ฐ**(REQ-2026-051 D7). ์กด์ฌํ ๋๋ง pathspec์ ํฉ๋ฅ์ํจ๋ค โ
|
|
181
|
+
* ์๋ ๊ฒฝ๋ก๋ฅผ ๋ฃ์ผ๋ฉด `commitPaths`๊ฐ ์คํจํด ์น์ธ ์ฆ๊ฑฐ ์ปค๋ฐ **์ ์ฒด**๊ฐ ๋ฌด์ฐ๋๋ค(์์ฅ ๋๋ฌธ์ ์น์ธ
|
|
182
|
+
* ์ฆ๊ฑฐ๋ฅผ ์๋ ๊ฒ์ ๋ณธ๋ง์ ๋๋ค).
|
|
183
|
+
* ์๋ต ๊ฐ๋ฅ: ๊ธฐ์กด 3-arg ํธ์ถ๋ถ๋ฅผ ๊นจ์ง ์๋๋ค.
|
|
184
|
+
*/
|
|
185
|
+
ledgerExists = false,
|
|
168
186
|
): string[] {
|
|
169
187
|
const dir = `${ticketRel.replace(/\\/g, '/').replace(/\/+$/, '')}/responses`
|
|
170
188
|
const archives = [...inventory.map((i) => i.response_path), responsePath].filter(
|
|
171
189
|
(p) => typeof p === 'string' && p.length > 0 && isConfinedArchivePath(p, ticketRel),
|
|
172
190
|
)
|
|
173
|
-
|
|
191
|
+
const tail = ledgerExists ? [`${dir}/approvals.jsonl`, `${dir}/review-ledger.jsonl`] : [`${dir}/approvals.jsonl`]
|
|
192
|
+
return [...new Set([...archives, ...tail])]
|
|
174
193
|
}
|
|
175
194
|
|
|
176
195
|
/** ์ธ๋ฒคํ ๋ฆฌ ํญ๋ชฉ์ ํ์ ๋ฌธ์ ๋ชฉ๋ก(์์). ๋น ๋ฐฐ์ด = ์ ์. `line N: ` ์ ๋๋ ํธ์ถ๋ถ๊ฐ ๋ถ์ธ๋ค. */
|
|
@@ -248,7 +267,11 @@ export function buildManifestEntry(
|
|
|
248
267
|
const approvedTree = ev.approved_tree
|
|
249
268
|
if (typeof approvedTree !== 'string' || !approvedTree)
|
|
250
269
|
throw new Error('buildManifestEntry: phase evidence์ approved_tree ๋๋ฝ(fail-fast)')
|
|
251
|
-
|
|
270
|
+
// REQ-2026-052 DEC-B5: phase_design_ref(์น์ธ ์์ design ๊ฒฐ์)๊ฐ ์์ผ๋ฉด phase ํ์ ํฌํจ. ๋ฏธ์ง์ ์ด๋ฉด
|
|
271
|
+
// ํค ์์ฒด๋ฅผ ๋ฃ์ง ์๋๋ค โ ๋ ๊ฑฐ์ phase ํ๊ณผ ๋ฐ์ดํธ ๋์ผ(๋ฌดํ๊ท). durable ํฐ์ผ์ designValid ๊ฒ์ดํธ๊ฐ
|
|
272
|
+
// ์น์ธ ์ ์ ๋ผ ์ ์ ๊ฒฝ๋ก์์ ํญ์ ์ฑ์์ง๊ณ , ์๋ฃ ํ์ ์ด ์ด ๊ฐ์ผ๋ก design-bound ํํฐํ๋ค.
|
|
273
|
+
const pdr = typeof ev.phase_design_ref === 'string' && ev.phase_design_ref ? { phase_design_ref: ev.phase_design_ref } : {}
|
|
274
|
+
return { ...base, approved_tree: approvedTree, ...pdr, ...inv }
|
|
252
275
|
}
|
|
253
276
|
|
|
254
277
|
/** ๋งค๋ํ์คํธ ํ ์ค ์ง๋ ฌํ(JSONL): JSON + ๋ ๊ฐํ. ๊ณ ์ ํค ์์๋ผ deterministic. */
|
|
@@ -305,10 +328,15 @@ export function validateManifest(content: string, opts: { ticketRel: string; val
|
|
|
305
328
|
problems.push(`line ${ln}: phase_id ๋น์ ํจ: ${String(e.phase_id)}`)
|
|
306
329
|
if (typeof e.approved_tree !== 'string' || !GIT_OID_RE.test(e.approved_tree)) problems.push(`line ${ln}: approved_tree ๋น-OID`)
|
|
307
330
|
if ('design_hash' in e) problems.push(`line ${ln}: phase entry์ design_hash ๊ธ์ง`)
|
|
331
|
+
// REQ-2026-052 DEC-B5: phase_design_ref๋ **์ ํ**(๋ถ์ฌ=๋ ๊ฑฐ์ ๋ฌดํ๊ท). ์์ผ๋ฉด 64hex(design_hash์ ๋ํ).
|
|
332
|
+
if ('phase_design_ref' in e && (typeof e.phase_design_ref !== 'string' || !SHA256_RE.test(e.phase_design_ref)))
|
|
333
|
+
problems.push(`line ${ln}: phase_design_ref ๋น-64hex`)
|
|
308
334
|
} else if (kind === 'design') {
|
|
309
335
|
if (e.phase_id !== null) problems.push(`line ${ln}: design entry๋ phase_id=null์ด์ด์ผ`)
|
|
310
336
|
if (typeof e.design_hash !== 'string' || !SHA256_RE.test(e.design_hash)) problems.push(`line ${ln}: design_hash ๋น-64hex`)
|
|
311
337
|
if ('approved_tree' in e) problems.push(`line ${ln}: design entry์ approved_tree ๊ธ์ง`)
|
|
338
|
+
// REQ-2026-052 DEC-B5: kind ๊ฒฉ๋ฆฌ โ design ํ์๋ phase_design_ref ๊ธ์ง.
|
|
339
|
+
if ('phase_design_ref' in e) problems.push(`line ${ln}: design entry์ phase_design_ref ๊ธ์ง`)
|
|
312
340
|
}
|
|
313
341
|
// archive_inventory(REQ-2026-048 DEC-2): **์ ํ** โ ๋ถ์ฌ๋ ์ ์(๊ธฐ์กด ํ ๋ฌดํ๊ท). ์์ผ๋ฉด ํ์ ๊ฒ์ฆ.
|
|
314
342
|
if ('archive_inventory' in e) {
|
|
@@ -371,6 +399,89 @@ export function findEvidenceRow(
|
|
|
371
399
|
return null
|
|
372
400
|
}
|
|
373
401
|
|
|
402
|
+
// โโโโโโโโโโโโโโโโโโโ ๋งค๋ํ์คํธ ์์ ํ์ (REQ-2026-052 phase-3b: req-commit์์ ์ด๋) โโ
|
|
403
|
+
// req-commit(command)๊ณผ intake ์ค์บ(leaf)์ด ๊ฐ์ ํ์๋ฅผ ๊ณต์ ํ๋๋ก ๋งค๋ํ์คํธ ๋ชจ๋ธ์ ์ ๋ณธ์ธ ์ฌ๊ธฐ๋ก ์ฎ๊ฒผ๋ค.
|
|
404
|
+
// req-commit์ด ๊ธฐ์กด ๊ฒฝ๋ก๋ก re-exportํ๋ค(ํธ์ถ๋ถ ํธํ).
|
|
405
|
+
|
|
406
|
+
/** ๋งค๋ํ์คํธ(JSONL) ๋ณธ๋ฌธ์์ ์์ ํ๊ฒ ์ํธ๋ฆฌ ๋ฐฐ์ด์ ๋ฝ๋๋ค(ํ์ฑ ๋ถ๊ฐ ํ์ ๊ฑด๋๋ด๋ค โ ๊ฒ์ฆ์ ๋ณ๋). */
|
|
407
|
+
export function parseManifestEntries(content: string): Array<Record<string, unknown>> {
|
|
408
|
+
const out: Array<Record<string, unknown>> = []
|
|
409
|
+
for (const line of content.split('\n')) {
|
|
410
|
+
if (line.trim() === '') continue
|
|
411
|
+
try {
|
|
412
|
+
const o = JSON.parse(line)
|
|
413
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) out.push(o as Record<string, unknown>)
|
|
414
|
+
} catch {
|
|
415
|
+
/* skip */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* ๋งค๋ํ์คํธ์์ ์ปค๋ฐ๋ **phase ์ฆ๊ฑฐ**๊ฐ ์๋ phase id ์งํฉ(consumed phase ์ํธ๋ฆฌ).
|
|
423
|
+
*
|
|
424
|
+
* ๐ด REQ-2026-052 DEC-B5(phase-3a2): `designRef`๋ฅผ ์ฃผ๋ฉด **design-bound** โ ๊ทธ phase ํ์ `phase_design_ref`๊ฐ
|
|
425
|
+
* `designRef`์ **์ผ์น**ํ๋ ๊ฒ๋ง ์ฐ์
ํ๋ค. dev-complete ์์ ์ฑ์ด ์ด ํํฐ๋ฅผ ์จ์ผ D1์์ ๊ฒํ ๋ phase๊ฐ D2
|
|
426
|
+
* ์ฌ์น์ธ ํ D2 ์๋ฃ ์ฆ๋ช
์ ์์ด๋ค์ง ์๋๋ค. `phase_design_ref` ๋ถ์ฌ ํ(๋ ๊ฑฐ์ยท๋ณด์ ์ด์ )์ **๋ถ์ฐ์
**(fail-closed).
|
|
427
|
+
* `designRef` ๋ฏธ์ง์ (ํ์ํธํ)์ด๋ฉด ๊ฒฐ์ ๋ฌด๊ด ์ ๋(์ ๋์) โ ์ ๊ท ์๋ฃ ๊ฒฝ๋ก๋ ํญ์ designRef๋ฅผ ์ค๋ค.
|
|
428
|
+
*/
|
|
429
|
+
export function evidencedPhaseIdsFromManifest(content: string, designRef?: string | null): string[] {
|
|
430
|
+
return parseManifestEntries(content)
|
|
431
|
+
.filter((e) => e.kind === 'phase' && typeof e.phase_id === 'string')
|
|
432
|
+
.filter((e) => (designRef == null ? true : e.phase_design_ref === designRef))
|
|
433
|
+
.map((e) => e.phase_id as string)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** ๋งค๋ํ์คํธ์์ ์ปค๋ฐ๋ design ์น์ธ์ design_hash(๊ฐ์ฅ ๋ง์ง๋ง design ์ํธ๋ฆฌ). ์์ผ๋ฉด null. */
|
|
437
|
+
export function designHashFromManifest(content: string): string | null {
|
|
438
|
+
const designs = parseManifestEntries(content).filter((e) => e.kind === 'design' && typeof e.design_hash === 'string')
|
|
439
|
+
return designs.length ? (designs[designs.length - 1]!.design_hash as string) : null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// โโโโโโโโโ phase ์น์ธ archive ๋ฌด๊ฒฐ์ฑ (REQ-2026-052 DEC-B6ยทphase-3b2) โ ๊ณต์ leaf ๋ชจ๋ โโ
|
|
443
|
+
|
|
444
|
+
export interface PhaseArchiveProblem {
|
|
445
|
+
phaseId: string
|
|
446
|
+
responsePath: string
|
|
447
|
+
reason: 'missing' | 'sha-mismatch'
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* ๐ด phase ์น์ธ **archive blob ๋ฌด๊ฒฐ์ฑ** ๊ฒ์ฆ(์์ + `headBlobSha256` ํฌํธ, DEC-B6). intake(`scanTicketIntake`)์
|
|
452
|
+
* req:commit ๋ฐํ ํ verifier(`verifyDevCompleteAtHead`)๊ฐ **์ด ํ ๋ชจ๋์ ๊ณต์ **ํ๋ค โ ๋ ๊ฒฝ๋ก์ phase archive
|
|
453
|
+
* ๊ท์น์ด ๊ฐ๋ผ์ง ์ ์๋ค(์๊ตฌ #3).
|
|
454
|
+
*
|
|
455
|
+
* ๊ฐ phase manifest ํ์ ๋ํด: `response_path` blob์ด **HEAD์ ์กด์ฌ**ํ๊ณ (`headBlobSha256` non-null) ๊ทธ
|
|
456
|
+
* sha256์ด `response_sha256`๊ณผ **์ผ์น**ํ๋์ง ํ์ธํ๋ค. archive๋ฅผ ์ญ์ ํ๋ฉด `missing`, ๋ฐ์ดํธ๋ฅผ ๋ฐ๊พธ๋ฉด
|
|
457
|
+
* `sha-mismatch` โ ๋ ๋ค archive ์์(corrupt)์ด๋ค. manifest ํ ํ์(`response_path`/`response_sha256`)์ ์ฌ๊ธฐ์
|
|
458
|
+
* ์ฌ๊ฒ์ฆํ์ง ์๋๋ค(๊ทธ๊ฑด `validateManifest` ๋ชซ) โ ํ์์ด ์ด๋ฏธ ํต๊ณผํ ํ์ **blob ์ ํฉ**๋ง ๋ณธ๋ค.
|
|
459
|
+
*
|
|
460
|
+
* ๐ด **๊ฐํ ์ ์ฑ
(์๊ตฌ #1 ์ฐ์ ์)**: `onlyDesignRef`๋ฅผ ์ฃผ์ง ์์ผ๋ฉด **๋ชจ๋ ** phase ํ์ ๊ฒ์ฆํ๋ค(inventory ํ์ ์๋).
|
|
461
|
+
* ๊ฐ์ฌ ๋ด๊ตฌ์ฑ์ ์ฌ์น์ธ ์ด์ ๋ผ์ด๋ ํ์ archive๊น์ง ์จ์ ํด์ผ ํ๋ค. `onlyDesignRef` ์ง์ ์ ๊ทธ design_ref์
|
|
462
|
+
* ๊ฒฐ์๋ phase ํ๋ง(๋ถ๋ถ ๊ฒ์ฆ โ ํ์ ์).
|
|
463
|
+
*
|
|
464
|
+
* ๐ด on-diskยท์ํนํธ๋ฆฌ๋ฅผ ์ ๋ ์ฝ์ง ์๋๋ค โ `headBlobSha256`(HEAD blob ๋ฐ์ดํธ sha)๋ง. leaf๋ผ git์ ์ง์ ๋ชจ๋ฅธ๋ค.
|
|
465
|
+
*/
|
|
466
|
+
export function verifyPhaseArchives(
|
|
467
|
+
content: string,
|
|
468
|
+
headBlobSha256: (repoRel: string) => string | null,
|
|
469
|
+
onlyDesignRef?: string | null,
|
|
470
|
+
): PhaseArchiveProblem[] {
|
|
471
|
+
const problems: PhaseArchiveProblem[] = []
|
|
472
|
+
for (const e of parseManifestEntries(content)) {
|
|
473
|
+
if (e.kind !== 'phase' || typeof e.phase_id !== 'string' || e.phase_id === '') continue
|
|
474
|
+
if (onlyDesignRef != null && e.phase_design_ref !== onlyDesignRef) continue
|
|
475
|
+
const responsePath = typeof e.response_path === 'string' ? e.response_path : ''
|
|
476
|
+
const expectedSha = typeof e.response_sha256 === 'string' ? e.response_sha256 : ''
|
|
477
|
+
if (!responsePath || !expectedSha) continue // ํ์ ๋ฌธ์ ๋ validateManifest๊ฐ corrupt๋ก ์ก๋๋ค(์ค๋ณต ํ์ ์ ํจ).
|
|
478
|
+
const actual = headBlobSha256(responsePath)
|
|
479
|
+
if (actual === null) problems.push({ phaseId: e.phase_id, responsePath, reason: 'missing' })
|
|
480
|
+
else if (actual !== expectedSha) problems.push({ phaseId: e.phase_id, responsePath, reason: 'sha-mismatch' })
|
|
481
|
+
}
|
|
482
|
+
return problems
|
|
483
|
+
}
|
|
484
|
+
|
|
374
485
|
// โโโโโโโโโโโโโโโโโโโโโโโโโโโ design evidence ๋ด๊ตฌํ (REQ-2026-048 DEC-3) โโ
|
|
375
486
|
|
|
376
487
|
/**
|
|
@@ -483,7 +594,11 @@ export function durableDesignEvidence(args: {
|
|
|
483
594
|
ports.writeText(manifestRel, candidate)
|
|
484
595
|
}
|
|
485
596
|
|
|
486
|
-
|
|
597
|
+
// REQ-2026-051 D7: ์์ฅ์ด ์์ผ๋ฉด ๊ฐ์ ์ปค๋ฐ์ ์ฃ๋๋ค. `readText`๋ก ์กด์ฌ๋ฅผ ํ์ธํ๋ค โ ํฌํธ ๊ฒฝ๊ณ๋ฅผ
|
|
598
|
+
// ๋์ง ์๊ณ (fs ์ง์ ์ ๊ทผ ๊ธ์ง), ์์ผ๋ฉด pathspec์ ๋ฃ์ง ์์ ์ปค๋ฐ์ด ์คํจํ์ง ์๋๋ค.
|
|
599
|
+
const ledgerRel = `${ticketRel.replace(/\\/g, '/').replace(/\/+$/, '')}/responses/review-ledger.jsonl`
|
|
600
|
+
const ledgerExists = ports.readText(ledgerRel) !== null
|
|
601
|
+
const stagePaths = designEvidenceStagePaths(inventory, ev.response_path, ticketRel, ledgerExists)
|
|
487
602
|
// ๐ด ๊ฐ๋๋ **์ฐ๋ฆฌ๊ฐ ์ปค๋ฐํ ๊ฒฝ๋ก**์๋ง ๊ฑด๋ค โ index ์ ์ฒด๊ฐ ์๋๋ค(phase-3 ๋ฆฌ๋ทฐ P1).
|
|
488
603
|
// index ์ ์ฒด๋ฅผ ๋ณด๋ฉด ์ค๊ณ ๋ฌธ์๋ฅผ stageํ ์ ์ ์น์ธ ๊ฒฝ๋ก์์ ํญ์ ์คํจํ๋ค. pathspec ๋ฒ์ ์ปค๋ฐ์ด๋ผ
|
|
489
604
|
// ๋ฌด๊ดํ staged ๋ณ๊ฒฝ์ ์ ์ด์ ์ด ์ปค๋ฐ์ ๋ค์ด๊ฐ ์ ์๊ณ , ์ปค๋ฐ ํ์๋ index์ ๊ทธ๋๋ก ๋จ๋๋ค.
|
|
@@ -628,6 +743,46 @@ export function verifyCommittedDesignEvidence(args: {
|
|
|
628
743
|
return { durable: true, reason: 'design ์น์ธ ์ฆ๊ฑฐ๊ฐ HEAD์ ์๋น๋จ' }
|
|
629
744
|
}
|
|
630
745
|
|
|
746
|
+
// โโโโโโโโโ committed ์ฆ๊ฑฐ ๋ฌด๊ฒฐ์ฑ ์ข
ํฉ(design+phase) (REQ-2026-052 DEC-B7ยทphase-3b3) โ ๊ณต์ deep ๋ชจ๋ โโ
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* ๐ด durable ํฐ์ผ์ **committed ์ฆ๊ฑฐ ๋ฌด๊ฒฐ์ฑ ์ข
ํฉ ํ์ **(design + phase, DEC-B7). intake(`scanTicketIntake`)์
|
|
750
|
+
* req:commit ๋ฐํ ํ verifier(`verifyDevCompleteAtHead`)๊ฐ **์ด ํ ํจ์๋ฅผ ๊ณต์ **ํ๋ค(์๊ตฌ #3) โ ๋ ๊ฒฝ๋ก์
|
|
751
|
+
* ์ฆ๊ฑฐ ๋ฌด๊ฒฐ์ฑ ๊ท์น์ด ๊ฐ๋ผ์ง ์ ์๋ค. ํธ์ถ์๋ designยทphase ๊ฒ์ฆ ์์ยท์ธ๋ถ ์กฐ๊ฑด์ ๋ชฐ๋ผ๋ ๋๋ค(์์ ์ธํฐํ์ด์ค ๊ธ์ง).
|
|
752
|
+
*
|
|
753
|
+
* ๋ด๋ถ๋ **๊ธฐ์กด ํจ์ ์ฌ์ฌ์ฉ**(์ค๋ณต ๊ท์น ์์):
|
|
754
|
+
* - **design**: manifest์ design ํ์ด ์์ผ๋ฉด **`verifyCommittedDesignEvidence`**(REQ-2026-048 DONE ๊ฒ์ดํธ)๋ก
|
|
755
|
+
* ์ต์ design ์น์ธ archive ์กด์ฌยทSHA + `archive_inventory` ์ ์ฒด(๊ณผ๊ฑฐ needs-fix ํฌํจ) ์กด์ฌยทSHA + HEAD ์งํฉ ์ ํฉ์
|
|
756
|
+
* ๊ฒ์ฆ. `durable=false`๋ฉด ๋ฌด๊ฒฐ์ฑ/์์ ์ฑ ์คํจ.
|
|
757
|
+
* - **phase**: **`verifyPhaseArchives`**(DEC-B6)๋ก ๋ชจ๋ phase ํ archive ์กด์ฌยทSHA.
|
|
758
|
+
*
|
|
759
|
+
* ๐ด **๋ถ์์ (incomplete) โ ์์(tampered)**: manifest์ **design ํ์ด ์์ผ๋ฉด** design ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฌ ๋์์ด ์๋๋ค
|
|
760
|
+
* (๋์ฒด๋ ๋ฏธ์ ํฐ์ผยทdesign ๋ฏธ์น์ธ์ ์์์ด ์๋๋ผ ๋ฏธ์ โ ํต๊ณผ ๊ฐ๋ฅ, series-terminal). `designEvidenceComplete`์
|
|
761
|
+
* needs-recovery ํ์ ์
๋ ฅ(design ํ ์์ผ๋ฉด false = ๋ฏธ์).
|
|
762
|
+
*
|
|
763
|
+
* ๐ด **HEAD blob๋ง** โ on-diskยท์ํนํธ๋ฆฌยทstate.json์ ํ์ ๊ทผ๊ฑฐ๋ก ์ฐ์ง ์๋๋ค(ํฌํธ ๊ฒฝ์ ).
|
|
764
|
+
*/
|
|
765
|
+
export function verifyCommittedEvidenceIntegrity(args: {
|
|
766
|
+
ticketRel: string
|
|
767
|
+
manifestText: string | null
|
|
768
|
+
ports: Pick<EvidencePorts, 'headText' | 'headBlobSha256' | 'headArchivePaths'>
|
|
769
|
+
}): { problems: string[]; designEvidenceComplete: boolean } {
|
|
770
|
+
const { ticketRel, manifestText, ports } = args
|
|
771
|
+
const problems: string[] = []
|
|
772
|
+
let designEvidenceComplete = false
|
|
773
|
+
if (manifestText === null) return { problems, designEvidenceComplete }
|
|
774
|
+
// design: design ํ์ด ์์ ๋๋ง ๊ฒ์ฌ(์์ผ๋ฉด ์์ ์๋ โ ๋ถ์์ โ ์์).
|
|
775
|
+
if (designHashFromManifest(manifestText) !== null) {
|
|
776
|
+
const d = verifyCommittedDesignEvidence({ ticketRel, ports })
|
|
777
|
+
designEvidenceComplete = d.durable
|
|
778
|
+
if (!d.durable) problems.push(`design ์ฆ๊ฑฐ ๋ฌด๊ฒฐ์ฑ/์์ ์ฑ ์คํจ: ${d.reason}`)
|
|
779
|
+
}
|
|
780
|
+
// phase: ๋ชจ๋ phase ํ archive ๋ฌด๊ฒฐ์ฑ.
|
|
781
|
+
for (const p of verifyPhaseArchives(manifestText, ports.headBlobSha256))
|
|
782
|
+
problems.push(`phase archive ${p.reason}: ${p.phaseId} (${p.responsePath})`)
|
|
783
|
+
return { problems, designEvidenceComplete }
|
|
784
|
+
}
|
|
785
|
+
|
|
631
786
|
/**
|
|
632
787
|
* approvals.jsonl์ **์ด evidence๊ฐ** ์ด๋ฏธ finalize๋ ์ํธ๋ฆฌ๊ฐ ์๋์ง(์์, ๋ฉฑ๋ฑ finalize์ฉ).
|
|
633
788
|
* โ ๏ธ B3-R2: consumed_by_commit_sha๋ง์ผ๋ก๋ ๋ถ์กฑ โ ๊ฐ์ source SHA๋ฅผ ์ฐ๋ design-finalize row ๋ฑ์ ์ค์ธ๋ ์ ์์.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* req:new intake gate (REQ-2026-052 phase-3b, DEC-C) โ ์ REQ ์์ฑ ์ , **HEAD-committed durable ์ฆ๊ฑฐ๋ง์ผ๋ก**
|
|
3
|
+
* ๊ฐ ๊ธฐ์กด ํฐ์ผ์ ๊ธฐ๋ณธ ์ํ๋ฅผ ํ์ํด ๋ฏธ์ข
๊ฒฐ(developing/needs-recovery) durable ํฐ์ผ์ด ์์ผ๋ฉด fail-closed๋ก ๋ง๋๋ค.
|
|
4
|
+
*
|
|
5
|
+
* ๐ด ํ์ ์
๋ ฅ์ **์ค์ง HEAD blob**: scaffold marker(`state.json`)ยท`approvals.jsonl`ยท`ticket-close.jsonl`ยท
|
|
6
|
+
* `review-ledger.jsonl`. ์ํน `state.json`ยท์ํนํธ๋ฆฌยท๋ฏธ์ปค๋ฐ ์์ฅ์ ์ ๋ ๋ณด์ง ์๋๋ค(DEC-B4). ์ค์บ์ git
|
|
7
|
+
* **์กฐํ๋ง**(read-only) โ write-treeยทcommitยทstate ์์ ์์.
|
|
8
|
+
* ๐ด **corrupt/๋ถ๋ถ HEAD ์ฆ๊ฑฐ**(๋งค๋ํ์คํธยทclose-proof ์์)๋ pass ์กฐ๊ฑด(dev-complete/series-terminal)์ด ์ฝ๋
|
|
9
|
+
* ์ํฐํฉํธ์ด๋ฏ๋ก, ์์ ์ ํต๊ณผ์ํค์ง ์๊ณ **block**ํ๋ค(fail-closed). ์์๋ ์ ํธ๋ก ์๋ฃ๋ฅผ ์์ฅํ ์ ์๋ค.
|
|
10
|
+
* ๐ด ์ค๋ฒ๋ ์ด(`integrated`ยท`reconstructed`)๋ ๊ธฐ๋ณธ ์ํ ํ์ ์ ๋ฐ๊พธ์ง ์๋๋ค โ ๊ฒ์ดํธ๋ `baseStateBlocksIntake`
|
|
11
|
+
* (๊ธฐ๋ณธ ์ํ๋ง) ์ผ๋ก ๊ฒฐ์ ํ๋ค. legacy(durability marker ์์)๋ ์ฐจ๋จํ์ง ์๊ณ ํ์๋ง ํ๋ค.
|
|
12
|
+
*
|
|
13
|
+
* ์์ ํ์ (`classifyIntake`)๊ณผ IO ์์ง(`scanTicketIntake`ยท`scanIntake`)์ ๋ถ๋ฆฌํ๋ค.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
deriveBaseState,
|
|
17
|
+
baseStateBlocksIntake,
|
|
18
|
+
isReconstructed,
|
|
19
|
+
parseCloseProof,
|
|
20
|
+
type CloseBaseState,
|
|
21
|
+
type CloseProofRow,
|
|
22
|
+
} from './close-proof'
|
|
23
|
+
import {
|
|
24
|
+
isDurabilityRequired,
|
|
25
|
+
verifyCommittedEvidenceIntegrity,
|
|
26
|
+
validateManifest,
|
|
27
|
+
evidencedPhaseIdsFromManifest,
|
|
28
|
+
designHashFromManifest,
|
|
29
|
+
} from './evidence'
|
|
30
|
+
import { parseLedger } from './review-ledger'
|
|
31
|
+
import { createEvidencePorts } from './evidence-ports'
|
|
32
|
+
|
|
33
|
+
export type IntakeVerdict = 'pass' | 'block' | 'legacy'
|
|
34
|
+
|
|
35
|
+
export interface IntakeTicketResult {
|
|
36
|
+
ticketId: string
|
|
37
|
+
ticketRel: string
|
|
38
|
+
/** ํ์ ๊ธฐ๋ณธ ์ํ. ๋งค๋ํ์คํธ/close-proof ์์์ 5-์ํ ๋ฐ์ `corrupt`(ํญ์ block). */
|
|
39
|
+
baseState: CloseBaseState | 'corrupt'
|
|
40
|
+
verdict: IntakeVerdict
|
|
41
|
+
reason: string
|
|
42
|
+
/** `reconstructed` ์ค๋ฒ๋ ์ด(ํ์์ฉ โ ๊ฒ์ดํธ ํ์ ์ ์ํฅ ์์). */
|
|
43
|
+
reconstructed: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IntakeFacts {
|
|
47
|
+
ticketId: string
|
|
48
|
+
ticketRel: string
|
|
49
|
+
/** HEAD scaffold marker(`isDurabilityRequired`). false๋ฉด legacy. */
|
|
50
|
+
durabilityRequired: boolean
|
|
51
|
+
/** HEAD approvals.jsonl ๋ณธ๋ฌธ(์์ผ๋ฉด null). */
|
|
52
|
+
manifestText: string | null
|
|
53
|
+
/** `validateManifest` ๊ฒฐ๊ณผ(๋น ๋ฐฐ์ด=์ ์). ๋น์ด์์ง ์์ผ๋ฉด corrupt block. */
|
|
54
|
+
manifestProblems: string[]
|
|
55
|
+
/** HEAD ticket-close.jsonl ํ์ฑ ๊ฒฐ๊ณผ. problems ๋น์ด์์ง ์์ผ๋ฉด corrupt block. */
|
|
56
|
+
closeParsed: { rows: CloseProofRow[]; problems: string[] }
|
|
57
|
+
/** ๐ด committed ์ฆ๊ฑฐ(design+phase) ๋ฌด๊ฒฐ์ฑ ๋ฌธ์ (DEC-B6ยทB7). ๋น์ด์์ง ์์ผ๋ฉด corrupt block(์ญ์ /๋ณ์กฐ). */
|
|
58
|
+
evidenceIntegrityProblems: string[]
|
|
59
|
+
ledgerHasApprovedClose: boolean
|
|
60
|
+
committedEvidenceComplete: boolean
|
|
61
|
+
committedDesignRef: string | null
|
|
62
|
+
/** **design-bound**(ํ์ฌ committed design_ref์ ๊ฒฐ์๋) phase evidence์ phase id. */
|
|
63
|
+
evidencedPhaseIds: string[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* HEAD ์ฌ์ค โ intake ํ์ (์์). corrupt(๋งค๋ํ์คํธ/close-proof ์์)๋ ๊ธฐ๋ณธ ์ํ ๋ฐ์ ๋ณ๋ block ์ฌ์ ๋ค.
|
|
68
|
+
*/
|
|
69
|
+
export function classifyIntake(facts: IntakeFacts): IntakeTicketResult {
|
|
70
|
+
const head = { ticketId: facts.ticketId, ticketRel: facts.ticketRel }
|
|
71
|
+
// legacy(durability marker ์์)๋ ์ฐจ๋จํ์ง ์๊ณ ํ์๋ง(DEC-Cยท์๊ตฌ).
|
|
72
|
+
if (!facts.durabilityRequired)
|
|
73
|
+
return { ...head, baseState: 'legacy', verdict: 'legacy', reason: 'legacy ํฐ์ผ(durability marker ์์) โ ํ์๋ง, ์ฐจ๋จ ์ ํจ', reconstructed: false }
|
|
74
|
+
// ๐ด pass ์กฐ๊ฑด(dev-complete/series-terminal)์ด ์ฝ๋ ์ํฐํฉํธ๊ฐ ์์๋์ผ๋ฉด ํต๊ณผ ๊ธ์ง(fail-closed).
|
|
75
|
+
if (facts.manifestText !== null && facts.manifestProblems.length)
|
|
76
|
+
return { ...head, baseState: 'corrupt', verdict: 'block', reason: `HEAD approvals.jsonl ์์ โ ํต๊ณผ ๋ถ๊ฐ(fail-closed): ${facts.manifestProblems.slice(0, 3).join('; ')}`, reconstructed: false }
|
|
77
|
+
if (facts.closeParsed.problems.length)
|
|
78
|
+
return { ...head, baseState: 'corrupt', verdict: 'block', reason: `HEAD ticket-close.jsonl ์์ โ ํต๊ณผ ๋ถ๊ฐ(fail-closed): ${facts.closeParsed.problems.slice(0, 3).join('; ')}`, reconstructed: false }
|
|
79
|
+
// ๐ด DEC-B6ยทB7: committed ์ฆ๊ฑฐ(designยทphase archive) blob ๋ถ์ฌ/๋ณ์กฐ๋ ํต๊ณผ ์กฐ๊ฑด์ด ์ฝ๋ ์ฆ๊ฑฐ์ ์์ โ
|
|
80
|
+
// corrupt block(dev-completeยทseries-terminal ์์ฅ ์ฐจ๋จ). design ํ์ด ์๋ ๋ฏธ์ ํฐ์ผ์ ์์ ๋์ ์๋(๋ถ์์ โ ์์).
|
|
81
|
+
if (facts.evidenceIntegrityProblems.length)
|
|
82
|
+
return { ...head, baseState: 'corrupt', verdict: 'block', reason: `committed ์ฆ๊ฑฐ ์์ โ ํต๊ณผ ๋ถ๊ฐ(fail-closed): ${facts.evidenceIntegrityProblems.slice(0, 3).join('; ')}`, reconstructed: false }
|
|
83
|
+
const state = deriveBaseState({
|
|
84
|
+
durabilityRequired: true,
|
|
85
|
+
closeProofRows: facts.closeParsed.rows,
|
|
86
|
+
ledgerHasApprovedClose: facts.ledgerHasApprovedClose,
|
|
87
|
+
committedEvidenceComplete: facts.committedEvidenceComplete,
|
|
88
|
+
evidencedPhaseIds: facts.evidencedPhaseIds,
|
|
89
|
+
committedDesignRef: facts.committedDesignRef,
|
|
90
|
+
})
|
|
91
|
+
const blocked = baseStateBlocksIntake(state) // ๊ธฐ๋ณธ ์ํ๋ง ๋ณธ๋ค โ ์ค๋ฒ๋ ์ด ๋ฌด๊ด(์๊ตฌ).
|
|
92
|
+
const reason = blocked
|
|
93
|
+
? state === 'developing'
|
|
94
|
+
? '๋ฏธ์ข
๊ฒฐ durable ํฐ์ผ(developing) โ ๋ชจ๋ phase ์๋ฃยท์ปค๋ฐ ๋๋ ์ข
๊ฒฐ ํ ์ฌ์๋'
|
|
95
|
+
: 'HEAD ์ฆ๊ฑฐ ๋ถ์ผ์น(needs-recovery) โ ์น์ธ ํ์ ์ ์์ผ๋ ์ปค๋ฐ๋ ์ฆ๊ฑฐ ๋ถ์์ , ๋ณต๊ตฌ ํ์'
|
|
96
|
+
: state === 'dev-complete'
|
|
97
|
+
? '๊ฐ๋ฐ ์๋ฃ(dev-complete)'
|
|
98
|
+
: state === 'series-terminal'
|
|
99
|
+
? 'series ์ข
๊ฒฐ(replace/human-resolution)'
|
|
100
|
+
: // ๐ด REQ-2026-053: migrated-complete๋ **phase-1(์ปค๋ฐ 3ed1b95 close-proof.ts)**์์ eventยทbase-stateยท
|
|
101
|
+
// ํ์ยทderiveBaseState(dev-complete ์๋ยทneeds-recovery ์ ๋น์ฐจ๋จ)๋ก ์ด๋ฏธ ํ์ฅ๋๋ค. ์ด phase-2 diff๋
|
|
102
|
+
// ๋ช
๋ น(req:close)๊ณผ ์ด reason ์ผ์ด์ค๋ง ์ถ๊ฐํ๋ค. ์ข
๊ฒฐโpass ์ ํ์ดํ๋ผ์ธ์ req-close.test.ts โฎ๊ฐ ์ค์ฆ.
|
|
103
|
+
state === 'migrated-complete'
|
|
104
|
+
? '๊ฐ๋ฐ ์๋ฃ(๋ง์ด๊ทธ๋ ์ด์
์ข
๊ฒฐยทmigrated-complete)'
|
|
105
|
+
: String(state)
|
|
106
|
+
return { ...head, baseState: state, verdict: blocked ? 'block' : 'pass', reason, reconstructed: isReconstructed(facts.closeParsed.rows) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ํ ํฐ์ผ์ HEAD ์ฌ์ค์ ๋ชจ์ ํ์ (IO โ git ์กฐํ๋ง, read-only). `ports`๊ฐ HEAD blob ์ ๊ทผ ์ ๋ณธ.
|
|
111
|
+
*/
|
|
112
|
+
export function scanTicketIntake(root: string, ticketRel: string, ticketId: string): IntakeTicketResult {
|
|
113
|
+
const ports = createEvidencePorts(root, `${ticketRel}/responses`)
|
|
114
|
+
const durabilityRequired = isDurabilityRequired(ports.headText(`${ticketRel}/state.json`))
|
|
115
|
+
if (!durabilityRequired)
|
|
116
|
+
return classifyIntake({ ticketId, ticketRel, durabilityRequired: false, manifestText: null, manifestProblems: [], closeParsed: { rows: [], problems: [] }, evidenceIntegrityProblems: [], ledgerHasApprovedClose: false, committedEvidenceComplete: false, committedDesignRef: null, evidencedPhaseIds: [] })
|
|
117
|
+
const manifestText = ports.headText(`${ticketRel}/responses/approvals.jsonl`)
|
|
118
|
+
const closeText = ports.headText(`${ticketRel}/responses/ticket-close.jsonl`)
|
|
119
|
+
const ledgerText = ports.headText(`${ticketRel}/responses/review-ledger.jsonl`)
|
|
120
|
+
// validPhaseIds๋ ๋งค๋ํ์คํธ ์์ ์ phase ํ id๋ก ๋ง๋ ๋ค(HEAD state.phases๋ ์ค๊ณ์ []์ด๋ฏ๋ก ๋ฉค๋ฒ์ญ ๊ฒ์ฌ๋ง ๋ฌดํจํ โ
|
|
121
|
+
// verifyCommittedDesignEvidence์ ๋์ผ ๊ธฐ๋ฒ). ๋๋จธ์ง ๊ตฌ์กฐยท๊ฒฝ๋กยทshaยท์ฃผ์
ยทkind ๊ฒฉ๋ฆฌยทphase_design_ref ํ์์ ๊ฐ์ ๋๋ค.
|
|
122
|
+
const manifestPhaseIds = manifestText ? evidencedPhaseIdsFromManifest(manifestText) : []
|
|
123
|
+
const manifestProblems = manifestText ? validateManifest(manifestText, { ticketRel, validPhaseIds: manifestPhaseIds }) : []
|
|
124
|
+
const closeParsed = closeText ? parseCloseProof(closeText) : { rows: [], problems: [] }
|
|
125
|
+
const ledgerParsed = ledgerText ? parseLedger(ledgerText) : { rows: [], problems: [] }
|
|
126
|
+
const ledgerHasApprovedClose = ledgerParsed.rows.some((r) => r.event === 'attempt-closed' && r.outcome === 'approved')
|
|
127
|
+
const committedDesignRef = manifestText ? designHashFromManifest(manifestText) : null
|
|
128
|
+
const evidencedPhaseIds = manifestText ? evidencedPhaseIdsFromManifest(manifestText, committedDesignRef) : []
|
|
129
|
+
// ๐ด DEC-B7: committed ์ฆ๊ฑฐ(design+phase) ๋ฌด๊ฒฐ์ฑ ์ข
ํฉ โ intakeยทreq:commit ๊ณต์ ๋ชจ๋. designEvidenceComplete๋ก
|
|
130
|
+
// needs-recovery ํ์ ์
๋ ฅ(committedEvidenceComplete)์ ๊ฐ์ ์กฐํ์์ ์ป๋๋ค(์ค๋ณต ์กฐํ ์์).
|
|
131
|
+
const integrity = verifyCommittedEvidenceIntegrity({ ticketRel, manifestText, ports })
|
|
132
|
+
return classifyIntake({ ticketId, ticketRel, durabilityRequired: true, manifestText, manifestProblems, closeParsed, evidenceIntegrityProblems: integrity.problems, ledgerHasApprovedClose, committedEvidenceComplete: integrity.designEvidenceComplete, committedDesignRef, evidencedPhaseIds })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* HEAD tree์ `workflow/REQ-*` ๋๋ ํฐ๋ฆฌ ์ด๋ฆ(์ ๋ ฌ). ๐ด **์ํน ๋๋ ํฐ๋ฆฌ๋ฅผ ์ฝ์ง ์๋๋ค**(HEAD only).
|
|
137
|
+
*
|
|
138
|
+
* ๐ด **ํํ ์ฌ๋์๊ฐ load-bearing์ด๋ค**: `git ls-tree -d --name-only HEAD workflow/`(์ฌ๋์ ๆ)๋ `workflow/`์
|
|
139
|
+
* **์ง๊ณ ์์ tree**(= `workflow/REQ-*`)๋ฅผ ์ด๊ฑฐํ๋ค. ์ฌ๋์๊ฐ **์์ผ๋ฉด**(`workflow`) ls-tree๋ ๊ทธ ํญ๋ชฉ ์์
|
|
140
|
+
* (`workflow`) ํ ์ค๋ง ๋ธ๋ค โ ์์์ ๋ชป ๋ณด๊ณ ์ค์บ์ด ํต์งธ๋ก ๋น์ด **๊ฒ์ดํธ๊ฐ ์ฐํ๋๋ค**. ๊ทธ๋์ ์๋์์ ํญ์
|
|
141
|
+
* `${dir}/`๋ก ์ฌ๋์๋ฅผ ๋ถ์ธ๋ค. ์ด ์ ์ ๊ฒฝ๋ก๋ `req-new-intake.test.ts`์ ์ค git ์ด๊ฑฐยท์์ฑ ์ ์ฐจ๋จ e2e๊ฐ ๊ณ ์ ํ๋ค
|
|
142
|
+
* (์ด๊ฑฐ๊ฐ ๋น๋ฉด developing ํฐ์ผ์ด ์ฐจ๋จ๋์ง ์์ ๊ทธ ํ
์คํธ๊ฐ ์คํจํ๋ค).
|
|
143
|
+
*/
|
|
144
|
+
export function listHeadTicketIds(workflowDirRel: string, gitFn: (a: string[]) => string): string[] {
|
|
145
|
+
const dir = workflowDirRel.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
146
|
+
let out: string
|
|
147
|
+
try {
|
|
148
|
+
out = gitFn(['ls-tree', '-d', '--name-only', 'HEAD', `${dir}/`]) // ๐ด ํํ ์ฌ๋์ ํ์(์ง๊ณ ์์ ์ด๊ฑฐ).
|
|
149
|
+
} catch {
|
|
150
|
+
return [] // workflow/ ๊ฐ HEAD์ ์์(์ฒซ REQ ๋ฑ) โ ์ค์บ ๋์ ์์.
|
|
151
|
+
}
|
|
152
|
+
return out
|
|
153
|
+
.split('\n')
|
|
154
|
+
.map((l) => l.trim())
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map((p) => p.split('/').pop() ?? '')
|
|
157
|
+
.filter((n) => /^REQ-\d{4}-\d+$/.test(n))
|
|
158
|
+
.sort()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* HEAD์ `workflow/REQ-*` ํฐ์ผ ์ ๋ถ๋ฅผ ์ค์บ(read-only). `excludeTicketId`(๋์ฒด๋ ๋ถ๋ชจ ๋ฑ)๋ ์ค์บ์์ ์ ์ธํ๋ค โ
|
|
163
|
+
* ๊ทธ ํฐ์ผ์ ์ง๊ธ ์ ๊ท replace ํ๋ฆ์ผ๋ก ์ข
๊ฒฐ๋๋ฏ๋ก ํ์ ์์ฑ์ ๋ง์์ ์ ๋๋ค(์ ์ธ๋ ๋ถ๋ชจ์ replace ๊ฒ์ฆ์ด ์ด๋ฏธ ๋๋ ๋ค์๋ง ์ด๋ค).
|
|
164
|
+
*/
|
|
165
|
+
export function scanIntake(
|
|
166
|
+
root: string,
|
|
167
|
+
workflowDirRel: string,
|
|
168
|
+
gitFn: (a: string[]) => string,
|
|
169
|
+
excludeTicketId?: string | null,
|
|
170
|
+
): { tickets: IntakeTicketResult[]; blocked: IntakeTicketResult[] } {
|
|
171
|
+
const ticketIds = listHeadTicketIds(workflowDirRel, gitFn).filter((id) => id !== excludeTicketId)
|
|
172
|
+
const dir = workflowDirRel.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
173
|
+
const tickets = ticketIds.map((id) => scanTicketIntake(root, `${dir}/${id}`, id))
|
|
174
|
+
return { tickets, blocked: tickets.filter((t) => t.verdict === 'block') }
|
|
175
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lockfile diff **์์ฝ** (REQ-2026-056ยทDEC-E1).
|
|
3
|
+
*
|
|
4
|
+
* `git diff --cached` ์ ๋ฌธ์ด ๋ฆฌ๋ทฐ ํ๋กฌํํธ์ ๋ค์ด๊ฐ๋๋ฐ, lockfile(`package-lock.json`ยท`pnpm-lock.yaml` ๋ฑ)์
|
|
5
|
+
* ์์ฒ ์ค ๊ธฐ๊ณ์์ฑ์ด๋ผ ํ ํฐ์ ๋ญ๋นํ๊ณ ์ฌ๋/๋ฆฌ๋ทฐ์ด ์ ํธ๋ฅผ ๋ฌป๋๋ค. lockfile ๊ตฌํ์ hunk๋ฅผ **์์ฝ**(๊ฒฝ๋กยทํค๋
|
|
6
|
+
* ๋ณด์กด + ยฑN/M + ์๋ต๋ถ sha256)์ผ๋ก ๋์ฒดํ๋ค.
|
|
7
|
+
*
|
|
8
|
+
* ๐ด **ํ๋กฌํํธ๋ง ๋ฐ๊พผ๋ค** โ ์น์ธ ๋ฐ์ธ๋ฉ(reviewTree = git write-tree)์ ์ ์ฒด index๋ผ ์ด ๋ฌธ์์ด ๋ณํ๊ณผ ๋ฌด๊ดํ๋ค.
|
|
9
|
+
* ์น์ธ์ ์ฌ์ ํ ์ ์ฒด lockfile์ ๊ฒฐ์ํ๋ค(์ด ๋ชจ๋์ ๊ทธ ์ฌ์ค์ ๊ฐ์ ํ์ง ์์ง๋ง, ํธ์ถ๋ถ๊ฐ stagedDiff ๋ฌธ์์ด์๋ง
|
|
10
|
+
* ์ด ๋ณํ์ ์ ์ฉํ๋ค).
|
|
11
|
+
*
|
|
12
|
+
* ์์ ๋ชจ๋ โ fsยทgit์ ๋ชจ๋ฅธ๋ค(deterministicยท`createHash`๋ง).
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
|
|
16
|
+
/** lockfile๋ก ์ธ์ํ basename(์ ํ ์ผ์น๋ง โ ๊ฒฝ๋ก ๋ถ๋ถ๋ฌธ์์ด ์คํ ๋ฐฉ์ง). */
|
|
17
|
+
export const LOCKFILE_NAMES: ReadonlySet<string> = new Set([
|
|
18
|
+
'package-lock.json',
|
|
19
|
+
'npm-shrinkwrap.json',
|
|
20
|
+
'pnpm-lock.yaml',
|
|
21
|
+
'yarn.lock',
|
|
22
|
+
'bun.lockb',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/** ๊ฒฝ๋ก ์ ๋(`a/`ยท`b/`)ยทgit C-quote(๋ฐ์ดํ)๋ฅผ ๋ฒ๊ฒจ ์ ๊ทํํ๋ค. */
|
|
26
|
+
function stripPathDecoration(raw: string): string {
|
|
27
|
+
let t = raw.trim()
|
|
28
|
+
if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) t = t.slice(1, -1) // git quoted path
|
|
29
|
+
if (t.startsWith('a/') || t.startsWith('b/')) t = t.slice(2)
|
|
30
|
+
return t
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ํ ๊ตฌํ์ **๋ชจ๋ ํ๋ณด ๊ฒฝ๋ก**๋ฅผ ํค๋ ์ค์์ ๋ชจ์๋ค(r01 P1). `diff --git`์ ์์ชฝ ๊ฒฝ๋ก๋ ๊ณต๋ฐฑ์์ ๋ชจํธํ๋ฏ๋ก
|
|
35
|
+
* `---`/`+++`/`rename from|to`/`copy from|to`/`Binary files โฆ differ`์ ๋ช
์ ๊ฒฝ๋ก๋ ํจ๊ป ๋ณธ๋ค โ
|
|
36
|
+
* rename(ํ์ชฝ๋ง lockfile)ยท๊ณต๋ฐฑ/๋ฐ์ดํ ๊ฒฝ๋ก์์๋ ์ธ์๋๊ฒ.
|
|
37
|
+
*/
|
|
38
|
+
function sectionPaths(section: string[]): string[] {
|
|
39
|
+
const paths: string[] = []
|
|
40
|
+
const add = (p: string | undefined): void => {
|
|
41
|
+
if (p === undefined) return
|
|
42
|
+
const s = stripPathDecoration(p)
|
|
43
|
+
if (s && s !== '/dev/null') paths.push(s)
|
|
44
|
+
}
|
|
45
|
+
for (const line of section) {
|
|
46
|
+
if (line.startsWith('--- ')) add(line.slice(4))
|
|
47
|
+
else if (line.startsWith('+++ ')) add(line.slice(4))
|
|
48
|
+
else if (line.startsWith('rename from ')) add(line.slice(12))
|
|
49
|
+
else if (line.startsWith('rename to ')) add(line.slice(10))
|
|
50
|
+
else if (line.startsWith('copy from ')) add(line.slice(10))
|
|
51
|
+
else if (line.startsWith('copy to ')) add(line.slice(8))
|
|
52
|
+
else if (line.startsWith('Binary files ')) {
|
|
53
|
+
const m = /^Binary files (.+) and (.+) differ$/.exec(line)
|
|
54
|
+
if (m) { add(m[1]); add(m[2]) }
|
|
55
|
+
} else if (line.startsWith('diff --git ')) {
|
|
56
|
+
const m = /^diff --git (.+) (.+)$/.exec(line) // best-effort(๊ณต๋ฐฑ ๊ฒฝ๋ก๋ฉด ---/+++๊ฐ ๋ณด์)
|
|
57
|
+
if (m) { add(m[1]); add(m[2]) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return paths
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** ๊ฒฝ๋ก basename์ด lockfile์ธ๊ฐ(์ ํ ์ผ์น). */
|
|
64
|
+
export function isLockfilePath(path: string): boolean {
|
|
65
|
+
const base = path.replace(/\\/g, '/').split('/').pop() ?? ''
|
|
66
|
+
return LOCKFILE_NAMES.has(base)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** ๊ตฌํ์ด lockfile์ ๋ค๋ฃจ๋๊ฐ(์์ชฝ ๊ฒฝ๋กยทrenameยทbinary ํค๋ ๋ชจ๋ ๊ฒ์ฌ). */
|
|
70
|
+
function sectionIsLockfile(section: string[]): { yes: boolean; display: string } {
|
|
71
|
+
const paths = sectionPaths(section)
|
|
72
|
+
const lock = paths.find(isLockfilePath)
|
|
73
|
+
return { yes: lock !== undefined, display: lock ?? paths[paths.length - 1] ?? '(lockfile)' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** ํ ํ์ผ ๊ตฌํ(`diff --git`์ผ๋ก ์์ํ๋ ์ค ๋ฐฐ์ด)์ ์์ฝ(lockfile์ผ ๋๋ง). ๋น-lockfile์ ๊ทธ๋๋ก. */
|
|
77
|
+
function summarizeSection(section: string[]): string[] {
|
|
78
|
+
const { yes, display: path } = sectionIsLockfile(section)
|
|
79
|
+
if (!yes) return section
|
|
80
|
+
|
|
81
|
+
const hunkIdx = section.findIndex((l) => l.startsWith('@@ '))
|
|
82
|
+
if (hunkIdx >= 0) {
|
|
83
|
+
const header = section.slice(0, hunkIdx)
|
|
84
|
+
const hunks = section.slice(hunkIdx)
|
|
85
|
+
// `+++`/`---`๋ ํ์ผ ํค๋๋ผ hunkIdx ์ดํ์ ์์ง๋ง ๋ฐฉ์ด์ ์ผ๋ก ์ ์ธํ๋ค.
|
|
86
|
+
const plus = hunks.filter((l) => l.startsWith('+') && !l.startsWith('+++')).length
|
|
87
|
+
const minus = hunks.filter((l) => l.startsWith('-') && !l.startsWith('---')).length
|
|
88
|
+
const sha = createHash('sha256').update(hunks.join('\n'), 'utf8').digest('hex').slice(0, 12)
|
|
89
|
+
return [
|
|
90
|
+
...header,
|
|
91
|
+
`# [lockfile ์ ๋ฌธ ์๋ต โ ์์ฝ ๋ชจ๋] ${path}: +${plus}/-${minus} lines ยท sha256(์๋ต๋ถ)=${sha} ยท ์ ๋ฌธ์ config lockfilePromptFull:true`,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
const binaryIdx = section.findIndex((l) => l.startsWith('Binary files ') && l.includes(' differ'))
|
|
95
|
+
if (binaryIdx >= 0) {
|
|
96
|
+
return [...section.slice(0, binaryIdx), `# [lockfile binary ๋ณ๊ฒฝ โ ์์ฝ ๋ชจ๋] ${path} ยท ์ ๋ฌธ์ config lockfilePromptFull:true`]
|
|
97
|
+
}
|
|
98
|
+
// hunk๋ binary๋ ์์(์์ rename/mode ๋ณ๊ฒฝ ๋ฑ) โ ํค๋๋ง ์์ผ๋ ๊ทธ๋๋ก ๋๋ค.
|
|
99
|
+
return section
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* staged diff์ lockfile ๊ตฌํ์ ์์ฝํ๋ค(์์). `opts.full`์ด๋ฉด ์๋ฌธ ๊ทธ๋๋ก.
|
|
104
|
+
* ๐ด lockfile ๊ตฌํ์ด ํ๋๋ ์์ผ๋ฉด **์์ no-op**(์
๋ ฅ===์ถ๋ ฅ) โ ๊ธฐ์กด byte-identical near-e2e ๋ฌดํ๊ท.
|
|
105
|
+
*/
|
|
106
|
+
export function summarizeLockfileDiff(stagedDiff: string, opts: { full: boolean }): string {
|
|
107
|
+
if (opts.full || stagedDiff === '') return stagedDiff
|
|
108
|
+
const lines = stagedDiff.split('\n')
|
|
109
|
+
const starts: number[] = []
|
|
110
|
+
for (let i = 0; i < lines.length; i++) if (lines[i]!.startsWith('diff --git ')) starts.push(i)
|
|
111
|
+
if (starts.length === 0) return stagedDiff // ํ์ค ํ์ผ ๊ตฌํ ์์ โ ์๋์ง ์๋๋ค.
|
|
112
|
+
|
|
113
|
+
// ๊ตฌํ๋ณ๋ก ์์ฝ(๋น-lockfile์ ๊ทธ๋๋ก). ์์ฝ์ด ํ๋๋ ์ผ์ด๋์ง ์์ผ๋ฉด ์๋ฌธ ๋ฐํ(no-op ๋ณด์ฅ).
|
|
114
|
+
const result: string[] = []
|
|
115
|
+
if (starts[0]! > 0) result.push(...lines.slice(0, starts[0]!)) // ์ฒซ ๊ตฌํ ๅ preamble(์์ผ๋ฉด) ๋ณด์กด.
|
|
116
|
+
let changed = false
|
|
117
|
+
for (let s = 0; s < starts.length; s++) {
|
|
118
|
+
const from = starts[s]!
|
|
119
|
+
const to = s + 1 < starts.length ? starts[s + 1]! : lines.length
|
|
120
|
+
const section = lines.slice(from, to)
|
|
121
|
+
const summarized = summarizeSection(section)
|
|
122
|
+
if (summarized !== section) changed = true // summarizeSection์ ์์ฝ ์์๋ง ์ ๋ฐฐ์ด์ ๋ฐํํ๋ค.
|
|
123
|
+
result.push(...summarized)
|
|
124
|
+
}
|
|
125
|
+
if (!changed) return stagedDiff // lockfile ๊ตฌํ ์์(๋๋ ์์ฝ ๋์ ์์) โ ์๋ฌธ ๊ทธ๋๋ก.
|
|
126
|
+
return result.join('\n')
|
|
127
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* req:reconstruct ๋ณต์ ๊ฐ๋ฅ์ฑ ๋งคํธ๋ฆญ์ค (REQ-2026-052 phase-4ยทDEC-D2) โ **์์** ์ฐ์ถ.
|
|
3
|
+
*
|
|
4
|
+
* ๐ด close-proof lifecycle event๋ `dev-complete`ยท`series-terminal` ๋๋ฟ์ด๋ค. ์ด ๋ชจ๋์ **HEAD-committed
|
|
5
|
+
* immutable evidence๊ฐ ํ์ ๋ชจ๋ ํ๋๋ฅผ ๋ช
ํยท๋ชจํธ์์ด ๊ฒฐ์ ํ ๋๋ง** ๋ณต์ ํ๋ณด๋ฅผ ๋ธ๋ค:
|
|
6
|
+
* - **dev-complete**: ๐ด **์ ๋ ์ฐ์ถํ์ง ์๋๋ค.** `phase_inventory`๋ฅผ approvals.jsonl์ phase_id ์งํฉ์ผ๋ก
|
|
7
|
+
* ํฉ์ฑํ๋ฉด ๊ณํ๋์ผ๋ ๋ฏธ์ปค๋ฐ์ธ phase๋ฅผ ์กฐ์ฉํ ๋นผ๋ DEC-B5 P1์ด ์ฌ๋ฐํ๋ค. inventory ๋
๋ฆฝ ๊ธฐ๋ก์ด ์์ผ๋ฏ๋ก
|
|
8
|
+
* self-verifying ํ์ด ์์ผ๋ฉด ๋ณต์ ๊ทผ๊ฑฐ๊ฐ ์๋ค โ ์ฌ๊ฒํ ยท์ฌ๋ด๊ตฌํ๊ฐ ์ ์ผ ๊ฒฝ๋ก.
|
|
9
|
+
* - **series-terminal(replace)**: ์ปค๋ฐ๋ **successor** ํฐ์ผ์ `successor_of`(parent_series_id ํฌํจ)๊ฐ
|
|
10
|
+
* ์ด ํฐ์ผ replace ์ข
๊ฒฐ์ ์์ ํ ์ฆ๋ช
ํ ๋๋ง.
|
|
11
|
+
* - **series-terminal(terminate)**: successor๋ฅผ ๋ง๋ค์ง ์์ ์ฆ๊ฑฐ๊ฐ ์๋ค โ ๋ถ๊ฐ.
|
|
12
|
+
*
|
|
13
|
+
* fsยทgitยทreview-codex๋ฅผ ๋ชจ๋ฅด๋ leaf(๋ถ์์ฉยทevidence ์ถ์ถ์ CLI๊ฐ ํ๋ค).
|
|
14
|
+
*/
|
|
15
|
+
import { type CloseProofRow, closeProofRowKey } from './close-proof'
|
|
16
|
+
|
|
17
|
+
/** ์ด ํฐ์ผ์ replace ๋ถ๋ชจ๋ก ์ง๋ชฉํ๋ committed successor์ ์ถ์ถ ์ฆ๊ฑฐ(CLI๊ฐ HEAD blob์์ ๋ฝ์ ๋ฃ๋๋ค). */
|
|
18
|
+
export interface SuccessorEvidence {
|
|
19
|
+
/** successor ํฐ์ผ id(์ง๋จ์ฉ). */
|
|
20
|
+
successorTicketId: string
|
|
21
|
+
/** successor `state.json`์ repo-์๋ ๊ฒฝ๋ก โ evidence_basis. */
|
|
22
|
+
successorStatePath: string
|
|
23
|
+
/** `successor_of.parent_series_id` โ ๋ถ๋ชจ์ replace ์ข
๊ฒฐ series_id. */
|
|
24
|
+
parentSeriesId: string
|
|
25
|
+
/** ์ข
๊ฒฐ ์ฌ์ . successor๋ ํญ์ `replace`(collect๊ฐ ๊ทธ๋ ๊ฒ ๊ฑฐ๋ฅธ๋ค) โ material field๋ก ๋ช
์ ๊ฒ์ฌํ๋ค. */
|
|
26
|
+
resolution: 'replace'
|
|
27
|
+
/** `successor_of.parent_replace_resolution.decided_at` โ ์ข
๊ฒฐ ์์ (๋ถ๋ชจ ๊ฐ). */
|
|
28
|
+
at: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReconstructCandidate {
|
|
32
|
+
row: CloseProofRow
|
|
33
|
+
evidenceBasis: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReconstructPlan {
|
|
37
|
+
/** ๋ณต์ ์์ (์ ๊ท) ํ. */
|
|
38
|
+
candidates: ReconstructCandidate[]
|
|
39
|
+
/** ๋ณต์ ๋ถ๊ฐยท๋ถํ์ยท๋ชจํธ ์ฌ์ (์ง๋จ ํ์์ฉ โ write๋ ์ ํ์ง๋ง ๋ช
๋ น์ ๊ณ์). */
|
|
40
|
+
refusals: string[]
|
|
41
|
+
/** ๐ด fail-closed conflict(HEAD ๋ชจ์) โ ํ๋๋ผ๋ ์์ผ๋ฉด CLI๋ **์ ์ฒด๋ฅผ write 0์ผ๋ก ์ค๋จ**ํ๋ค. */
|
|
42
|
+
conflicts: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ์ด ํฐ์ผ์ ๋ณต์ ๊ฐ๋ฅํ close-proof ํ์ ๋งคํธ๋ฆญ์ค(DEC-D2 multi-witness)๋๋ก ์ฐ์ถ(์์). dev-complete๋ ์ ๋ ์ฐ์ถ ์ ํจ.
|
|
47
|
+
*
|
|
48
|
+
* ๐ด **parent_series_id๋ณ ๊ทธ๋ฃนํ**: ๊ฐ์ series๋ฅผ ๊ฐ๋ฆฌํค๋ ๋ณต์ witness์ material field(resolutionยทat)๊ฐ **์ ๋ถ ์ผ์น**ํ
|
|
49
|
+
* ๋๋ง ํ๋ณด 1๊ฐ(evidence_basis=๋ชจ๋ successor state ๊ฒฝ๋ก ์ ๋ ฌยท์ค๋ณต์ ๊ฑฐ). ๋ถ์ผ์น=ambiguity refusal(๊ทธ series write 0).
|
|
50
|
+
* ๐ด **HEAD ์ ํฉ**: ๊ฐ์ ์์ฐํค ํ์ด HEAD์ ์์ผ๋ฉด material ์ผ์น=๋ฉฑ๋ฑ no-op, ๋ชจ์=**conflict**(์จ๊ธฐ์ง ์๊ณ fail-closed).
|
|
51
|
+
*
|
|
52
|
+
* @param existingRows ์ด ํฐ์ผ HEAD close-proof๋ฅผ ํ์ฑํ ํ๋ค. ์์ ํฐ์ผ์ CLI๊ฐ ์ด ํจ์ **์ ์** ๊ฑฐ๋ฅธ๋ค.
|
|
53
|
+
* @param successors ์ด ํฐ์ผ์ ๋ถ๋ชจ๋ก ์ง๋ชฉํ๋ committed successor ์ฆ๊ฑฐ๋ค.
|
|
54
|
+
*/
|
|
55
|
+
export function planReconstruction(args: {
|
|
56
|
+
ticketId: string
|
|
57
|
+
existingRows: readonly CloseProofRow[]
|
|
58
|
+
successors: readonly SuccessorEvidence[]
|
|
59
|
+
}): ReconstructPlan {
|
|
60
|
+
const candidates: ReconstructCandidate[] = []
|
|
61
|
+
const refusals: string[] = []
|
|
62
|
+
const conflicts: string[] = []
|
|
63
|
+
|
|
64
|
+
// โ parent_series_id๋ณ ๊ทธ๋ฃนํ(๋น ํ๋๋ ๋ฏธ๊ฒฐ์ ์ผ๋ก ๊ฐ๋ณ refusal).
|
|
65
|
+
const groups = new Map<string, SuccessorEvidence[]>()
|
|
66
|
+
for (const s of args.successors) {
|
|
67
|
+
if (!s.parentSeriesId || !s.at || !s.resolution) {
|
|
68
|
+
refusals.push(`successor ${s.successorTicketId}: parent_series_idยทresolutionยทat ์ค ๋ฏธ๊ฒฐ์ โ ๋ณต์ ๋ถ๊ฐ(๋ชจํธ)`)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
const g = groups.get(s.parentSeriesId)
|
|
72
|
+
if (g) g.push(s)
|
|
73
|
+
else groups.set(s.parentSeriesId, [s])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const seriesId of [...groups.keys()].sort()) {
|
|
77
|
+
const witnesses = groups.get(seriesId)!
|
|
78
|
+
// โก material field(resolutionยทat) ์ ๋ถ ์ผ์นํด์ผ ํ๋ณด. ๋ถ์ผ์น=ambiguity(๊ทธ series ํ๋ณด ์์).
|
|
79
|
+
const ats = new Set(witnesses.map((w) => w.at))
|
|
80
|
+
const resolutions = new Set(witnesses.map((w) => w.resolution))
|
|
81
|
+
if (ats.size > 1 || resolutions.size > 1) {
|
|
82
|
+
refusals.push(
|
|
83
|
+
`series ${seriesId}: ๋ณต์ successor์ material field ๋ถ์ผ์น(at=[${[...ats].sort().join(', ')}] resolution=[${[...resolutions].sort().join(', ')}]) โ ambiguity ๋ณต์ ๋ถ๊ฐ(write 0)`,
|
|
84
|
+
)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const at = witnesses[0]!.at
|
|
88
|
+
const resolution = witnesses[0]!.resolution
|
|
89
|
+
// ์์ ์ผ์นํ๋ ๋ณต์ witness โ evidence_basis์ ๋ชจ๋ ๊ฒฝ๋ก ์ ๋ ฌยท์ค๋ณต์ ๊ฑฐ.
|
|
90
|
+
const evidenceBasis = [...new Set(witnesses.map((w) => w.successorStatePath))].sort()
|
|
91
|
+
const row: CloseProofRow = {
|
|
92
|
+
ticket_id: args.ticketId,
|
|
93
|
+
event: 'series-terminal',
|
|
94
|
+
series_id: seriesId,
|
|
95
|
+
resolution,
|
|
96
|
+
phase_inventory: null,
|
|
97
|
+
design_ref: null,
|
|
98
|
+
at,
|
|
99
|
+
reconstructed: true,
|
|
100
|
+
evidence_basis: evidenceBasis,
|
|
101
|
+
}
|
|
102
|
+
// โข HEAD ์ ํฉ: ๊ฐ์ ์์ฐํค ํ์ด ์์ผ๋ฉด material ์ผ์น=๋ฉฑ๋ฑ no-op, ๋ชจ์=conflict(fail-closed).
|
|
103
|
+
const key = closeProofRowKey(row)
|
|
104
|
+
const prior = args.existingRows.find((r) => closeProofRowKey(r) === key)
|
|
105
|
+
if (prior) {
|
|
106
|
+
if (prior.resolution === resolution && prior.at === at) {
|
|
107
|
+
refusals.push(`series ${seriesId}: ๋์ผ series-terminal ํ์ด HEAD์ ์กด์ฌ โ ๋ณต์ ๋ถํ์(๋ฉฑ๋ฑ no-op)`)
|
|
108
|
+
} else {
|
|
109
|
+
conflicts.push(
|
|
110
|
+
`series ${seriesId}: HEAD ํ๊ณผ ๋ชจ์(HEAD at=${prior.at} resolution=${prior.resolution} โ ํ๋ณด at=${at} resolution=${resolution}) โ fail-closed(์จ๊ธฐ์ง ์์)`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
candidates.push({ row, evidenceBasis })
|
|
116
|
+
}
|
|
117
|
+
return { candidates, refusals, conflicts }
|
|
118
|
+
}
|