okstra 0.28.0 → 0.30.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.
@@ -33,6 +33,7 @@ Template authoring notes (NOT rendered to readers — HTML comments).
33
33
  - Task Key: {{TASK_KEY}}
34
34
  - Task Type: {{TASK_TYPE}}
35
35
  - Report Owner: `Claude lead`
36
+ - Report Author: `<Report writer worker | Claude lead (release-handoff or recorded-fallback only)>`
36
37
  - Lead Model: `{{LEAD_MODEL}}`
37
38
  - Okstra Version: `{{OKSTRA_VERSION}}`
38
39
 
@@ -360,7 +361,17 @@ H1 = `skip` 이거나 H3 = `cancel` 인 경우 4.6.4 ~ 4.6.6 은 빈 결과로
360
361
 
361
362
  commit 범위가 비어 있으면 release-handoff 는 실행되면 안 됩니다 → `- No implementation commits found; release-handoff is blocked.` + routing 을 `implementation` 으로 되돌림.
362
363
 
363
- ### 4.6.6 Pull Request Outcome (PR 결과)
364
+ ### 4.6.6 Merge Conflict Probe (사전 머지 충돌 점검)
365
+
366
+ `push + PR` 경로에서만 실행. 다음 셋 중 정확히 하나의 형식으로 한 줄을 기록합니다 (read-only — `git fetch origin <base>` + `git merge-tree --merge-base origin/<base> HEAD origin/<base>` 만 허용; mutating git 명령 금지):
367
+
368
+ - `- Not run (user picked local only or skip).`
369
+ - `- Clean — no conflicts against <base> at <origin/base SHA>.`
370
+ - `- Conflicts detected against <base> at <origin/base SHA>; user chose <proceed anyway | change base branch | cancel>. Conflicting paths: <list>.`
371
+
372
+ `push + PR` 인데 본 항목이 없거나 위 셋 외의 자유 서술이면 self-review 가 `contract-violated` 로 종료합니다 (`release-handoff` profile self-review 6번 — merge-conflict probe audit).
373
+
374
+ ### 4.6.7 Pull Request Outcome (PR 결과)
364
375
 
365
376
  다음 네 가지 중 정확히 하나의 형식으로 한 줄:
366
377
 
@@ -369,7 +380,7 @@ commit 범위가 비어 있으면 release-handoff 는 실행되면 안 됩니다
369
380
  - `- PR reused: <url>` (run 시작 시 같은 head 의 open PR 이 이미 존재해 `gh pr create` 생략)
370
381
  - `- PR creation skipped: <reason>` (H3 = `cancel`, 또는 push/PR 도중 사용자 중단)
371
382
 
372
- ### 4.6.7 Routing Recommendation (마지막 phase 라우팅)
383
+ ### 4.6.8 Routing Recommendation (마지막 phase 라우팅)
373
384
 
374
385
  `release-handoff` 는 lifecycle 종착 phase 이므로 일반적으로 `done` 으로 라우팅합니다. H1 = `skip` 또는 H3 = `cancel` 로 종료된 경우 재진입 가능 여부를 한 줄로 명시합니다.
375
386
 
@@ -377,6 +388,162 @@ commit 범위가 비어 있으면 release-handoff 는 실행되면 안 됩니다
377
388
 
378
389
  <!-- /RENDER_IF (task-type == release-handoff) -->
379
390
 
