okstra 0.155.0 → 0.157.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/docs/architecture.md +3 -1
  2. package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
  3. package/docs/project-structure-overview.md +7 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/prompts/profiles/_common-contract.md +1 -1
  7. package/runtime/python/okstra_ctl/clarification_items.py +3 -3
  8. package/runtime/python/okstra_ctl/render_final_report.py +31 -44
  9. package/runtime/python/okstra_ctl/report_contract.py +15 -0
  10. package/runtime/python/okstra_ctl/report_finalize.py +22 -3
  11. package/runtime/python/okstra_ctl/report_markdown.py +441 -0
  12. package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
  13. package/runtime/python/okstra_ctl/stage_map.py +203 -1
  14. package/runtime/python/okstra_ctl/wizard.py +1 -11
  15. package/runtime/python/okstra_project/state.py +14 -2
  16. package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
  17. package/runtime/templates/reports/final-report-v2.template.md +74 -10
  18. package/runtime/templates/reports/md/macros/sections.md +19 -0
  19. package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
  20. package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
  21. package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
  22. package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
  23. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
  24. package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
  25. package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
  26. package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
  27. package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
  28. package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
  29. package/runtime/templates/reports/schedule.template.md +166 -63
  30. package/runtime/validators/validate-run.py +19 -6
  31. package/runtime/validators/validate-schedule.py +94 -65
  32. package/src/commands/inspect/stage-map.mjs +6 -1
  33. package/src/commands/inspect/worker-liveness.mjs +15 -3
  34. package/src/commands/lifecycle/install.mjs +69 -4
  35. package/src/commands/lifecycle/uninstall.mjs +21 -35
  36. package/src/lib/install-assets.mjs +37 -0
@@ -22,11 +22,17 @@ taskType: "{{FM_TASK_TYPE}}"
22
22
 
23
23
  **<N> tasks total / estimated effort: <X.X> ~ <Y.Y> days (Effort sum)**
24
24
 
25
- | # | Task ID | Title | Category | Priority | Effort | Days | taskType | Risk | Phase |
26
- |---|---------|-------|----------|----------|--------|------|----------|------|-------|
27
- | 1 | <TASK-ID> | <Title> | <category> | <P0~P3> | <S/M/L/XL> | <range> | <taskType> | <risk> | <1/2/3> |
25
+ <!-- `Days` is the task's own range and MUST equal the sum of its Work Breakdown
26
+ stage rows. Append ` (est)` when the range was derived from `step_count`
27
+ rather than stated by the plan — which is every range today, because
28
+ implementation-planning does not estimate duration. `[NEEDS-PLANNING]` is
29
+ reserved for a task with NO planning report at all. -->
28
30
 
29
- **Effort distribution**: S × <n> / M × <n> / L × <n> / XL × <n>
31
+ | # | Task ID | Title | Category | Priority | Effort | Days | Risk |
32
+ |---|---------|-------|----------|----------|--------|------|------|
33
+ | 1 | <TASK-ID> | <Title> | <category> | <P0~P3> | <S/M/L/XL/XXL> | <X.X> ~ <Y.Y> (est) | <risk> |
34
+
35
+ **Effort distribution**: S × <n> / M × <n> / L × <n> / XL × <n> / XXL × <n>
30
36
  **Risk distribution**: Very Low × <n> / Low × <n> / Medium × <n> / Med-High × <n> / High × <n>
31
37
  **Impact by repo**: <repo1> (<n>) / <repo2> (<n>)
32
38
 
@@ -34,11 +40,13 @@ taskType: "{{FM_TASK_TYPE}}"
34
40
 
35
41
  ## Executive Summary
36
42
 
37
- <2-4 sentences — phase-classification summary + execution strategy>
43
+ <2-4 sentences — what this schedule covers and the execution strategy.>
44
+
45
+ <!-- One bullet per in-scope task, naming the stages actually selected. This is
46
+ the stage roster, not a priority bucket — At a Glance is sorted
47
+ Priority-first and that ordering is where priority lives. -->
38
48
 
39
- - **Phase 1 (Critical Fixes)**: <n> tasks — <summary>
40
- - **Phase 2 (Enhancements)**: <n> tasks — <summary>
41
- - **Phase 3 (Architecture)**: <n> tasks — <summary>
49
+ - **<TASK-ID>**: stages <n>–<m> selected (<total> steps) — <summary>
42
50
 
43
51
  ### Effort Sizing Criteria
44
52
 
@@ -48,31 +56,54 @@ taskType: "{{FM_TASK_TYPE}}"
48
56
  | **M** | 3-5 files, 1 repo, includes new tests, no DB changes | 2 - 3 |
49
57
  | **L** | 5-15 files, 2-3 repos, sequential deploy, may include package release | 3 - 5 |
50
58
  | **XL** | 15+ files, 3+ repos + infra, concurrent frontend changes, architecture migration | 5 - 10 |
