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.
- package/docs/architecture.md +3 -1
- package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
- package/docs/project-structure-overview.md +7 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/python/okstra_ctl/clarification_items.py +3 -3
- package/runtime/python/okstra_ctl/render_final_report.py +31 -44
- package/runtime/python/okstra_ctl/report_contract.py +15 -0
- package/runtime/python/okstra_ctl/report_finalize.py +22 -3
- package/runtime/python/okstra_ctl/report_markdown.py +441 -0
- package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
- package/runtime/python/okstra_ctl/stage_map.py +203 -1
- package/runtime/python/okstra_ctl/wizard.py +1 -11
- package/runtime/python/okstra_project/state.py +14 -2
- package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
- package/runtime/templates/reports/final-report-v2.template.md +74 -10
- package/runtime/templates/reports/md/macros/sections.md +19 -0
- package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
- package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
- package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
- package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
- package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
- package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
- package/runtime/templates/reports/schedule.template.md +166 -63
- package/runtime/validators/validate-run.py +19 -6
- package/runtime/validators/validate-schedule.py +94 -65
- package/src/commands/inspect/stage-map.mjs +6 -1
- package/src/commands/inspect/worker-liveness.mjs +15 -3
- package/src/commands/lifecycle/install.mjs +69 -4
- package/src/commands/lifecycle/uninstall.mjs +21 -35
- 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
-
|
|
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** |
|
|
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
|
-
<!--
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
##
|
|
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
|
|
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** |
|
|
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
|
-
|
|
130
|
-
```bash
|
|
131
|
-
<commands>
|
|
132
|
-
```
|
|
191
|
+
<!-- One `#### Stage <n> — <title>` per SELECTED stage, in Work Breakdown order.
|
|
133
192
|
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
#### Stage 2 — Build adapter
|
|
141
206
|
|
|
142
|
-
|
|
207
|
+
**Steps**:
|
|
143
208
|
|
|
144
|
-
|
|
209
|
+
| Step | 작업 | 대상 | 기대 결과 |
|
|
210
|
+
|---:|---|---|---|
|
|
211
|
+
| 1 | RED: <무엇을 왜 하는지> | `<files>` | <성공했을 때 보이는 것> |
|
|
212
|
+
| 2 | GREEN: <무엇을 왜 하는지> | `<files>` | <성공했을 때 보이는 것> |
|
|
145
213
|
|
|
146
|
-
|
|
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
|
-
|
|
228
|
+
| Phase | Check | Command / Observation | Expected |
|
|
229
|
+
|-------|-------|-----------------------|----------|
|
|
230
|
+
| pre | <check> | `<commandOrObservation>` | <expectedOutcome> |
|
|
151
231
|
|
|
152
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
|
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
|
-
#
|
|
198
|
-
#
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
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 (
|
|
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
|
-
"'##
|
|
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
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
#
|
|
322
|
-
#
|
|
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 |
|
|
354
|
-
if len(cells) <
|
|
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[
|
|
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
|
|
418
|
-
start =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:[
|
|
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
|
|