okstra 0.201.1 → 0.201.2
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 +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/report_assembly.py +8 -0
- package/runtime/python/okstra_ctl/write_policy.py +34 -2
- package/runtime/schemas/final-report-v2.0.schema.json +2 -2
- package/runtime/schemas/final-report-v3.0.schema.json +2 -2
- package/runtime/validators/validate-implementation-plan-stages.py +5 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -173,7 +173,7 @@ roles:
|
|
|
173
173
|
**Pick the cases by distinct outcome, not by line coverage.** When the stage writes or reconciles state, its meaningfully different outcomes are usually more than three — normal success, target already in the desired state (resume), existing data reused rather than created, a conflicting concurrent state, target absent, and mid-way failure with rollback. Enumerate the ones this stage actually implements and route them across the three lines (the `boundary` line is where resume / already-done / reuse belongs; `failure` carries conflict, absence, and rollback), naming each in the cell rather than collapsing them into "edge input". An implemented outcome with no declared case is a coverage gap the executor will not backfill.
|
|
174
174
|
- **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
|
|
175
175
|
- `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
176
|
-
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the
|
|
176
|
+
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | outcome | expected`. `outcome` is one word — `PASS` or `FAIL` — and `expected` is the sentence saying what that looks like here; a verdict written inside the sentence is not read as one. The `files` cell lists each touched path in full and `<PROJECT_ROOT>`-relative — never ellipsis-abbreviated (`…` / `...`), which does not resolve and is rejected by plan-body verification as a kind-b path mismatch. **The narrative row additionally carries `plannedPaths`: the paths this step may write as an array, one repository-relative path per entry, with no globs, exclusions, counts or commentary. Read-only checks use `plannedPaths: []`; their working directory belongs in the command, not the write ledger. Never declare the repository or worktree root. Planning assembly and correction preflight enforce this through `write_policy.planned_path_declaration_errors`.** `files` is the sentence a reader sees; `plannedPaths` is the ledger report assembly preserves and the implementer write policy enforces. When a step legitimately covers a set too large to enumerate, split it or name the directory the set lives under. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`outcome` = `FAIL`); at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`outcome` = `PASS`); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section. A stage that is truthfully none of those three declares `TDD exemption: user-bypass — <the user's words>`, which holds only while the user has granted it for that stage with `okstra prepare --tdd-bypass "<stage>:<reason>"` — never file the nearest of the three instead. Validator S10c enforces RED-first + GREEN; the `outcome` cell agreeing with its prefix is a schema conditional. S10e rejects an unsupported exemption reason and a `user-bypass` with no user grant (`validators/validate-implementation-plan-stages.py`).
|
|
177
177
|
- **The `command` cell runs inside an okstra task worktree, not a bare checkout (BLOCKING).** okstra provisions `.okstra`, the configured sync entries (`.project-docs`, `.claude`, …), and — for `implementation` — a nested `stage-<N>/` worktree into the tree the step executes in. Two consequences bind every command you write:
|
|
178
178
|
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
|
|
179
179
|
- **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
|
|
@@ -41,6 +41,7 @@ from .report_projections import (
|
|
|
41
41
|
)
|
|
42
42
|
from .scope_provenance import parse_source
|
|
43
43
|
from .verification_target import read_verification_target
|
|
44
|
+
from .write_policy import planned_path_declaration_errors
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
@dataclass(frozen=True)
|
|
@@ -594,6 +595,7 @@ def validate_plan_draft(
|
|
|
594
595
|
*task_narrative_errors(
|
|
595
596
|
draft, load_schema_version("3.0"), str(manifest.get("taskType", "")),
|
|
596
597
|
),
|
|
598
|
+
*planned_path_declaration_errors(draft),
|
|
597
599
|
*[
|
|
598
600
|
f"implementationPlanning: {error}"
|
|
599
601
|
for error in selected_direction_plan_errors(draft, project_root, manifest)
|
|
@@ -880,6 +882,12 @@ def assemble_report(
|
|
|
880
882
|
raise ReportAssemblyError(tuple(input_issues))
|
|
881
883
|
data = _compose(project_root, manifest_path, manifest, inputs, schema)
|
|
882
884
|
errors = validate(data, schema)
|
|
885
|
+
path_errors = planned_path_declaration_errors(data)
|
|
886
|
+
if path_errors:
|
|
887
|
+
raise ReportAssemblyError(tuple(
|
|
888
|
+
AssemblyIssue("report-writer", str(inputs["narrative"].path), error.split(":", 1)[0], error)
|
|
889
|
+
for error in path_errors
|
|
890
|
+
))
|
|
883
891
|
direction_errors = selected_direction_plan_errors(
|
|
884
892
|
data, project_root, manifest
|
|
885
893
|
)
|
|
@@ -447,7 +447,7 @@ def _relative_to_root(path: Path, root: Path, label: str) -> str:
|
|
|
447
447
|
except ValueError as exc:
|
|
448
448
|
raise WritePolicyError(f"{label} path is outside project root") from exc
|
|
449
449
|
if not relative or relative == ".":
|
|
450
|
-
raise WritePolicyError(f"{label} path must name a file")
|
|
450
|
+
raise WritePolicyError(f"{label} path must name a file or scoped subdirectory, not root: {path}")
|
|
451
451
|
current = root
|
|
452
452
|
for part in PurePosixPath(relative).parts:
|
|
453
453
|
current /= part
|
|
@@ -466,6 +466,9 @@ def _planned_paths_from_report(
|
|
|
466
466
|
# 그 파일은 없으므로 implementer 디스패치가 전부 막혔다. 정본 헬퍼는 이미
|
|
467
467
|
# 레코드인 경로를 그대로 돌려주고 `.md` 만 짝으로 바꾼다.
|
|
468
468
|
payload = _read_json(final_report_data_path(report_path), "approved plan data")
|
|
469
|
+
errors = planned_path_declaration_errors(payload)
|
|
470
|
+
if errors:
|
|
471
|
+
raise WritePolicyError(f"{report_path}: {'; '.join(errors)}")
|
|
469
472
|
planning = payload.get("implementationPlanning")
|
|
470
473
|
stages = planning.get("stages") if isinstance(planning, Mapping) else None
|
|
471
474
|
selected = next(
|
|
@@ -489,7 +492,10 @@ def _planned_paths_from_report(
|
|
|
489
492
|
for path in (step.get("plannedPaths") or [])
|
|
490
493
|
if isinstance(path, str) and path.strip()
|
|
491
494
|
}
|
|
492
|
-
if paths
|
|
495
|
+
if paths or (rows and all(
|
|
496
|
+
isinstance(step, Mapping) and isinstance(step.get("plannedPaths"), list)
|
|
497
|
+
for step in rows
|
|
498
|
+
)):
|
|
493
499
|
# 경로 분류는 산출물 권한과 배정 작업 디렉터리를 함께 아는 정책 생성기가 맡는다.
|
|
494
500
|
return tuple(sorted(paths)), True
|
|
495
501
|
# A plan approved before `plannedPaths` existed carries its paths only in
|
|
@@ -516,6 +522,32 @@ def _planned_paths_from_report(
|
|
|
516
522
|
return (), False
|
|
517
523
|
|
|
518
524
|
|
|
525
|
+
def planned_path_declaration_errors(data: Mapping[str, Any]) -> list[str]:
|
|
526
|
+
"""검사 단계의 빈 선언은 보존하고 저장소 전체를 쓰기 대상으로 승인하지 않는다."""
|
|
527
|
+
planning = data.get("implementationPlanning")
|
|
528
|
+
stages = planning.get("stages") if isinstance(planning, Mapping) else None
|
|
529
|
+
errors: list[str] = []
|
|
530
|
+
for si, stage in enumerate(stages if isinstance(stages, list) else []):
|
|
531
|
+
steps = stage.get("stepwiseExecution") if isinstance(stage, Mapping) else None
|
|
532
|
+
for ti, step in enumerate(steps if isinstance(steps, list) else []):
|
|
533
|
+
paths = step.get("plannedPaths") if isinstance(step, Mapping) else None
|
|
534
|
+
for pi, value in enumerate(paths if isinstance(paths, list) else []):
|
|
535
|
+
if not isinstance(value, str):
|
|
536
|
+
continue
|
|
537
|
+
path = Path(value.strip().strip("`"))
|
|
538
|
+
# 절대 경로는 디스패치와 같은 실제 작업트리를 가리킨다. 새 파일은 없어도 된다.
|
|
539
|
+
root = path == Path(".") or path == Path(path.anchor or ".")
|
|
540
|
+
if path.is_absolute():
|
|
541
|
+
root = root or (path / ".git").exists()
|
|
542
|
+
if root:
|
|
543
|
+
field = f"implementationPlanning.stages[{si}].stepwiseExecution[{ti}].plannedPaths[{pi}]"
|
|
544
|
+
errors.append(
|
|
545
|
+
f"{field}: planned path must name a file or a scoped subdirectory, "
|
|
546
|
+
f"not a repository/worktree root: {value!r}; use [] for a read-only step"
|
|
547
|
+
)
|
|
548
|
+
return errors
|
|
549
|
+
|
|
550
|
+
|
|
519
551
|
def _rooted(project_root: Path, value: str) -> Path:
|
|
520
552
|
path = Path(value)
|
|
521
553
|
return path if path.is_absolute() else project_root / path
|
|
@@ -8943,9 +8943,9 @@
|
|
|
8943
8943
|
"minLength": 1
|
|
8944
8944
|
},
|
|
8945
8945
|
"plannedPaths": {
|
|
8946
|
-
"description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
8946
|
+
"description": "Paths this step may write, one per entry. Use an empty array for read-only steps. Never name the repository or worktree root; scoped subdirectories remain allowed. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
8947
8947
|
"type": "array",
|
|
8948
|
-
"minItems":
|
|
8948
|
+
"minItems": 0,
|
|
8949
8949
|
"items": {
|
|
8950
8950
|
"type": "string",
|
|
8951
8951
|
"minLength": 1
|
|
@@ -9088,9 +9088,9 @@
|
|
|
9088
9088
|
"minLength": 1
|
|
9089
9089
|
},
|
|
9090
9090
|
"plannedPaths": {
|
|
9091
|
-
"description": "Paths this step may write, one per entry. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
9091
|
+
"description": "Paths this step may write, one per entry. Use an empty array for read-only steps. Never name the repository or worktree root; scoped subdirectories remain allowed. Source paths are relative to the assigned worktree; absolute paths under that worktree or the project checkout identify the same repository-relative source. `.okstra` artifact paths are anchored at the original project root and may be absolute. The write-policy builder checks artifact paths against the worker's existing artifact permissions and keeps them out of the source ledger. Keep absolute artifact paths in executable commands. `files` is human-readable prose, not a path ledger.",
|
|
9092
9092
|
"type": "array",
|
|
9093
|
-
"minItems":
|
|
9093
|
+
"minItems": 0,
|
|
9094
9094
|
"items": {
|
|
9095
9095
|
"type": "string",
|
|
9096
9096
|
"minLength": 1
|
|
@@ -21,6 +21,7 @@ for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "
|
|
|
21
21
|
sys.path.insert(0, str(_ssot_dir))
|
|
22
22
|
|
|
23
23
|
from okstra_ctl.md_table import split_pipe_row # noqa: E402
|
|
24
|
+
from okstra_ctl.write_policy import planned_path_declaration_errors # noqa: E402
|
|
24
25
|
from okstra_ctl.tdd_bypass import ( # noqa: E402
|
|
25
26
|
REASON_TOKEN as TDD_USER_BYPASS_TOKEN,
|
|
26
27
|
bypass_file,
|
|
@@ -688,6 +689,10 @@ def collect_data_validation_errors(
|
|
|
688
689
|
|
|
689
690
|
raw_stage_map = planning.get("stageMap") or []
|
|
690
691
|
stage_map, errors = _data_stage_metas(raw_stage_map)
|
|
692
|
+
errors.extend(
|
|
693
|
+
ValidationError("S10", 0, error)
|
|
694
|
+
for error in planned_path_declaration_errors({"implementationPlanning": planning})
|
|
695
|
+
)
|
|
691
696
|
stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
|
|
692
697
|
if not stage_map and not stages:
|
|
693
698
|
return errors
|