51
- | **XXL** | needs further decomposition | 10 - |
59
+ | **XXL** | decomposed into stages; the day range is the stage sum | 10 - |
60
+
61
+ <!-- XXL has no finite upper bound in this table on purpose: an XXL task is
62
+ sized by its own stage decomposition, so its `Days` range comes from the
63
+ Work Breakdown sum and NOT from this row. Every other size takes its range
64
+ from here. -->
65
+
66
+ ### Priority & Risk Scale
67
+
68
+ <!-- Controlled vocabulary the At a Glance cells draw from. The schedule is read by people who never saw the source report,
69
+ so a `P0` or a `High` that is never defined is an opaque code with a
70
+ friendlier shape. Keep both tables verbatim. -->
71
+
72
+ | Priority | 기준 |
73
+ |----------|------|
74
+ | **P0** | 다른 작업을 막고 있거나 운영 중인 서비스에 직접 영향 — 이번 주기에 착수 |
75
+ | **P1** | 다음 주기 안에 착수해야 하며, 미루면 P0 로 올라갈 사유가 있음 |
76
+ | **P2** | 계획된 개선 — 일정 여유에 맞춰 배치 |
77
+ | **P3** | 가치는 인정되나 착수 시점이 정해지지 않음 |
78
+
79
+ | Risk | 기준 |
80
+ |------|------|
81
+ | **High** | 되돌림 비용이 크거나, 실패가 사용자에게 보이는 형태로 드러남 |
82
+ | **Med-High** | 회귀 가능성이 있고 감지 장치가 일부 구간에만 있음 |
83
+ | **Medium** | 표준 절차로 감지·복구 가능 |
84
+ | **Low** | 영향 범위가 좁고 즉시 되돌릴 수 있음 |
85
+ | **Very Low** | 실패해도 관측 가능한 영향이 없음 |
52
86
 
53
87
  ---
54
88
 
55
89
  ## Task Dependency Graph
56
90
 
57
- <!-- Use ONE of:
58
- (A) literal `_none_` when no edges exist (single-task scope or no extracted edges).
59
- (B) plain ``` fenced adjacency-list block. Each non-empty line is one of:
60
- - adjacency: `<TASK-ID> -> <TASK-ID>[, <TASK-ID>]*`
61
- - comment: `# <free prose>` (max one per group)
62
- - blank: group separator
63
- Example (B):
64
- ```
65
- DEV-1 -> DEV-2, DEV-3
66
- DEV-2 -> DEV-4
67
- # Phase 3 fan-out
68
- DEV-5 -> DEV-6
69
- ```
70
- Rules:
71
- - ASCII arrow `->` only; Unicode `→` is rejected so downstream tooling does not handle both.
72
- - Both endpoints MUST be `TASK-ID` literals (same identifiers as the At a Glance table). Free text is forbidden.
73
- - Fence MUST be plain ``` with NO language tag. ```mermaid / ```plantuml / ```graphviz / ```dot are all rejected.
74
- - If only one task is in scope (no dependencies), use Shape A — do NOT emit an empty fence. -->
75
- _none_
91
+ <!-- OPTIONAL — omit the whole section, heading included, when no task depends on
92
+ another. That is always the case for a single-task schedule, and a section
93
+ whose entire body says "the relation is in that other table" is noise the
94
+ reader steps over. Stage order is NEVER drawn here: the Work Breakdown's
95
+ `Depends On` column owns it.
96
+ When two or more tasks are coupled, render a plain ``` fenced adjacency
97
+ list, `<TASK-ID> -> <TASK-ID>[, …]`, one edge per line, `# <prose>`
98
+ comments allowed. ASCII `->` only; Unicode `→` is rejected. The fence takes
99
+ no language tag — ```mermaid / ```plantuml / ```graphviz / ```dot are all
100
+ rejected. -->
101
+
102
+ ```
103
+ DEV-1 -> DEV-2, DEV-3
104
+ # release order
105
+ DEV-2 -> DEV-4
106
+ ```
76
107
 
77
108
  ---
78
109
 
@@ -82,35 +113,56 @@ _none_
82
113
  If included, MUST use this exact heading literal `## Gantt Chart`.