391
+ <!-- RENDER_IF: task-type == implementation
392
+ Delete the entire `## 4.7` block + sub-sections otherwise. -->
393
+
394
+ ## 4.7 Implementation Deliverables
395
+
396
+ `implementation` profile 의 "Required deliverable shape" 를 보고서 본문 구조로 옮겨 적습니다. 모든 sub-section 은 필수이며, 비어 있는 경우에도 헤딩은 유지하고 본문에 명시적 빈 상태 한 줄을 적습니다. validator 는 8 개 substring (`Approved Plan Reference`, `Commit List`, `Diff Summary`, `Out-of-plan Edits`, `Validation Evidence`, `Verifier Results`, `Rollback Verification`, `Routing Recommendation`) 의 등장 여부를 검사합니다.
397
+
398
+ ### 4.7.1 Approved Plan Reference (승인된 계획 참조)
399
+
400
+ - Plan file (project-relative): `<runs/implementation-planning/.../reports/final-report-implementation-planning-<seq>.md>`
401
+ - Approval evidence (해당 plan 의 정확한 인용 — `- [x] Approved` 마커 + Section 4.5.3 Recommended Option 한 줄):
402
+ > <원문 인용>
403
+ - 본 run 의 `EXECUTOR_WORKTREE_PATH`: `<absolute path>`
404
+ - 본 run 의 base ref (`{{EXECUTOR_WORKTREE_BASE_REF}}`): `<commit SHA>`
405
+
406
+ ### 4.7.2 Commit List (생성된 commit)
407
+
408
+ | # | Short SHA | Full SHA | Plan Step | Subject | Files |
409
+ |---|-----------|----------|-----------|---------|-------|
410
+ | 1 | `<abc1234>` | `<full-sha>` | Step 1 | `<exact commit subject>` | `<file paths>` |
411
+
412
+ 규칙: 한 commit = 한 plan step (또는 cohesive sub-step). `Subject` 는 git log 에 적힌 원문 그대로 — 재해석·요약 금지. commit 이 없으면 본 run 은 실행되지 않아야 했음 → `- No implementation commits produced; routing recommendation must be back to implementation-planning.` 한 줄.
413
+
414
+ ### 4.7.3 Diff Summary (변경 요약)
415
+
416
+ ```
417
+ <git diff --stat <base>..HEAD 의 raw 출력>
418
+ ```
419
+
420
+ 다음으로 file-by-file 한 줄 요약 표:
421
+
422
+ | File | Action | Lines (+/-) | Plan step / Out-of-plan |
423
+ |------|--------|-------------|--------------------------|
424
+ | `<path>` | created / modified / deleted | `+12 / -3` | Step 2 또는 `Out-of-plan` |
425
+
426
+ ### 4.7.4 Out-of-plan Edits (계획 외 편집)
427
+
428
+ 승인된 plan 의 file list 에 없는 파일을 편집한 경우 row 로 기록. 없으면 `- 계획 외 편집 없음.` 한 줄.
429
+
430
+ | ID | File | Rationale | Trigger (어떤 step 수행 중 발견) |
431
+ |----|------|-----------|--------------------------------|
432
+ | OOP-001 | `<path>` | <한 두 문장> | Step `<N>` |
433
+
434
+ ### 4.7.5 Validation Evidence (검증 증거)
435
+
436
+ plan 의 `4.5.6 Validation Checklist` 의 pre / mid / post 각 row 에 대해 실제 명령과 출력 / exit code 를 모두 기록합니다. 요약·"tests pass" 같은 단어 금지 — 명령 line 과 exit code 는 원문 그대로.
437
+
438
+ | Phase | Command | Exit code | Output tail (≤10 lines) | TDD evidence (failing→passing SHAs) |
439
+ |-------|---------|-----------|--------------------------|--------------------------------------|
440
+ | pre | `<cmd>` | `0` | <인용> | -- |
441
+ | mid | `<cmd>` | `0` | <인용> | `<failing SHA>` → `<passing SHA>` |
442
+ | post | `<cmd>` | `0` | <인용> | -- |
443
+
444
+ ### 4.7.6 Verifier Results (verifier 별 결과)
445
+
446
+ verifier role 마다 한 sub-block 으로 정리합니다 (`Claude verifier`, `Codex verifier`, opt-in 시 `Gemini verifier`). 각 verifier 의 read-only 명령 로그, 독립 재실행 결과, lint/format/typecheck 결과, 그리고 Discrepancy 라인을 모두 보존합니다. lead 는 합의 verdict 를 합성하되 의견을 collapse 하지 않습니다.
447
+
448
+ - **Claude verifier** — Verdict: `<PASS | CONCERNS | FAIL>`
449
+ - Read-only command log: <verifier 의 worker result 에서 원문 인용>
450
+ - Independent validation re-run: <plan validation command 별 exit code + tail>
451
+ - Style / lint / type-check: <도구·exit code·새 위반 수>
452
+ - Declined fix recommendations: <한 줄씩 — 없으면 `- 없음.`>
453
+ - Discrepancy (vs executor): `- 없음.` 또는 `- <plan step / command>: executor=<result>, verifier=<result>`
454
+ - **Codex verifier** — Verdict: ...
455
+ - **Gemini verifier** (opt-in 시) — Verdict: ...
456
+
457
+ 합의 verdict (lead 합성): `<PASS | CONCERNS | FAIL>` — 한 verifier 라도 `FAIL` 이면 합의는 `FAIL` (override 시 lead 가 구체적 재현 시점 이유 인용 필수).
458
+
459
+ ### 4.7.7 Rollback Verification (롤백 검증)
460
+
461
+ 변경 카테고리별로 다른 강도의 확인. 표 1 행 = 1 카테고리.
462
+
463
+ | Category | Rollback command | Verification | Result |
464
+ |----------|-------------------|---------------|--------|
465
+ | Pure code | `git revert <SHA>` | `git rev-parse <SHA>` 가 resolve 됨 | `ok` |
466
+ | Feature-flag-gated | flag off 상태 validation run | 위 4.7.5 의 해당 명령 인용 | `ok` |
467
+ | Schema/config/stateful | rollback step `<cmd>` dry-run | exit code + stdout 인용 | `ok` 또는 `unable — route back to planning` |
468
+
469
+ dry-run 모드가 없는 stateful 변경은 본 항목을 `unable` 로 적고 `## 6.` 의 첫 항목을 `implementation-planning` 재진입으로 권장해야 합니다.
470
+
471
+ ### 4.7.8 Routing Recommendation (다음 phase 라우팅)
472
+
473
+ 다음 셋 중 하나의 형식으로 한 줄:
474
+
475
+ - `- Routing: final-verification. All plan steps satisfied; rollback verified; verifier consensus <PASS|CONCERNS>.`
476
+ - `- Routing: error-analysis. <한 줄 — 어떤 결함이 추가 분석을 요구하는지>.`
477
+ - `- Routing: implementation-planning. <한 줄 — 왜 새 plan 이 필요한지 (drift / scope-mismatch / stateful-rollback-gap)>.`
478
+
479
+ <!-- /RENDER_IF (task-type == implementation) -->
480
+
481
+ <!-- RENDER_IF: task-type == final-verification
482
+ Delete the entire `## 4.8` block + sub-sections otherwise. -->
483
+
484
+ ## 4.8 Final Verification Deliverables
485
+
486
+ `final-verification` profile 의 "Required deliverable shape" 를 본문 구조로 옮겨 적습니다. 모든 sub-section 필수. validator 는 6 개 substring (`Source Implementation Report`, `Acceptance Blockers`, `Residual Risk`, `Validation Evidence`, `Read-only Command Log`, `Routing Recommendation`) 등장과 `## 2. Final Verdict` 의 `Verdict Token` 값이 `accepted` / `conditional-accept` / `blocked` 중 하나인지 검사합니다.
487
+
488
+ ### 4.8.1 Source Implementation Report (선행 implementation 인용)
489
+
490
+ - Path (project-relative): `<runs/implementation/.../reports/final-report-implementation-<seq>.md>`
491
+ - 인용된 commit list / diff summary 요약:
492
+ > <원문 인용 — `## 4.7.2` / `## 4.7.3` 에서>
493
+ - 검증 대상 worktree path: `<absolute path>`
494
+ - run 시작 시 capture 한 head/base SHA: `<base SHA> .. <head SHA>`
495
+ - `git status --short` (run 시작 시점):
496
+ ```
497
+ <원문 그대로>
498
+ ```
499
+
500
+ ### 4.8.2 Acceptance Blockers (수락 차단 항목)
501
+
502
+ 비어 있으면 표 대신 `- No acceptance blockers found.` 한 줄. 모든 blocker 는 구체적 artifact (file:line, log, exit code, MCP SELECT 결과) 인용 필수 — 증거 없는 blocker 는 4.8.3 residual risk 로 강등.
503
+
504
+ | ID | Severity | Statement | Evidence (path:line / log / exit code) | Recommended follow-up phase |
505
+ |----|----------|-----------|-----------------------------------------|------------------------------|
506
+ | AB-001 | `critical / major / minor` | <한 줄 결함 요약> | `<file>:<line>` 또는 로그 인용 | `error-analysis` / `implementation-planning` |
507
+
508
+ ### 4.8.3 Residual Risk (잔존 위험)
509
+
510
+ blocker 는 아니지만 추적해야 할 위험. 각 항목에 mitigation owner 와 blocker 로 격상되는 trigger 를 명시.
511
+
512
+ | ID | Item | Mitigation owner | Escalation trigger |
513
+ |----|------|------------------|---------------------|
514
+ | RR-001 | <한 줄> | <역할 / 팀> | <어떤 조건이면 AB-? 로 격상> |
515
+
516
+ 비어 있으면: `- 추적 대상 잔존 위험 없음.`
517
+
518
+ ### 4.8.4 Validation Evidence (요구사항 커버리지)
519
+
520
+ 선행 plan / task brief 의 모든 요구사항에 대해 cover 한 artifact 를 cite. 패러프레이즈 ("verified") 금지.
521
+
522
+ | ID | Requirement (plan/brief 인용) | Artifact (commit SHA / test output / log line / MCP SELECT) | Status (covered / blocker AB-? / gap) |
523
+ |----|--------------------------------|--------------------------------------------------------------|----------------------------------------|
524
+ | VE-001 | <한 줄> | `<artifact>` | covered |
525
+
526
+ ### 4.8.5 Read-only Command Log (실행 명령 로그)
527
+
528
+ 본 run 에서 실행한 모든 명령 (Tier 1 plan validation + Tier 2 `project.json.qaCommands`) 을 실행 순서대로 한 row 씩 기록. mutating 명령은 등장하면 안 됨.
529
+
530
+ | # | Tier | Command (verbatim) | Exit code | Output tail (≤5 lines) |
531
+ |---|------|---------------------|-----------|-------------------------|
532
+ | 1 | 1 | `<plan validation cmd>` | `0` | <인용> |
533
+ | 2 | 2 | `cargo clippy --all-targets -- -D warnings` | `0` | <인용> |
534
+
535
+ `project.json.qaCommands` 의 category 가 비어 있으면 `qa-command not configured: <lint/format/typecheck/test>` 한 줄. deny-list (`--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, `npm install` without `ci` 등) 토큰을 포함한 cmd 는 `qa-command rejected (denied token: <token>): <label>` 한 줄로 기록 후 실행하지 않음.
536
+
537
+ ### 4.8.6 Routing Recommendation (다음 phase 라우팅)
538
+
539
+ `## 2. Final Verdict` 의 `Verdict Token` 과 1:1 대응. 다음 셋 중 하나:
540
+
541
+ - `- Routing: release-handoff. Verdict Token = accepted; PR-ready.`
542
+ - `- Routing: release-handoff with conditions. Verdict Token = conditional-accept; conditions listed in 4.8.2 / 4.8.3 must be resolved before push.`
543
+ - `- Routing: error-analysis (or implementation-planning). Verdict Token = blocked; <blocker AB-?> requires <re-analysis | replan>.`
544
+
545
+ <!-- /RENDER_IF (task-type == final-verification) -->
546
+
380
547
  ## 5. Clarification Items
381
548
 
382
549
  다음 run 으로 넘어가기 전에 사용자가 답하거나 자료를 첨부해야 하는 항목을 **한 표 안에서** 추적합니다. `task-type` 이 `error-analysis` / `requirements-discovery` 이고 지금까지의 증거만으로 확신 있는 최종 판단이 어려울 때는 반드시 채웁니다. 그 외 task-type 에서는 lead 가 필요하다고 판단할 때만 채우고, 그렇지 않다면 `- 추가 정보 요청 없음. Section 2 의 최종 판단이 그대로 유효합니다.` 한 줄만 남깁니다.
@@ -0,0 +1,151 @@
1
+ /* Single self-contained stylesheet for the okstra final-report HTML view.
2
+ * Inlined verbatim by scripts/okstra_ctl/report_views.py render_html.
3
+ * No external @import, no url() references. System colors only so dark
4
+ * mode follows the user's OS without a media query toggle.
5
+ */
6
+
7
+ * { box-sizing: border-box; }
8
+
9
+ html { color-scheme: light dark; }
10
+
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
15
+ font-size: 15px;
16
+ line-height: 1.55;
17
+ background: Canvas;
18
+ color: CanvasText;
19
+ }
20
+
21
+ .report-header,
22
+ .report-footer {
23
+ position: sticky;
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.75rem;
27
+ padding: 0.6rem 1rem;
28
+ background: Canvas;
29
+ border-bottom: 1px solid GrayText;
30
+ z-index: 10;
31
+ }
32
+
33
+ .report-header { top: 0; }
34
+ .report-footer {
35
+ bottom: 0;
36
+ border-top: 1px solid GrayText;
37
+ border-bottom: none;
38
+ flex-wrap: wrap;
39
+ }
40
+
41
+ .report-header > div { flex: 1; font-weight: 600; }
42
+
43
+ main {
44
+ max-width: 80ch;
45
+ margin: 1.5rem auto;
46
+ padding: 0 1rem 4rem;
47
+ }
48
+
49
+ h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin-top: 1.6em; margin-bottom: 0.4em; }
50
+ h1 { font-size: 1.7rem; }
51
+ h2 { font-size: 1.35rem; border-bottom: 1px solid GrayText; padding-bottom: 0.2em; }
52
+ h3 { font-size: 1.12rem; }
53
+ h4 { font-size: 1rem; }
54
+
55
+ p, ul, ol, blockquote, pre, table { margin: 0.6em 0; }
56
+
57
+ ul, ol { padding-left: 1.4em; }
58
+
59
+ blockquote {
60
+ border-left: 3px solid GrayText;
61
+ padding: 0.2em 0.8em;
62
+ color: GrayText;
63
+ }
64
+
65
+ code {
66
+ font-family: "SFMono-Regular", Menlo, Consolas, monospace;
67
+ font-size: 0.92em;
68
+ padding: 0.1em 0.35em;
69
+ background: color-mix(in srgb, CanvasText 8%, transparent);
70
+ border-radius: 3px;
71
+ }
72
+
73
+ pre {
74
+ overflow-x: auto;
75
+ padding: 0.8em 1em;
76
+ background: color-mix(in srgb, CanvasText 6%, transparent);
77
+ border-radius: 4px;
78
+ }
79
+ pre code { background: transparent; padding: 0; }
80
+
81
+ table {
82
+ width: 100%;
83
+ border-collapse: collapse;
84
+ font-size: 0.92rem;
85
+ }
86
+ th, td {
87
+ border: 1px solid color-mix(in srgb, GrayText 50%, transparent);
88
+ padding: 0.45em 0.6em;
89
+ vertical-align: top;
90
+ text-align: left;
91
+ }
92
+ thead th {
93
+ position: sticky;
94
+ top: 3rem;
95
+ background: Canvas;
96
+ z-index: 5;
97
+ }
98
+
99
+ tr[data-response-id] {
100
+ background: color-mix(in srgb, Highlight 6%, transparent);
101
+ }
102
+ tr[data-response-id][data-status="resolved"],
103
+ tr[data-response-id][data-status="obsolete"] {
104
+ background: transparent;
105
+ opacity: 0.65;
106
+ }
107
+
108
+ textarea {
109
+ width: 100%;
110
+ min-height: 2.2em;
111
+ font: inherit;
112
+ padding: 0.3em 0.4em;
113
+ border: 1px solid GrayText;
114
+ border-radius: 3px;
115
+ background: Canvas;
116
+ color: CanvasText;
117
+ resize: vertical;
118
+ }
119
+ textarea[disabled] { opacity: 0.55; }
120
+
121
+ button[data-action] {
122
+ font: inherit;
123
+ padding: 0.4em 0.9em;
124
+ border: 1px solid GrayText;
125
+ border-radius: 4px;
126
+ background: ButtonFace;
127
+ color: ButtonText;
128
+ cursor: pointer;
129
+ }
130
+ button[data-action]:hover { background: color-mix(in srgb, Highlight 20%, ButtonFace); }
131
+
132
+ #user-response-output {
133
+ flex-basis: 100%;
134
+ max-height: 14em;
135
+ overflow: auto;
136
+ margin: 0.6em 0 0;
137
+ padding: 0.6em 0.8em;
138
+ background: color-mix(in srgb, CanvasText 6%, transparent);
139
+ border-radius: 4px;
140
+ white-space: pre-wrap;
141
+ font-family: "SFMono-Regular", Menlo, Consolas, monospace;
142
+ font-size: 0.85rem;
143
+ }
144
+ #user-response-output:empty { display: none; }
145
+
146
+ @media print {
147
+ .report-header, .report-footer { position: static; }
148
+ thead th { position: static; }
149
+ button[data-action] { display: none; }
150
+ #user-response-output { display: none; }
151
+ }
@@ -0,0 +1,163 @@
1
+ /* Client-side glue for the okstra final-report HTML view.
2
+ *
3
+ * Responsibilities:
4
+ * 1. Collect <textarea> values for every <tr data-response-id> whose
5
+ * Status is open/answered (disabled rows are skipped automatically).
6
+ * 2. Serialise them into markdown whose bytes are IDENTICAL to
7
+ * scripts/okstra_ctl/report_views.py serialize_user_response.
8
+ * 3. Write the result to <pre id="user-response-output"> and offer a
9
+ * [Copy] button.
10
+ *
11
+ * The byte-identity contract is enforced by tests/test_report_views.py
12
+ * which spawns Node to execute buildUserResponseMarkdown and diffs the
13
+ * output against the Python function. If you edit the format here you
14
+ * MUST edit serialize_user_response too. The template at
15
+ * templates/reports/user-response.template.md documents the schema.
16
+ */
17
+
18
+ (function () {
19
+ "use strict";
20
+
21
+ function readRunMeta() {
22
+ var el = document.getElementById("run-meta");
23
+ if (!el) return {};
24
+ try {
25
+ return JSON.parse(el.textContent || "{}");
26
+ } catch (e) {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ function isoNowUtc() {
32
+ return new Date().toISOString().replace(/\.\d+Z$/, "Z");
33
+ }
34
+
35
+ function trimMultiline(s) {
36
+ return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
37
+ }
38
+
39
+ function collectEntries() {
40
+ var entries = [];
41
+ var rows = document.querySelectorAll("tr[data-response-id]");
42
+ for (var i = 0; i < rows.length; i++) {
43
+ var row = rows[i];
44
+ var ta = row.querySelector("textarea[data-response-id]");
45
+ if (!ta || ta.disabled) continue;
46
+ var value = trimMultiline(ta.value);
47
+ if (!value) continue;
48
+ entries.push({
49
+ responseId: row.getAttribute("data-response-id") || "",
50
+ kind: row.getAttribute("data-kind") || "",
51
+ value: value,
52
+ rationale: null,
53
+ });
54
+ }
55
+ return entries;
56
+ }
57
+
58
+ function buildUserResponseMarkdown(runMeta, entries, createdAt) {
59
+ var head =
60
+ "---\n" +
61
+ "task-key: " + (runMeta["task-key"] || "") + "\n" +
62
+ "task-type: " + (runMeta["task-type"] || "") + "\n" +
63
+ "seq: " + (runMeta["seq"] || "") + "\n" +
64
+ "source-report: " + (runMeta["source-report"] || "") + "\n" +
65
+ "created-by: user\n" +
66
+ "created-at: " + createdAt + "\n" +
67
+ "---\n" +
68
+ "\n" +
69
+ "# User Response\n";
70
+
71
+ if (!entries || entries.length === 0) {
72
+ return head + "\n_(No user responses recorded.)_\n";
73
+ }
74
+
75
+ var chunks = "";
76
+ for (var i = 0; i < entries.length; i++) {
77
+ var e = entries[i];
78
+ var chunk =
79
+ "\n## " + e.responseId + "\n" +
80
+ "- Kind: " + e.kind + "\n" +
81
+ "- Value:\n" +
82
+ " > " + trimMultiline(e.value) + "\n";
83
+ if (e.rationale) {
84
+ chunk += "- Rationale: " + trimMultiline(e.rationale) + "\n";
85
+ }
86
+ chunks += chunk;
87
+ }
88
+ return head + chunks;
89
+ }
90
+
91
+ function exportUserResponse() {
92
+ var runMeta = readRunMeta();
93
+ var entries = collectEntries();
94
+ var md = buildUserResponseMarkdown(runMeta, entries, isoNowUtc());
95
+ var out = document.getElementById("user-response-output");
96
+ if (out) out.textContent = md;
97
+ return md;
98
+ }
99
+
100
+ function copyUserResponse() {
101
+ var out = document.getElementById("user-response-output");
102
+ if (!out || !out.textContent) return;
103
+ var text = out.textContent;
104
+ if (navigator.clipboard && navigator.clipboard.writeText) {
105
+ navigator.clipboard.writeText(text).catch(function () {
106
+ fallbackCopy(text);
107
+ });
108
+ } else {
109
+ fallbackCopy(text);
110
+ }
111
+ }
112
+
113
+ function fallbackCopy(text) {
114
+ var ta = document.createElement("textarea");
115
+ ta.value = text;
116
+ ta.style.position = "fixed";
117
+ ta.style.opacity = "0";
118
+ document.body.appendChild(ta);
119
+ ta.select();
120
+ try { document.execCommand("copy"); } catch (e) {}
121
+ document.body.removeChild(ta);
122
+ }
123
+
124
+ function bind() {
125
+ var clickables = document.querySelectorAll("button[data-action]");
126
+ for (var i = 0; i < clickables.length; i++) {
127
+ var btn = clickables[i];
128
+ var action = btn.getAttribute("data-action");
129
+ if (action === "export-user-response") {
130
+ btn.addEventListener("click", exportUserResponse);
131
+ } else if (action === "copy-user-response") {
132
+ btn.addEventListener("click", copyUserResponse);
133
+ }
134
+ }
135
+ }
136
+
137
+ if (typeof window !== "undefined") {
138
+ if (document.readyState === "loading") {
139
+ document.addEventListener("DOMContentLoaded", bind);
140
+ } else {
141
+ bind();
142
+ }
143
+ // Expose for tests / debug.
144
+ window.okstraReportView = {
145
+ buildUserResponseMarkdown: buildUserResponseMarkdown,
146
+ collectEntries: collectEntries,
147
+ exportUserResponse: exportUserResponse,
148
+ };
149
+ }
150
+
151
+ // Node export for cross-impl byte-identity test. We expose on both
152
+ // CommonJS module.exports (works in `require()` callers) and
153
+ // globalThis (works under ESM where the parent uses `vm.runInThisContext`
154
+ // — see tests/test_report_views.py for the byte-identity harness).
155
+ if (typeof module !== "undefined" && module.exports) {
156
+ module.exports = { buildUserResponseMarkdown: buildUserResponseMarkdown };
157
+ }
158
+ if (typeof globalThis !== "undefined") {
159
+ globalThis.__okstraReportViewExports__ = {
160
+ buildUserResponseMarkdown: buildUserResponseMarkdown,
161
+ };
162
+ }
163
+ })();
@@ -0,0 +1,69 @@
1
+ <!-- single source of truth: scripts/okstra_ctl/report_views.py serialize_user_response -->
2
+ <!-- byte-identical client implementation: templates/reports/report.js buildUserResponseMarkdown -->
3
+
4
+ # User-Response Sidecar Template
5
+
6
+ 이 파일은 final-report HTML 의 **Export user response** 버튼이 만들어내는 markdown 의 표준 포맷을 정의합니다. 산출물은 `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` 경로에 저장됩니다. 원본 final-report MD 는 어떤 경우에도 머지하지 않습니다.
7
+
8
+ ## Frontmatter 스키마
9
+
10
+ ```yaml
11
+ task-key: <task-group>/<task-id>
12
+ task-type: <requirements-discovery | error-analysis | implementation-planning | implementation | final-verification | release-handoff>
13
+ seq: <3-digit zero-padded run sequence>
14
+ source-report: <project-relative path to the final-report .md the HTML was derived from>
15
+ created-by: user
16
+ created-at: <ISO 8601 UTC timestamp>
17
+ ```
18
+
19
+ ## Body 스키마
20
+
21
+ 본문은 `# User Response` 단일 H1 헤딩 아래, 각 응답을 `## <Response ID>` 헤딩으로 구분합니다. Response ID 는 final-report `## 5. Clarification Items` 표의 `ID` 컬럼(`C-NNN`)을 그대로 인용합니다.
22
+
23
+ 각 응답 블록의 schema:
24
+
25
+ ```markdown
26
+ ## <Response ID>
27
+ - Kind: <material | decision | data-point>
28
+ - Value:
29
+ > <multi-line value, trimmed>
30
+ - Rationale: <optional one-line rationale>
31
+ ```
32
+
33
+ 빈 응답 집합(사용자가 어떤 행도 채우지 않고 Export 를 누른 경우)은 다음 한 줄을 본문에 출력합니다:
34
+
35
+ ```markdown
36
+ _(No user responses recorded.)_
37
+ ```
38
+
39
+ ## Example
40
+
41
+ ```markdown
42
+ ---
43
+ task-key: demo/T-1
44
+ task-type: implementation-planning
45
+ seq: 003
46
+ source-report: runs/implementation-planning/reports/final-report-implementation-planning-003.md
47
+ created-by: user
48
+ created-at: 2026-05-17T10:00:00Z
49
+ ---
50
+
51
+ # User Response
52
+
53
+ ## C-001
54
+ - Kind: decision
55
+ - Value:
56
+ > (a) 일회성. 재발 없음.
57
+ - Rationale: 결제 로그 재확인 결과 동일 패턴 미관측.
58
+
59
+ ## C-003
60
+ - Kind: data-point
61
+ - Value:
62
+ > (prediction=0: 1,204) (prediction=1: 38)
63
+ ```
64
+
65
+ ## 호환성 규칙
66
+
67
+ - `Kind` 가 알 수 없는 값이면 폼은 `<textarea>` fallback 으로 렌더되며, 직렬화 시 받은 `Kind` 문자열을 그대로 보존합니다.
68
+ - `Status` 가 `resolved` 또는 `obsolete` 인 행은 HTML 폼이 `disabled` 로 렌더되어 Export 결과에서 자동 제외됩니다. 그 행을 다시 열려면 final-report 의 `Status` 를 `open` / `answered` 로 되돌리고 보고서를 재생성해야 합니다.
69
+ - Python (`scripts/okstra_ctl/report_views.py` `serialize_user_response`) 과 JavaScript (`templates/reports/report.js` `buildUserResponseMarkdown`) 의 출력은 **byte-identical** 이어야 합니다. `tests/test_report_views.py` 가 두 구현의 동일성을 검증합니다.
@@ -328,7 +328,7 @@ report_lines = [
328
328
  "| 항목 | 값 |",
329
329
  "|------|----|",
330
330
  "| Final Conclusion | validation fixture |",
331
- "| Verdict Token | `not-applicable` |",
331
+ "| Verdict Token | `accepted` |",
332
332
  "| Direction | `continue-investigation` |",
333
333
  "| Approval Required? | `no` |",
334
334
  "| Next Step | fixture |",
@@ -350,13 +350,50 @@ report_lines.extend(
350
350
  "| **전체 합계** | **`2`** | **`2`** | **`$0.02`** |",
351
351
  "| Codex/Gemini CLI 추가 비용 | | | `$0.00` |",
352
352
  "",
353
- "## Final Verdict",
354
- "- Validation fixture report generated.",
353
+ "## 2. Final Verdict",
354
+ "",
355
+ "| 항목 | 값 |",
356
+ "|------|----|",
357
+ "| Verdict Token | `accepted` |",
358
+ "",
359
+ "## 4.8 Final Verification Deliverables",
360
+ "",
361
+ "Source Implementation Report / Acceptance Blockers / Residual Risk / "
362
+ "Validation Evidence / Read-only Command Log / Routing Recommendation: "
363
+ "fixture stub.",
355
364
  ]
356
365
  )
357
366
  report_path.parent.mkdir(parents=True, exist_ok=True)
358
367
  report_path.write_text("\n".join(report_lines) + "\n")
359
368
 
369
+ # Phase 7 step 1.5 (BLOCKING) — render the slim/html sibling artifacts
370
+ # next to the final-report so validate-run.py's new report-views hook
371
+ # passes. The workflow validator's fixture predates that step; we
372
+ # materialise both files in-place using the same single-reference-point
373
+ # helper the CLI uses.
374
+ import os
375
+ WORKSPACE_ROOT = os.environ.get("OKSTRA_WORKSPACE_ROOT_FOR_FIXTURE", "")
376
+ if WORKSPACE_ROOT:
377
+ import sys as _sys
378
+ _sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
379
+ try:
380
+ from okstra_ctl.report_views import RunMeta, render_both_views
381
+ css = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.css").read_text(encoding="utf-8")
382
+ js = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.js").read_text(encoding="utf-8")
383
+ render_both_views(
384
+ report_path,
385
+ run_meta=RunMeta(
386
+ task_key=str(task_manifest.get("taskKey", "validation/fixture")),
387
+ task_type=str(task_manifest.get("taskType", "validation")),
388
+ seq="001",
389
+ source_report=report_path.name,
390
+ ),
391
+ css=css,
392
+ js=js,
393
+ )
394
+ except Exception as exc: # pragma: no cover — fixture path only
395
+ raise SystemExit(f"failed to render report views in fixture: {exc}")
396
+
360
397
  if final_status_path.exists():
361
398
  final_status_path.unlink()
362
399