okstra 0.151.1 → 0.152.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/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -84,8 +84,12 @@
|
|
|
84
84
|
- The plan lives in `data.json` under `implementationPlanning`, and `schemas/final-report-v2.0.schema.json` requires every one of these keys: `optionCandidates`, `tradeoffMatrix`, `recommendedOption`, `stageMap`, `stages`, `dependencyMigrationRisk`, `validationChecklist`, `rollbackStrategy`, `requirementCoverage`, `planBodyVerification`, `crossProjectDependencies`, `decisionDrafts`, `skippedAdrCandidates`, `variationPointAnalysis`, `userNarrative`. A missing block fails schema validation; there is nothing to satisfy by naming a heading. (Approval is not a body section — it is the YAML frontmatter `approved` field.)
|
|
85
85
|
- Each `stages[]` entry requires `stage`, `title`, `sliceValue`, `acceptance`, `carryIn`, `stepwiseExecution` (1–6 rows), `exitContract`, and `stageValidation`. Each `stageMap[]` row requires `stage`, `title`, `dependsOn`, `stepCount`, `exitContractSummary`.
|
|
86
86
|
- Beyond the schema, `validators/validate-run.py` reads the same data.json for `_validate_planning_conformance_declared`, `_validate_end_state_coverage`, `_validate_requirement_provenance`, `_validate_stage_has_requirement`, and `_validate_plan_body_state_file`. These run for every planning report regardless of schema version.
|
|
87
|
-
- **Do not chase English heading substrings.** `PLANNING_REQUIRED_SECTIONS` and the
|
|
88
|
-
|
|
87
|
+
- **Do not chase English heading substrings.** `PLANNING_REQUIRED_SECTIONS` and the Markdown scan in `collect_validation_errors` live inside `validate_phase_boundary`, which returns immediately when `schemaVersion == "2.0"` — they gate historical v1 Markdown only. The v2 AI-handoff template renders nine headings and serialises the plan as JSON beneath them, so those substrings cannot appear, and a report is not defective for lacking them.
|
|
88
|
+
- Per-stage vertical slice and TDD contract (BLOCKING — enforced on the data, not on heading tokens):
|
|
89
|
+
- Every stage declares `sliceValue`, `acceptance`, and the three cases `testCaseSuccess` / `testCaseBoundary` / `testCaseFailure` — happy path, edge/boundary input, failure input. **Enforced:** the v2 schema's `if not tddExemption then require` conditional on `ImplementationPlanStage`.
|
|
90
|
+
- The first `stepwiseExecution` row's `action` starts with `RED:` and its `expected` reads FAIL; some later row's `action` starts with `GREEN:` and its `expected` reads PASS. **Enforced (S10c):** `collect_data_validation_errors` in `validators/validate-implementation-plan-stages.py`, run from `validate-run.py` `_append_stage_data_failures`.
|
|
91
|
+
- `tddExemption` waives both rules above, and only for `doc-only`, `config-only`, or `pure-rename` work. An empty or arbitrary reason waives nothing. **Enforced (S10e):** same function — the schema alone cannot reject it, because it types the field as a plain string and keys its conditional on the property merely being present.
|
|
92
|
+
- `stageMap[].dependsOn` must form a DAG (no self-dependency, no unknown stage, no cycle), each row's `stepCount` must equal its stage's actual `stepwiseExecution` row count, and two `(none)`-dependency stages must not name the same file in their `exitContract` — they run as concurrent implementation runs in separate worktrees. **Enforced (S8/S4/S9):** same function.
|
|
89
93
|
- Required deliverable shape (final report, in addition to the standard sections):
|
|
90
94
|
- at least two implementation options. **Each option must include**:
|
|
91
95
|
- **File Structure**: an explicit list of files to create / modify / delete with each file's responsibility (one-line each). Use the form `Create: path — responsibility` / `Modify: path:line-range — change summary` / `Delete: path — reason`. Write every `path` in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...` / a trailing `/…`); an abbreviated path does not resolve and is rejected by plan-body verification as a kind-b path mismatch.
|
|
@@ -59,6 +59,30 @@ def _check_stage_map_present(text: str) -> List[ValidationError]:
|
|
|
59
59
|
return []
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _parse_depends_on_cell(raw: str) -> List[int] | None:
|
|
63
|
+
"""Stage numbers in a `depends-on` cell; None when the cell is unparseable.
|
|
64
|
+
|
|
65
|
+
Schema v2 keeps the same literal cell text in `stageMap[].dependsOn`, so
|
|
66
|
+
both the Markdown scan and the data scan read it through here.
|
|
67
|
+
"""
|
|
68
|
+
value = raw.strip()
|
|
69
|
+
if value in ("(none)", ""):
|
|
70
|
+
return []
|
|
71
|
+
try:
|
|
72
|
+
return [int(x.strip()) for x in value.split(",") if x.strip()]
|
|
73
|
+
except ValueError:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _stage_numbers_monotonic(stages: List[StageMeta]) -> List[ValidationError]:
|
|
78
|
+
return [
|
|
79
|
+
ValidationError("S2", r.stage_number,
|
|
80
|
+
f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}")
|
|
81
|
+
for i, r in enumerate(stages, start=1)
|
|
82
|
+
if r.stage_number != i
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
62
86
|
def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]:
|
|
63
87
|
m = STAGE_MAP_HEADING.search(text)
|
|
64
88
|
if not m:
|
|
@@ -80,29 +104,19 @@ def _parse_stage_map(text: str) -> Tuple[List[StageMeta], List[ValidationError]]
|
|
|
80
104
|
n = int(cells[0])
|
|
81
105
|
except ValueError:
|
|
82
106
|
continue
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# 비정수 depends_on 셀 → 행 skip (비정수 stage_number 와 동일). raw
|
|
91
|
-
# ValueError 를 흘리면 _stage_map_reject_detail·handoff 의 except
|
|
92
|
-
# PrepareError 를 우회해 traceback 으로 죽으므로, 누락된 행을 하류
|
|
93
|
-
# S2(비단조) 검사가 잡게 한다.
|
|
94
|
-
continue
|
|
107
|
+
depends = _parse_depends_on_cell(cells[2])
|
|
108
|
+
if depends is None:
|
|
109
|
+
# 비정수 depends_on 셀 → 행 skip (비정수 stage_number 와 동일). raw
|
|
110
|
+
# ValueError 를 흘리면 _stage_map_reject_detail·handoff 의 except
|
|
111
|
+
# PrepareError 를 우회해 traceback 으로 죽으므로, 누락된 행을 하류
|
|
112
|
+
# S2(비단조) 검사가 잡게 한다.
|
|
113
|
+
continue
|
|
95
114
|
try:
|
|
96
115
|
step_count = int(cells[3])
|
|
97
116
|
except ValueError:
|
|
98
117
|
step_count = -1
|
|
99
118
|
rows.append(StageMeta(n, cells[1], depends, step_count, cells[4]))
|
|
100
|
-
|
|
101
|
-
for i, r in enumerate(rows, start=1):
|
|
102
|
-
if r.stage_number != i:
|
|
103
|
-
errors.append(ValidationError("S2", r.stage_number,
|
|
104
|
-
f"stage numbers must be 1..N monotonic, got {r.stage_number} at row {i}"))
|
|
105
|
-
return rows, errors
|
|
119
|
+
return rows, _stage_numbers_monotonic(rows)
|
|
106
120
|
|
|
107
121
|
|
|
108
122
|
def _slice_stage_section(text: str, stage_number: int) -> str:
|
|
@@ -358,16 +372,8 @@ def _extract_exit_contract_files(section: str) -> set:
|
|
|
358
372
|
return set(PATH_TOKEN.findall(body))
|
|
359
373
|
|
|
360
374
|
|
|
361
|
-
def
|
|
362
|
-
"""S9
|
|
363
|
-
otherwise two parallel implementation runs would edit it concurrently."""
|
|
364
|
-
files = {
|
|
365
|
-
s.stage_number: _extract_exit_contract_files(
|
|
366
|
-
_slice_stage_section(text, s.stage_number)
|
|
367
|
-
)
|
|
368
|
-
for s in stages
|
|
369
|
-
if not s.depends_on
|
|
370
|
-
}
|
|
375
|
+
def _report_shared_parallel_files(files: dict) -> List[ValidationError]:
|
|
376
|
+
"""S9 over an already-extracted {stage_number: {path}} map."""
|
|
371
377
|
errs: List[ValidationError] = []
|
|
372
378
|
nums = sorted(files)
|
|
373
379
|
for i in range(len(nums)):
|
|
@@ -381,6 +387,18 @@ def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[Validatio
|
|
|
381
387
|
return errs
|
|
382
388
|
|
|
383
389
|
|
|
390
|
+
def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[ValidationError]:
|
|
391
|
+
"""S9: two `depends-on (none)` stages must not predict the same file —
|
|
392
|
+
otherwise two parallel implementation runs would edit it concurrently."""
|
|
393
|
+
return _report_shared_parallel_files({
|
|
394
|
+
s.stage_number: _extract_exit_contract_files(
|
|
395
|
+
_slice_stage_section(text, s.stage_number)
|
|
396
|
+
)
|
|
397
|
+
for s in stages
|
|
398
|
+
if not s.depends_on
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
|
|
384
402
|
def collect_validation_errors(text: str) -> List[ValidationError]:
|
|
385
403
|
"""All S1–S11 checks against the report text; empty list means valid.
|
|
386
404
|
|
|
@@ -404,6 +422,130 @@ def collect_validation_errors(text: str) -> List[ValidationError]:
|
|
|
404
422
|
return errors
|
|
405
423
|
|
|
406
424
|
|
|
425
|
+
def _data_stage_metas(
|
|
426
|
+
stage_map: List[dict],
|
|
427
|
+
) -> Tuple[List[StageMeta], List[ValidationError]]:
|
|
428
|
+
rows = []
|
|
429
|
+
for row in stage_map:
|
|
430
|
+
if not isinstance(row, dict) or not isinstance(row.get("stage"), int):
|
|
431
|
+
continue
|
|
432
|
+
depends = _parse_depends_on_cell(str(row.get("dependsOn") or ""))
|
|
433
|
+
if depends is None:
|
|
434
|
+
continue
|
|
435
|
+
rows.append(StageMeta(
|
|
436
|
+
row["stage"],
|
|
437
|
+
str(row.get("title") or ""),
|
|
438
|
+
depends,
|
|
439
|
+
row.get("stepCount") if isinstance(row.get("stepCount"), int) else -1,
|
|
440
|
+
str(row.get("exitContractSummary") or ""),
|
|
441
|
+
))
|
|
442
|
+
return rows, _stage_numbers_monotonic(rows)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _check_data_slice_tdd(stage: dict) -> List[ValidationError]:
|
|
446
|
+
"""S10c / S10e over one schema-v2 `stages[]` entry.
|
|
447
|
+
|
|
448
|
+
The schema already requires `sliceValue`, `acceptance`, and — through its
|
|
449
|
+
`if not tddExemption then testCase*` conditional — the three test cases, so
|
|
450
|
+
S10a/S10b/S10d are covered declaratively. Two rules a JSON Schema cannot
|
|
451
|
+
state are left: the RED→GREEN ordering across `stepwiseExecution` rows, and
|
|
452
|
+
that a `tddExemption` naming no allowed category cannot waive them. The
|
|
453
|
+
schema types `tddExemption` as a plain string, so `""` currently satisfies
|
|
454
|
+
the conditional and drops all three test cases with no reason given.
|
|
455
|
+
"""
|
|
456
|
+
number = stage.get("stage") if isinstance(stage.get("stage"), int) else 0
|
|
457
|
+
if "tddExemption" in stage:
|
|
458
|
+
reason = str(stage.get("tddExemption") or "").lower()
|
|
459
|
+
if not any(cat in reason for cat in TDD_EXEMPTION_ALLOWED):
|
|
460
|
+
return [ValidationError("S10", number,
|
|
461
|
+
"S10e: 'tddExemption' reason must be one of "
|
|
462
|
+
+ " / ".join(TDD_EXEMPTION_ALLOWED)
|
|
463
|
+
+ " — an empty or arbitrary reason cannot waive RED/GREEN "
|
|
464
|
+
"and the three test cases")]
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
steps = [s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)]
|
|
468
|
+
actions = [str(s.get("action") or "") for s in steps]
|
|
469
|
+
if not (actions and actions[0].startswith("RED:")
|
|
470
|
+
and any(a.startswith("GREEN:") for a in actions)):
|
|
471
|
+
return [ValidationError("S10", number,
|
|
472
|
+
"S10c: first stepwiseExecution action must start with 'RED:' and "
|
|
473
|
+
"some action with 'GREEN:', or declare a 'tddExemption'")]
|
|
474
|
+
|
|
475
|
+
errs: List[ValidationError] = []
|
|
476
|
+
for step in steps:
|
|
477
|
+
action = str(step.get("action") or "")
|
|
478
|
+
expected = str(step.get("expected") or "")
|
|
479
|
+
if action.startswith("RED:") and "FAIL" not in expected.upper():
|
|
480
|
+
errs.append(ValidationError("S10", number,
|
|
481
|
+
f"S10c: 'RED:' step's expected must read FAIL, got '{expected}'"))
|
|
482
|
+
elif action.startswith("GREEN:") and "PASS" not in expected.upper():
|
|
483
|
+
errs.append(ValidationError("S10", number,
|
|
484
|
+
f"S10c: 'GREEN:' step's expected must read PASS, got '{expected}'"))
|
|
485
|
+
return errs
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _check_data_step_counts(
|
|
489
|
+
stage_map: List[StageMeta], stages: List[dict]
|
|
490
|
+
) -> List[ValidationError]:
|
|
491
|
+
"""The Stage Map row is the plan's own index of its stage body. A row
|
|
492
|
+
claiming a step count the body does not have makes the map unusable for
|
|
493
|
+
sizing a stage, which is the only reason the cell exists."""
|
|
494
|
+
by_number = {
|
|
495
|
+
s.get("stage"): s for s in stages
|
|
496
|
+
if isinstance(s, dict) and isinstance(s.get("stage"), int)
|
|
497
|
+
}
|
|
498
|
+
errs: List[ValidationError] = []
|
|
499
|
+
for meta in stage_map:
|
|
500
|
+
stage = by_number.get(meta.stage_number)
|
|
501
|
+
if stage is None:
|
|
502
|
+
errs.append(ValidationError("S3", meta.stage_number,
|
|
503
|
+
"stageMap row has no matching stages[] entry"))
|
|
504
|
+
continue
|
|
505
|
+
actual = len([
|
|
506
|
+
s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)
|
|
507
|
+
])
|
|
508
|
+
if meta.step_count != actual:
|
|
509
|
+
errs.append(ValidationError("S4", meta.stage_number,
|
|
510
|
+
f"stageMap stepCount {meta.step_count} != "
|
|
511
|
+
f"{actual} stepwiseExecution rows"))
|
|
512
|
+
for number in sorted(n for n in by_number if n not in {m.stage_number for m in stage_map}):
|
|
513
|
+
errs.append(ValidationError("S3", number,
|
|
514
|
+
"stages[] entry has no matching stageMap row"))
|
|
515
|
+
return errs
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
519
|
+
"""The S-checks that schema v2 cannot express, over `implementationPlanning`.
|
|
520
|
+
|
|
521
|
+
`collect_validation_errors` scans rendered v1 Markdown, and
|
|
522
|
+
`validate_phase_boundary` returns before calling it for a v2 report — so on
|
|
523
|
+
the current schema nothing enforced the depends-on DAG, parallel-stage file
|
|
524
|
+
safety, RED→GREEN ordering, or the TDD-exemption vocabulary. The schema
|
|
525
|
+
covers presence and cardinality; this covers the relationships between
|
|
526
|
+
fields, which is what a JSON Schema has no way to say.
|
|
527
|
+
"""
|
|
528
|
+
stage_map, errors = _data_stage_metas(planning.get("stageMap") or [])
|
|
529
|
+
stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
|
|
530
|
+
if not stage_map and not stages:
|
|
531
|
+
return errors
|
|
532
|
+
|
|
533
|
+
errors.extend(_check_data_step_counts(stage_map, stages))
|
|
534
|
+
errors.extend(_check_depends_on(stage_map))
|
|
535
|
+
errors.extend(_report_shared_parallel_files({
|
|
536
|
+
meta.stage_number: set(PATH_TOKEN.findall(
|
|
537
|
+
str((next(
|
|
538
|
+
(s for s in stages if s.get("stage") == meta.stage_number), {}
|
|
539
|
+
)).get("exitContract") or "")
|
|
540
|
+
))
|
|
541
|
+
for meta in stage_map
|
|
542
|
+
if not meta.depends_on
|
|
543
|
+
}))
|
|
544
|
+
for stage in stages:
|
|
545
|
+
errors.extend(_check_data_slice_tdd(stage))
|
|
546
|
+
return errors
|
|
547
|
+
|
|
548
|
+
|
|
407
549
|
def main(argv: List[str]) -> int:
|
|
408
550
|
p = argparse.ArgumentParser()
|
|
409
551
|
p.add_argument("--plan", required=True)
|
|
@@ -6177,6 +6177,31 @@ def _load_stage_validator():
|
|
|
6177
6177
|
return mod
|
|
6178
6178
|
|
|
6179
6179
|
|
|
6180
|
+
def _append_stage_data_failures(data: Mapping[str, Any], failures: list[str]) -> None:
|
|
6181
|
+
"""Run the stage relationship checks that schema v2 cannot express.
|
|
6182
|
+
|
|
6183
|
+
`_append_stage_structure_failures` scans rendered Markdown and sits after
|
|
6184
|
+
the v2 early return in `validate_phase_boundary`, so for a v2 report the
|
|
6185
|
+
depends-on DAG, parallel-stage file safety, RED→GREEN ordering, and the
|
|
6186
|
+
TDD-exemption vocabulary had nothing enforcing them. The same validator
|
|
6187
|
+
owns both modes so the rule vocabulary stays defined once.
|
|
6188
|
+
"""
|
|
6189
|
+
if (data or {}).get("schemaVersion") != "2.0":
|
|
6190
|
+
return # v1 reports are covered by the Markdown scan.
|
|
6191
|
+
planning = (data or {}).get("implementationPlanning")
|
|
6192
|
+
if not isinstance(planning, Mapping):
|
|
6193
|
+
return # Schema validation already reported the missing block.
|
|
6194
|
+
mod = _load_stage_validator()
|
|
6195
|
+
if mod is None: # pragma: no cover — repo/runtime always ship the file
|
|
6196
|
+
failures.append(f"cannot load Stage Map validator at {_STAGE_VALIDATOR_PATH}")
|
|
6197
|
+
return
|
|
6198
|
+
for e in mod.collect_data_validation_errors(dict(planning)):
|
|
6199
|
+
failures.append(
|
|
6200
|
+
f"implementation-planning stage contract invalid "
|
|
6201
|
+
f"[{e.code} stage={e.stage}]: {e.message}"
|
|
6202
|
+
)
|
|
6203
|
+
|
|
6204
|
+
|
|
6180
6205
|
def _append_stage_structure_failures(content: str, failures: list[str]) -> None:
|
|
6181
6206
|
"""Enforce the Stage Map structural contract at the implementation-planning
|
|
6182
6207
|
boundary. Without this, a plan missing `## 5.5 Stage Map` passes the
|
|
@@ -7420,6 +7445,7 @@ def main() -> int:
|
|
|
7420
7445
|
validation_data, brief_path, failures
|
|
7421
7446
|
)
|
|
7422
7447
|
_validate_stage_has_requirement(validation_data, failures)
|
|
7448
|
+
_append_stage_data_failures(validation_data, failures)
|
|
7423
7449
|
if task_type == "improvement-discovery":
|
|
7424
7450
|
run_dir = report_path.parent.parent
|
|
7425
7451
|
_validate_improvement_discovery(report_path, run_dir, brief_path, failures)
|