83
114
  Render as ASCII inside a fenced ``` block with NO language tag.
84
115
  Mermaid / PlantUML / Graphviz are forbidden.
85
- The axis is RELATIVE DAY-COUNTS (Day 1 / J1, …) — never calendar dates.
116
+
117
+ GEOMETRY — the validator checks these, they are not decoration:
118
+ - One column = 0.5 day. This is what lets a 2.5-day stage draw honestly.
119
+ - The axis starts at Day 1 and its last tick is the smallest multiple of 5
120
+ that covers the schedule's upper bound. Do not draw empty months.
121
+ - A row's bar starts at the cumulative LOWER bound of everything it waits
122
+ on, and runs `lower / 0.5` filled cells `█` then `(upper - lower) / 0.5`
123
+ open cells `░`.
124
+ - Row label is `Stage <n>` for a single-task schedule, spelled out so the
125
+ reader never cross-references an abbreviation. With 2+ tasks, prefix the
126
+ task-id: `<TASK-ID> Stage <n>`.
127
+ - A row is a label and a bar, with NOTHING after it. No `days=` (the Work
128
+ Breakdown's Days column owns those numbers and the validator compares the
129
+ bar against it), and no per-row `! crit` / `est` markers — when every row
130
+ carries the same marker it distinguishes nothing.
86
131
  If day estimates are missing, omit this whole section and replace it with one blockquote: `> _Gantt Chart omitted: <reason>_`. -->
87
132
 
133
+ > 한 칸은 반나절이다. `█` 는 하한 일수만큼, `░` 는 거기서 상한까지의 여유만큼 그린다.
134
+
88
135
  ```
89
- Day: 1 5 10 15 20 25 30
90
- | | | | | | |
91
- Phase 1
92
- <TASK-ID>/S<stage-number> ██████ ! crit days=<lower>~<upper>
93
- <TASK-ID>/S<stage-number> ████████ days=<lower>~<upper>
94
- Phase 2
95
- <TASK-ID>/S<stage-number> ██████░░ est days=<lower>~<upper>
96
- Phase 3
97
- <TASK-ID>/S<stage-number> ████ (after <TASK-ID>/S<stage-number>) days=<lower>~<upper>
136
+ Day: 1 5 10 15
137
+ | | | |
138
+ Stage 1 ████░░
139
+ Stage 2 ████░░
140
+ Stage 3 ██████░░░░
98
141
  ```
99
142
 
100
- > The axis is in **relative day-counts** (Day 1 = Phase 1 start). Legend: `! crit` = critical path, `est` = estimated allocation, `█` = confirmed span, `░` = upper-bound / uncertain span.
101
-
102
143
  ---
103
144
 
104
- ## Phase 1: Critical Fixes
145
+ ## Task Details
146
+
147
+ <!-- One `### <n>. <Title>` block per in-scope task, numbered from 1 in At a
148
+ Glance order — which is Priority-first, so that order carries priority.
149
+ There are no phase buckets and no separate priority table; stages carry
150
+ sequence.
151
+
152
+ The heading is the work's NAME, not its task-id. A task-id is a machine key
153
+ (`nestjs-migration-nlpvibe-to-nestjs-and-org-standard-structure`); printing
154
+ it as the reader's heading makes them decode a slug to learn what the work
155
+ is. The id stays in the At a Glance row, and the block binds to that row by
156
+ its number — which is also what the validator matches on. -->
105
157
 
106
- ### 1-1. <TASK-ID> — <Title>
158
+ ### 1. <Title>
107
159
 
108
160
  | Item | Detail |
109
161
  |------|--------|
110
162
  | **Category** | <category> |
111
163
  | **Priority** | <P0~P3> |
112
- | **Effort** | **<S/M/L/XL>** (<scope summary>) |
113
- | **Status** | <taskType> / <currentPhase> |
164
+ | **Effort** | **<S/M/L/XL/XXL>** (<scope summary>) |
165
+ | **Status** | <작업이 지금 어느 단계인지, 사람 말로> |
114
166
  | **Risk** | <risk> |
115
167
  | **Scope** | <files / repos summary> |
116
168
  | **Repo** | <repos> |
@@ -119,56 +171,107 @@ Phase 3
119
171
 
120
172
  **Solution**: <…>
121
173
 
174
+ <!-- Everything below this line is transcribed from the planning report the CLI
175
+ resolved — `okstra stage-map <task-key> --json` carries each stage's
176
+ `acceptance` / `exitContract` / `sliceValue` and the task-level
177
+ `rollbackStrategy` / `validationChecklist` / `dependencyMigrationRisk`
178
+ rows. Transcribe those rows; do not re-summarise the report body. -->
179
+
122
180
  **Work Breakdown**:
123
181
 
182
+ <!-- `Depends On` spells the stage out: `None`, `Stage 2`, `Stage 1 (done)`.
183
+ A bare number makes the reader match a digit against the Stage column of
184
+ the same table. -->
185
+
124
186
  | Stage | Title | Steps | Depends On | Days |
125
187
  |---:|---|---:|---|---:|
126
- | 2 | Build adapter | 3 | 1 (done) | 2.0 ~ 3.0 |
127
- | 3 | Wire consumer | 2 | 2 | 1.0 ~ 2.0 |
188
+ | 2 | Build adapter | 3 | Stage 1 (done) | 2.0 ~ 3.0 |
189
+ | 3 | Wire consumer | 2 | Stage 2 | 1.0 ~ 2.0 |
128
190
 
129
- **Verification Commands**:
130
- ```bash
131
- <commands>
132
- ```
191
+ <!-- One `#### Stage <n> — <title>` per SELECTED stage, in Work Breakdown order.
133
192
 
134
- **Rollback**: <…>
193
+ `Steps` comes from that stage's `stepwiseExecution`: what the step does
194
+ (keep a leading RED:/GREEN: marker), the files it touches, and what the
195
+ executor should see when it succeeds — the step's own `expected`, written
196
+ as an outcome they can check, not pasted.
135
197
 
136
- ---
198
+ `Exit criteria` is one line from `exitContract`: when the stage is done.
137
199
 
138
- ## Phase 2: Enhancements
200
+ Do NOT render `sliceValue`. It argues why the plan sliced the work this
201
+ way, which is a reviewer's question; an implementer gets nothing from it.
202
+ Do NOT render `acceptance` next to `exitContract` — they restate each
203
+ other, `exitContract` being the wider of the two. -->
139
204
 
140
- _none_ <!-- or repeat per-task block -->
205
+ #### Stage 2 — Build adapter
141
206
 
142
- ---
207
+ **Steps**:
143
208
 
144
- ## Phase 3: Architecture
209
+ | Step | 작업 | 대상 | 기대 결과 |
210
+ |---:|---|---|---|
211
+ | 1 | RED: <무엇을 왜 하는지> | `<files>` | <성공했을 때 보이는 것> |
212
+ | 2 | GREEN: <무엇을 왜 하는지> | `<files>` | <성공했을 때 보이는 것> |
145
213
 
146
- _none_ <!-- or repeat per-task block -->
214
+ **Exit criteria**: <이 stage 가 끝났다고 판정하는 상태>
147
215
 
148
- ---
216
+ **Verification Commands**:
217
+
218
+ <!-- One row per `validationChecklist` entry whose stage is in scope, SORTED
219
+ `pre` → `mid` → `post`. Source order interleaves them, and a reader working
220
+ down the table then runs a precondition after the post checks it gates.
221
+ Introduce the three values in one line above the table — they are
222
+ controlled vocabulary like `P0`, and just as opaque undefined.
223
+ Do NOT carry the source row's `id`: a schedule reader has no access to the
224
+ report those codes index, and the self-contained rule rejects them. -->
225
+
226
+ `Phase` 는 이 검사를 언제 돌리는지를 뜻한다 — **pre**: 첫 stage 착수 전, **mid**: 매 stage 진행 중 상시, **post**: 각 stage 종료 조건으로.
149
227
 
150
- ## Execution Priority Matrix
228
+ | Phase | Check | Command / Observation | Expected |
229
+ |-------|-------|-----------------------|----------|
230
+ | pre | <check> | `<commandOrObservation>` | <expectedOutcome> |
151
231
 
152
- | Priority | Task | Effort | Risk | Next Action |
153
- |----------|------|--------|------|-------------|
232
+ **Rollback**:
233
+
234
+ <!-- One row per `rollbackStrategy` entry, in source order. The source `id` is
235
+ dropped for the same reason as the verification table. -->
236
+
237
+ | Stage | Action | Trigger Signal | Verification |
238
+ |------:|--------|----------------|--------------|
239
+ | 1 | <action> | <triggerSignal> | <verificationMethod> |
154
240
 
155
241
  ---
156
242
 
157
243
  ## Cross-Task Dependencies & Shared Concerns
158
244
 
245
+ <!-- When the planning report carries `crossProjectDependencies`, render one row
246
+ per entry before any prose. Prose covers only what those rows do not. -->
247
+
248
+ | Direction | Project | Required Work | Verification Signal |
249
+ |-----------|---------|---------------|---------------------|
250
+ | upstream-precondition | <project> | <requiredWork> | <verificationSignal> |
251
+
159
252
  <free-form prose — inter-task overlap, shared packages, release order>
160
253
 
161
254
  ---
162
255
 
163
256
  ## Risk Mitigation Strategy
164
257
 
165
- 1. <…>
166
- 2. <…>
258
+ <!-- When the planning report carries `dependencyMigrationRisk`, render one row
259
+ per entry. Additional numbered prose items may follow for risks the report
260
+ does not carry. -->
261
+
262
+ | Kind | Item | Impact | Mitigation |
263
+ |------|------|--------|------------|
264
+ | order | <item> | <impact> | <mitigation> |
167
265
 
168
266
  ---
169
267
 
170
268
  ## Recommended Immediate Actions
171
269
 
270
+ <!-- The single place the schedule says what to do next. With several tasks in
271
+ scope, lead each task's first action with its `<TASK-ID>` and keep them in
272
+ At a Glance order — that ordering is already Priority-first, so a separate
273
+ priority table would restate the At a Glance row and this list at once. -->
274
+
172
275
  - <action>
173
276
  - <action>
174
277
 
@@ -1045,6 +1045,19 @@ _V2_AI_HANDOFF_HEADINGS = (
1045
1045
  )
1046
1046
 
1047
1047
 
1048
+ # `humanSummary` / `userNarrative` belong to the human HTML. The AI Markdown
1049
+ # renders field names as headings or bold labels
1050
+ # (scripts/okstra_ctl/report_markdown.py `humanise`), so this matches the two
1051
+ # forms the renderer can emit rather than the raw JSON key — a key name in
1052
+ # quotes no longer appears anywhere in the Markdown. Anchored to the start of a
1053
+ # line so the same words inside worker prose are not a violation.
1054
+ _V2_HUMAN_ONLY_RENDERED_RE = re.compile(
1055
+ r"^(?:#{2,6}[ \t]+(?P<heading>Human Summary|User Narrative)[ \t]*$"
1056
+ r"|[ \t]*-[ \t]+\*\*(?P<label>Human Summary|User Narrative)\*\*[ \t]*:)",
1057
+ re.MULTILINE,
1058
+ )
1059
+
1060
+
1048
1061
  def _validate_v2_ai_handoff(content: str, failures: list[str]) -> None:
1049
1062
  positions: list[int] = []
1050
1063
  for heading in _V2_AI_HANDOFF_HEADINGS:
@@ -1063,12 +1076,12 @@ def _validate_v2_ai_handoff(content: str, failures: list[str]) -> None:
1063
1076
  "schema-v2 AI handoff markdown heading order does not match "
1064
1077
  "templates/reports/final-report-v2.template.md."
1065
1078
  )
1066
- for human_only_field in ('"humanSummary"', '"userNarrative"'):
1067
- if human_only_field in content:
1068
- failures.append(
1069
- "schema-v2 AI handoff markdown contains human-only field "
1070
- f"{human_only_field}; render it only in the task-specific HTML."
1071
- )
1079
+ for match in _V2_HUMAN_ONLY_RENDERED_RE.finditer(content):
1080
+ field = match.group("heading") or match.group("label")
1081
+ failures.append(
1082
+ "schema-v2 AI handoff markdown contains human-only field "
1083
+ f"{field!r}; render it only in the task-specific HTML."
1084
+ )
1072
1085
  if _READING_CONFIRMATION_HEADING_RE.search(content) is not None:
1073
1086
  failures.append(
1074
1087
  "final report contains a `## 0. Reading Confirmation` heading — "
@@ -36,21 +36,26 @@ from okstra_ctl.schedule_semantics import ( # noqa: E402
36
36
  REQUIRED_SECTIONS_IN_ORDER: list[str] = [
37
37
  "## At a Glance",
38
38
  "## Executive Summary",
39
- "## Task Dependency Graph",
40
- "## Phase 1: Critical Fixes",
41
- "## Phase 2: Enhancements",
42
- "## Phase 3: Architecture",
43
- "## Execution Priority Matrix",
39
+ "## Task Details",
44
40
  "## Cross-Task Dependencies & Shared Concerns",
45
41
  "## Risk Mitigation Strategy",
46
42
  "## Recommended Immediate Actions",
47
43
  ]
48
44
 
45
+ # Both are omitted when they carry nothing: a single-task schedule has no
46
+ # cross-task edge to draw, and a section whose whole body says "look at the
47
+ # other table" is noise the reader has to step over.
49
48
  OPTIONAL_GRAPHIC_SECTIONS_IN_ORDER: list[str] = [
49
+ "## Task Dependency Graph",
50
50
  "## Gantt Chart",
51
51
  ]
52
52
 
53
53
  REQUIRED_EXEC_SUMMARY_SUBSECTION = "### Effort Sizing Criteria"
54
+ # `P0` and `High` are controlled vocabulary the tables draw from, and a reader who
55
+ # never saw the source report cannot rank work by a code the document never
56
+ # defines — an opaque code with a friendlier shape.
57
+ REQUIRED_SCALE_SUBSECTION = "### Priority & Risk Scale"
58
+ REQUIRED_SCALE_HEADERS = ("| Priority | 기준 |", "| Risk | 기준 |")
54
59
 
55
60
  REQUIRED_TASK_FIELDS = [
56
61
  "**Category**",
@@ -93,10 +98,21 @@ FORBIDDEN_FRENCH_BODY_RE = re.compile(
93
98
  ALLOWED_EFFORT = {"S", "M", "L", "XL", "XXL"}
94
99
  ALLOWED_PRIORITY = {"P0", "P1", "P2", "P3"}
95
100
  ALLOWED_RISK = {"Very Low", "Low", "Medium", "Med-High", "High"}
96
- ALLOWED_PHASE = {"1", "2", "3"}
97
101
 
98
102
  # Per-task block sub-section labels in REQUIRED order. Each label is matched
99
103
  # at the start of a line as `**Label**:` or as a `####` heading for the last.
104
+ # A stage block states what the stage does and when it is finished. `Value` was
105
+ # the plan arguing its own slicing at a reviewer; `Acceptance` restated
106
+ # `Exit contract`. Both are refused by label so a regenerated schedule cannot
107
+ # quietly bring them back.
108
+ STAGE_BLOCK_RE = re.compile(r"^####\s+Stage\s+(\d+)\s+—", re.MULTILINE)
109
+ STAGE_REQUIRED_LABELS = ("**Steps**:", "**Exit criteria**:")
110
+ STAGE_FORBIDDEN_LABELS = (
111
+ ("**Value**:", "the plan's justification for its own slicing"),
112
+ ("**Acceptance**:", "a restatement of Exit criteria"),
113
+ ("**Exit contract**:", "renamed to `**Exit criteria**:`"),
114
+ )
115
+
100
116
  PER_TASK_SUBSECTIONS_IN_ORDER = [
101
117
  "**Problem**:",
102
118
  "**Solution**:",
@@ -194,8 +210,8 @@ def _validate_format(path: Path) -> list[str]:
194
210
  violations.append("missing `> Source: okstra <mode> …` metadata line")
195
211
 
196
212
  # 3. Required sections present and in order. The optional `## Gantt Chart`
197
- # section, if present, MUST sit between Task Dependency Graph and
198
- # Phase 1: Critical Fixes.
213
+ # sections, if present, MUST sit between Executive Summary and
214
+ # Task Details.
199
215
  section_positions: dict[str, int] = {}
200
216
  optional_positions: dict[str, int] = {}
201
217
  for idx, line in enumerate(visible_lines):
@@ -230,15 +246,31 @@ def _validate_format(path: Path) -> list[str]:
230
246
  + "\n ".join(ordered_actual)
231
247
  )
232
248
 
233
- # Optional sections must sit between Task Dependency Graph and Phase 1
234
- dep_idx = section_positions["## Task Dependency Graph"]
235
- phase1_idx = section_positions["## Phase 1: Critical Fixes"]
249
+ # Optional sections sit between Executive Summary and Task Details
250
+ summary_idx = section_positions["## Executive Summary"]
251
+ details_idx = section_positions["## Task Details"]
236
252
  for opt_name, opt_idx in optional_positions.items():
237
- if not (dep_idx < opt_idx < phase1_idx):
253
+ if not (summary_idx < opt_idx < details_idx):
238
254
  violations.append(
239
255
  f"optional section {opt_name!r} is misplaced — must appear between "
240
- "'## Task Dependency Graph' and '## Phase 1: Critical Fixes'"
256
+ "'## Executive Summary' and '## Task Details'"
257
+ )
258
+ # 3b. Priority / Risk scale definitions
259
+ if sum(
260
+ line.rstrip() == REQUIRED_SCALE_SUBSECTION for line in visible_lines
261
+ ) != 1:
262
+ violations.append(
263
+ f"missing subsection {REQUIRED_SCALE_SUBSECTION!r} — `P0` and "
264
+ "`High` are undefined codes without it"
265
+ )
266
+ else:
267
+ for header in REQUIRED_SCALE_HEADERS:
268
+ if header not in text:
269
+ violations.append(
270
+ f"{REQUIRED_SCALE_SUBSECTION} requires the header literal "
271
+ f"{header!r}"
241
272
  )
273
+
242
274
  # 4. Executive Summary subsection
243
275
  effort_heading_count = sum(
244
276
  line.rstrip() == REQUIRED_EXEC_SUMMARY_SUBSECTION
@@ -271,7 +303,7 @@ def _validate_format(path: Path) -> list[str]:
271
303
 
272
304
  # 7. Per-task field labels — every per-task heading (### N-i.) MUST be followed
273
305
  # (within the next 60 lines) by all required field labels.
274
- task_heading_re = re.compile(r"^###\s+\d+-\d+\.\s+", re.MULTILINE)
306
+ task_heading_re = re.compile(r"^###\s+\d+\.\s+", re.MULTILINE)
275
307
  for m in task_heading_re.finditer(text):
276
308
  start = m.start()
277
309
  # find the next ### or ## heading
@@ -285,41 +317,14 @@ def _validate_format(path: Path) -> list[str]:
285
317
  )
286
318
 
287
319
  # 8. Item table header literal
288
- if "| Item | Detail |" not in text and any(
289
- h.startswith("## Phase") for h in section_positions
290
- ):
291
- # only flag if there is at least one phase with a per-task block
292
- if task_heading_re.search(text):
293
- violations.append(
294
- "per-task tables must use header `| Item | Detail |` (literal English)"
295
- )
296
-
297
- # 9. Checklist tables MUST carry a leading `Done` column with markdown
298
- # checkboxes. The check is a header-presence test — we require the
299
- # canonical header literal to appear in the section's text block.
300
- checklist_header_specs = [
301
- (
302
- "## Execution Priority Matrix",
303
- ["| Priority | Task | Effort | Risk | Next Action |"],
304
- "Execution Priority Matrix table must use header "
305
- "`| Priority | Task | Effort | Risk | Next Action |` "
306
- "(client-facing schedule — no Done/Ready/Blocking columns)",
307
- ),
308
- ]
309
- for section_name, expected_substrings, message in checklist_header_specs:
310
- if section_name not in section_positions:
311
- continue
312
- start = section_positions[section_name]
313
- # slice from this heading to the next `## ` (string offsets, not line idx)
314
- rest = "\n".join(lines[start + 1:])
315
- next_h = re.search(r"^##\s", rest, re.MULTILINE)
316
- section_text = rest[: next_h.start()] if next_h else rest
317
- if not any(sub in section_text for sub in expected_substrings):
318
- violations.append(message)
320
+ if "| Item | Detail |" not in text and task_heading_re.search(text):
321
+ violations.append(
322
+ "per-task tables must use header `| Item | Detail |` (literal English)"
323
+ )
319
324
 
320
- # Forbid client-leaking artifacts: Consolidated User Decision Checklist /
321
- # per-task 사용자 확인 필요 항목 — schedule is for client delivery, blocking
322
- # / approval items must NOT surface.
325
+ # 9. Forbid client-leaking artifacts: Consolidated User Decision Checklist /
326
+ # per-task 사용자 확인 필요 항목 — schedule is for client delivery, blocking
327
+ # / approval items must NOT surface.
323
328
  if "## Consolidated User Decision Checklist" in text:
324
329
  violations.append(
325
330
  "`## Consolidated User Decision Checklist` is forbidden — schedule "
@@ -333,6 +338,28 @@ def _validate_format(path: Path) -> list[str]:
333
338
  "report instead"
334
339
  )
335
340
 
341
+ # 9b. Stage blocks: Steps + Exit criteria, and neither reviewer-facing label.
342
+ stage_headings = list(STAGE_BLOCK_RE.finditer(text))
343
+ for index, heading in enumerate(stage_headings):
344
+ end = (
345
+ stage_headings[index + 1].start()
346
+ if index + 1 < len(stage_headings)
347
+ else len(text)
348
+ )
349
+ block = text[heading.start():end]
350
+ stage_no = heading.group(1)
351
+ for label in STAGE_REQUIRED_LABELS:
352
+ if label not in block:
353
+ violations.append(
354
+ f"Stage {stage_no} block is missing {label!r}"
355
+ )
356
+ for label, why in STAGE_FORBIDDEN_LABELS:
357
+ if label in block:
358
+ violations.append(
359
+ f"Stage {stage_no} block carries forbidden {label!r} — "
360
+ f"{why}; a stage block is Steps plus Exit criteria"
361
+ )
362
+
336
363
  # 10. Days total format inside At a Glance: `**N tasks total / estimated effort: X.X ~ Y.Y days (Effort sum)**`
337
364
  if "## At a Glance" in section_positions:
338
365
  start = section_positions["## At a Glance"]
@@ -350,15 +377,14 @@ def _validate_format(path: Path) -> list[str]:
350
377
  r"^\|\s*\d+\s*\|.*$", body, re.MULTILINE
351
378
  ):
352
379
  cells = split_pipe_row(row.group(0))
353
- # header: # | Task ID | Title | Category | Priority | Effort | Days | taskType | Risk | Phase
354
- if len(cells) < 10:
380
+ # header: # | Task ID | Title | Category | Priority | Effort | Days | Risk
381
+ if len(cells) < 8:
355
382
  continue
356
383
  priority = re.sub(r"\*+", "", cells[4]).strip()
357
384
  effort = re.sub(r"\*+", "", cells[5]).strip()
358
385
  # `Effort` cell may contain extra hint like `L (5-15 files)`; pick the leading token
359
386
  effort_token = effort.split()[0] if effort else ""
360
- risk = re.sub(r"\*+", "", cells[8]).strip()
361
- phase = re.sub(r"\*+", "", cells[9]).strip()
387
+ risk = re.sub(r"\*+", "", cells[7]).strip()
362
388
  task_id = cells[1]
363
389
  if priority and priority not in ALLOWED_PRIORITY:
364
390
  violations.append(
@@ -375,11 +401,6 @@ def _validate_format(path: Path) -> list[str]:
375
401
  f"At a Glance row for {task_id!r}: Risk {risk!r} "
376
402
  f"not in allowed set {sorted(ALLOWED_RISK)}"
377
403
  )
378
- if phase and phase not in ALLOWED_PHASE:
379
- violations.append(
380
- f"At a Glance row for {task_id!r}: Phase {phase!r} "
381
- f"not in allowed set {sorted(ALLOWED_PHASE)}"
382
- )
383
404
 
384
405
  # 12. Gantt Chart content checks — forbid DSL fences, force relative-day axis
385
406
  if "## Gantt Chart" in optional_positions:
@@ -413,9 +434,9 @@ def _validate_format(path: Path) -> list[str]:
413
434
  "(`Day 1`, `J1`, …)"
414
435
  )
415
436
 
416
- # 12b. Task Dependency Graph content check.
417
- if "## Task Dependency Graph" in section_positions:
418
- start = section_positions["## Task Dependency Graph"]
437
+ # 12b. Task Dependency Graph content check (the section is optional).
438
+ if "## Task Dependency Graph" in optional_positions:
439
+ start = optional_positions["## Task Dependency Graph"]
419
440
  rest = "\n".join(lines[start + 1:])
420
441
  next_h = re.search(r"^##\s", rest, re.MULTILINE)
421
442
  body = (rest[: next_h.start()] if next_h else rest).strip()
@@ -442,9 +463,15 @@ def _validate_format(path: Path) -> list[str]:
442
463
  f"`## Task Dependency Graph` contains forbidden DSL fence "
443
464
  f"{forbidden!r} — only ASCII adjacency lines are allowed"
444
465
  )
466
+ # A node is a TASK-ID. Stage order is deliberately NOT drawn here —
467
+ # the Work Breakdown's `Depends On` column already carries it, and
468
+ # a second arrow-list rendering makes the reader cross-reference one
469
+ # table against another to learn nothing new. A stage node would
470
+ # also reintroduce the `S<n>` abbreviation the schedule spells out
471
+ # everywhere else.
472
+ node = r"[A-Za-z][A-Za-z0-9_-]*"
445
473
  adjacency_re = re.compile(
446
- r"^[A-Za-z][A-Za-z0-9_-]*\s*->\s*"
447
- r"[A-Za-z][A-Za-z0-9_-]*(?:\s*,\s*[A-Za-z][A-Za-z0-9_-]*)*\s*$"
474
+ rf"^{node}\s*->\s*{node}(?:\s*,\s*{node})*\s*$"
448
475
  )
449
476
  for line_no, raw in enumerate(inner.splitlines(), start=1):
450
477
  ln = raw.rstrip()
@@ -461,8 +488,10 @@ def _validate_format(path: Path) -> list[str]:
461
488
  if not adjacency_re.match(ln.strip()):
462
489
  violations.append(
463
490
  f"`## Task Dependency Graph` line {line_no} is not a valid "
464
- "adjacency line: expected `<TASK-ID> -> <TASK-ID>[, <TASK-ID>]*`; "
465
- f"got {ln.strip()!r}"
491
+ "adjacency line: expected `<TASK-ID> -> <TASK-ID>[, <TASK-ID>]*` "
492
+ "— this graph carries cross-task edges only, and stage "
493
+ "order belongs to the Work Breakdown's `Depends On` "
494
+ f"column; got {ln.strip()!r}"
466
495
  )
467
496
  elif no_data_marker not in body:
468
497
  violations.append(
@@ -472,7 +501,7 @@ def _validate_format(path: Path) -> list[str]:
472
501
  )
473
502
 
474
503
  # 13. Per-task block sub-section order check
475
- task_heading_re = re.compile(r"^###\s+\d+-\d+\.\s+", re.MULTILINE)
504
+ task_heading_re = re.compile(r"^###\s+\d+\.\s+", re.MULTILINE)
476
505
  headings = list(task_heading_re.finditer(text))
477
506
  for idx, m in enumerate(headings):
478
507
  block_start = m.start()
@@ -7,7 +7,12 @@ Usage:
7
7
  okstra stage-map <task-key> --cwd <dir> Resolve PROJECT_ROOT from <dir>
8
8
  okstra stage-map <task-key> --project <dir> Use <dir> directly as PROJECT_ROOT
9
9
 
10
- Output: JSON { ok, taskKey, taskRoot, state, sourcePlanPath, stages:[{stage_number,title,depends_on,step_count}], doneStages:[int] }.
10
+ Output: JSON { ok, taskKey, taskRoot, state, sourcePlanPath, stages:[…], doneStages:[int], planning:{…} }.
11
+ Each stage row carries stage_number, title, depends_on, step_count and exit_contract_summary; a
12
+ schema-v2 report adds that stage's sliceValue, acceptance and exitContract.
13
+ planning carries the report's task-level rows — rollbackStrategy, validationChecklist,
14
+ crossProjectDependencies, dependencyMigrationRisk, recommendedOption — so a consumer renders the
15
+ plan instead of re-summarising the report body. Both are absent for a schema-v1 report.
11
16
  stages is [] when no implementation-planning Stage Map exists.
12
17
  `;
13
18