okstra 0.55.0 → 0.56.1
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/bin/okstra +24 -7
- package/docs/kr/architecture.md +2 -2
- package/docs/project-structure-overview.md +0 -1
- package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -1
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +275 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +282 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +147 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +262 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +184 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +88 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +250 -0
- package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +409 -0
- package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +169 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/usage.sh +5 -0
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +4 -4
- package/runtime/prompts/profiles/_implementation-executor.md +1 -4
- package/runtime/prompts/profiles/_implementation-verifier.md +23 -2
- package/runtime/prompts/profiles/final-verification.md +2 -1
- package/runtime/prompts/profiles/implementation-planning.md +9 -5
- package/runtime/prompts/profiles/implementation.md +6 -6
- package/runtime/prompts/profiles/improvement-discovery.md +1 -0
- package/runtime/prompts/profiles/release-handoff.md +4 -4
- package/runtime/python/okstra_ctl/conformance.py +270 -0
- package/runtime/python/okstra_ctl/paths.py +2 -0
- package/runtime/python/okstra_ctl/run.py +29 -0
- package/runtime/schemas/final-report-v1.0.schema.json +127 -10
- package/runtime/skills/okstra-coding-preflight/SKILL.md +8 -0
- package/runtime/skills/okstra-coding-preflight/clean-code.md +6 -0
- package/runtime/skills/okstra-run/SKILL.md +12 -0
- package/runtime/skills/okstra-run/templates/pr-body.template.md +12 -12
- package/runtime/skills/okstra-setup/SKILL.md +35 -0
- package/runtime/templates/reports/final-report.template.md +63 -19
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/i18n/ko.json +1 -1
- package/runtime/templates/reports/implementation-input.template.md +1 -1
- package/runtime/templates/reports/implementation-planning-input.template.md +3 -3
- package/runtime/validators/validate-implementation-plan-stages.py +28 -3
- package/runtime/validators/validate-run.py +98 -0
- package/src/okstra-dirs.mjs +1 -1
- package/src/migrate.mjs +0 -146
|
@@ -89,12 +89,12 @@ The final report of an `implementation-planning` run MUST contain every section
|
|
|
89
89
|
1. **Option Candidates** — at least two viable options, each with the exact list of files to create or modify and the principal change per file.
|
|
90
90
|
2. **Trade-off Matrix** — comparison of the candidates across complexity, risk, reversibility, performance impact, scope, and required test surface.
|
|
91
91
|
3. **Recommended Option** — selected option with explicit rationale referencing the trade-off matrix.
|
|
92
|
-
4. **
|
|
92
|
+
4. **Stage Map** — `## 5.5 Stage Map` plus one `## 5.5.<i> Stage <i>:` section per stage. Each stage is a thin vertical slice with `Slice value:`, `Acceptance:`, `Conformance tests:` or `Conformance exemption:`, the four required subsections, and a RED/GREEN step order unless a `TDD exemption:` line applies.
|
|
93
93
|
5. **Dependency and Migration Risk** — schema, data, ordering, feature-flag, and cross-service risks that the recommended option introduces.
|
|
94
94
|
6. **Validation Checklist** — pre-execution, mid-execution, and post-execution checks (commands, expected outputs, observability points).
|
|
95
95
|
7. **Rollback Strategy** — exact reverse procedure or compensating action for each significant step.
|
|
96
96
|
8. **Scope Boundary** — an explicit list of adjacent areas, files, refactors, or quality improvements that this plan **does NOT** cover, each with a one-line reason (deferred, separate owner, separate decision, out of requirement). Any item the analysers were tempted to fold in but chose to exclude MUST appear here. An empty list is allowed only when the analysers explicitly state "no adjacent expansion was considered" — silence is not acceptable.
|
|
97
|
-
9. **
|
|
97
|
+
9. **Approval frontmatter** — the final report's YAML frontmatter MUST emit `approved: false` and `implementation-option:`. Do NOT create a `User Approval Request` body block; the next `implementation` run reads only the frontmatter gate.
|
|
98
98
|
|
|
99
99
|
## Phase Boundary
|
|
100
100
|
|
|
@@ -108,7 +108,7 @@ The final report of an `implementation-planning` run MUST contain every section
|
|
|
108
108
|
|
|
109
109
|
## Stage Output Shape (reference)
|
|
110
110
|
|
|
111
|
-
This run's final report MUST emit `## 5.5 Stage Map` and `## 5.5.<i> Stage <i>` sections per the implementation-planning profile §"Required deliverable shape". Two illustrative
|
|
111
|
+
This run's final report MUST emit `## 5.5 Stage Map` and `## 5.5.<i> Stage <i>` sections per the implementation-planning profile §"Required deliverable shape". Two illustrative Stage Map tables:
|
|
112
112
|
|
|
113
113
|
### Shape A — single stage (small work)
|
|
114
114
|
| stage | title | depends-on | step-count | exit-contract-summary |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""S1–
|
|
2
|
+
"""S1–S11 checks for the Stage Map structure of an approved
|
|
3
3
|
implementation-planning final-report.md. Run from prepare_task_bundle
|
|
4
4
|
of `implementation` task or standalone."""
|
|
5
5
|
|
|
@@ -40,7 +40,7 @@ class StageMeta:
|
|
|
40
40
|
|
|
41
41
|
@dataclass
|
|
42
42
|
class ValidationError:
|
|
43
|
-
code: str # S1..
|
|
43
|
+
code: str # S1..S11
|
|
44
44
|
stage: int # 0 = global
|
|
45
45
|
message: str
|
|
46
46
|
|
|
@@ -168,6 +168,8 @@ def _check_each_stage_section(text: str, stages: List[StageMeta]) -> List[Valida
|
|
|
168
168
|
SLICE_VALUE = re.compile(r"^\s*Slice value\s*:\s*(.+?)\s*$", re.M)
|
|
169
169
|
ACCEPTANCE = re.compile(r"^\s*Acceptance\s*:\s*(.+?)\s*$", re.M)
|
|
170
170
|
TDD_EXEMPTION = re.compile(r"^\s*TDD exemption\s*:\s*\S", re.M)
|
|
171
|
+
CONFORMANCE_TESTS = re.compile(r"^\s*Conformance tests\s*:\s*\S", re.M)
|
|
172
|
+
CONFORMANCE_EXEMPTION = re.compile(r"^\s*Conformance exemption\s*:\s*\S", re.M)
|
|
171
173
|
|
|
172
174
|
|
|
173
175
|
def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError]:
|
|
@@ -204,6 +206,28 @@ def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError
|
|
|
204
206
|
return errs
|
|
205
207
|
|
|
206
208
|
|
|
209
|
+
def _check_conformance_declaration(
|
|
210
|
+
text: str, stages: List[StageMeta]
|
|
211
|
+
) -> List[ValidationError]:
|
|
212
|
+
"""S11: 각 stage 는 conformance 검증을 선언하거나 명시적으로 면제한다.
|
|
213
|
+
|
|
214
|
+
S11 — `Conformance tests:` 라인(Tier3 검증 스크립트 선언) 또는
|
|
215
|
+
`Conformance exemption:` 라인(테스트 불필요 사유) 중 하나 필수.
|
|
216
|
+
diff 가 db/io/http surface 를 건드렸는데 아무 선언이 없는 silent-pass(DEV-9184)
|
|
217
|
+
를 planning boundary 에서 차단한다.
|
|
218
|
+
"""
|
|
219
|
+
errs: List[ValidationError] = []
|
|
220
|
+
for s in stages:
|
|
221
|
+
section = _slice_stage_section(text, s.stage_number)
|
|
222
|
+
if not (CONFORMANCE_TESTS.search(section) or CONFORMANCE_EXEMPTION.search(section)):
|
|
223
|
+
errs.append(ValidationError(
|
|
224
|
+
"S11", s.stage_number,
|
|
225
|
+
"S11: stage must declare 'Conformance tests:' (Tier3 검증 스크립트) "
|
|
226
|
+
"or 'Conformance exemption:' (사유) — stage conformance QA design §12.2",
|
|
227
|
+
))
|
|
228
|
+
return errs
|
|
229
|
+
|
|
230
|
+
|
|
207
231
|
def _check_depends_on(stages: List[StageMeta]) -> List[ValidationError]:
|
|
208
232
|
errs: List[ValidationError] = []
|
|
209
233
|
valid = {s.stage_number for s in stages}
|
|
@@ -274,7 +298,7 @@ def _check_parallel_safety(text: str, stages: List[StageMeta]) -> List[Validatio
|
|
|
274
298
|
|
|
275
299
|
|
|
276
300
|
def collect_validation_errors(text: str) -> List[ValidationError]:
|
|
277
|
-
"""All S1–
|
|
301
|
+
"""All S1–S11 checks against the report text; empty list means valid.
|
|
278
302
|
|
|
279
303
|
S1 (missing `## 5.5 Stage Map` heading) makes the rest unparseable, so it
|
|
280
304
|
short-circuits. Shared by `main()` (CLI / implementation entry) and the
|
|
@@ -290,6 +314,7 @@ def collect_validation_errors(text: str) -> List[ValidationError]:
|
|
|
290
314
|
if stages:
|
|
291
315
|
errors.extend(_check_each_stage_section(text, stages))
|
|
292
316
|
errors.extend(_check_slice_tdd(text, stages))
|
|
317
|
+
errors.extend(_check_conformance_declaration(text, stages))
|
|
293
318
|
errors.extend(_check_depends_on(stages))
|
|
294
319
|
errors.extend(_check_parallel_safety(text, stages))
|
|
295
320
|
return errors
|
|
@@ -30,8 +30,17 @@ except ImportError: # pragma: no cover — runtime guarantees this import
|
|
|
30
30
|
load_schema = None # type: ignore[assignment]
|
|
31
31
|
schema_validate = None # type: ignore[assignment]
|
|
32
32
|
|
|
33
|
+
from okstra_project import project_json_path # noqa: E402
|
|
33
34
|
from okstra_project.dirs import tasks_root as _okstra_tasks_root # noqa: E402
|
|
34
35
|
|
|
36
|
+
from okstra_ctl.conformance import ( # noqa: E402
|
|
37
|
+
detect_surfaces,
|
|
38
|
+
evaluate_conformance,
|
|
39
|
+
manifest_required_surfaces,
|
|
40
|
+
qa_result_from_dict,
|
|
41
|
+
validate_conformance_manifest,
|
|
42
|
+
)
|
|
43
|
+
|
|
35
44
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
36
45
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
37
46
|
|
|
@@ -700,6 +709,84 @@ _DEPRECATED_FINAL_REPORT_PATTERNS: tuple[tuple[re.Pattern, str], ...] = (
|
|
|
700
709
|
)
|
|
701
710
|
|
|
702
711
|
|
|
712
|
+
def _load_conformance_results(qa_dir: Path, manifest: dict) -> dict:
|
|
713
|
+
"""매니페스트 각 entry 에 대응하는 `result-<stageKey>.json` 을 로드.
|
|
714
|
+
파일 부재/파손은 키를 비워 둬 미실행(None→BLOCKING)으로 흐르게 한다."""
|
|
715
|
+
results: dict = {}
|
|
716
|
+
for entry in manifest.get("entries", []):
|
|
717
|
+
key = entry.get("stageKey") if isinstance(entry, dict) else None
|
|
718
|
+
if not isinstance(key, str) or not key:
|
|
719
|
+
continue
|
|
720
|
+
sidecar = qa_dir / f"result-{key}.json"
|
|
721
|
+
if not sidecar.is_file():
|
|
722
|
+
continue
|
|
723
|
+
try:
|
|
724
|
+
results[key] = qa_result_from_dict(json.loads(sidecar.read_text()))
|
|
725
|
+
except (OSError, json.JSONDecodeError):
|
|
726
|
+
results[key] = qa_result_from_dict(None) # → MISSING → BLOCKING
|
|
727
|
+
return results
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
_DIFF_SUMMARY_RE = re.compile(r"^###\s+5\.7\.3\b.*?(?=^###\s|\Z)", re.MULTILINE | re.DOTALL)
|
|
731
|
+
_DIFF_ROW_PATH_RE = re.compile(r"^\|\s*`([^`]+)`\s*\|", re.MULTILINE)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _parse_diff_summary_files(content: str) -> list[str]:
|
|
735
|
+
"""implementation 리포트 §5.7.3 Diff Summary 표의 첫 셀(백틱 경로)들을 추출."""
|
|
736
|
+
section = _DIFF_SUMMARY_RE.search(content)
|
|
737
|
+
if section is None:
|
|
738
|
+
return []
|
|
739
|
+
return _DIFF_ROW_PATH_RE.findall(section.group(0))
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _validate_conformance(report_path: Path, failures: list[str],
|
|
743
|
+
surface_patterns: object = None) -> None:
|
|
744
|
+
"""Tier 3 conformance 게이트(implementation / final-verification).
|
|
745
|
+
|
|
746
|
+
`<task_root>/qa/conformance-manifest.json` 이 없으면 inert(선언된 conformance
|
|
747
|
+
가 없다는 뜻 — 선언을 강제하는 것은 planning 계약(Phase 4)의 몫). 매니페스트가
|
|
748
|
+
있으면 결과 사이드카와 함께 evaluate_conformance 로 판정하고 BLOCKING verdict
|
|
749
|
+
를 run 검증 실패로 승격한다. WAIVED(conditional)/EXEMPT 는 통과시킨다.
|
|
750
|
+
"""
|
|
751
|
+
# conformance 산출물은 task-level(<task_root>/qa)에 있어 planning/
|
|
752
|
+
# implementation/final-verification 가 공유한다. report_path 는
|
|
753
|
+
# task_root/runs/<task-type>/reports/final-report.md 이므로 task_root 는
|
|
754
|
+
# run_dir(=report_path.parent.parent)에서 두 단계 위.
|
|
755
|
+
run_dir = report_path.parent.parent
|
|
756
|
+
qa_dir = run_dir.parent.parent / "qa"
|
|
757
|
+
manifest_path = qa_dir / "conformance-manifest.json"
|
|
758
|
+
if not manifest_path.is_file():
|
|
759
|
+
return
|
|
760
|
+
try:
|
|
761
|
+
manifest = json.loads(manifest_path.read_text())
|
|
762
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
763
|
+
failures.append(f"conformance manifest unreadable at {manifest_path}: {exc}")
|
|
764
|
+
return
|
|
765
|
+
schema_errors = validate_conformance_manifest(manifest)
|
|
766
|
+
if schema_errors:
|
|
767
|
+
failures.extend(f"conformance manifest: {e}" for e in schema_errors)
|
|
768
|
+
return
|
|
769
|
+
results = _load_conformance_results(qa_dir, manifest)
|
|
770
|
+
for verdict in evaluate_conformance(manifest, results):
|
|
771
|
+
if not verdict.ok:
|
|
772
|
+
failures.append(
|
|
773
|
+
f"conformance gate BLOCKING for stage {verdict.stage_key}: "
|
|
774
|
+
f"{verdict.message}. Run the stage's conformance script (or declare "
|
|
775
|
+
f"an exemption / user waiver) — see "
|
|
776
|
+
f"docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md."
|
|
777
|
+
)
|
|
778
|
+
changed_files = _parse_diff_summary_files(report_path.read_text(encoding="utf-8"))
|
|
779
|
+
if changed_files:
|
|
780
|
+
uncovered = detect_surfaces(changed_files, surface_patterns) - manifest_required_surfaces(manifest)
|
|
781
|
+
if uncovered:
|
|
782
|
+
failures.append(
|
|
783
|
+
"conformance gate BLOCKING: implementation diff touches undeclared "
|
|
784
|
+
f"surface(s) {sorted(uncovered)} — no stage declares `requires` for "
|
|
785
|
+
"them. Declare a conformance entry (requires=[...]) for the touching "
|
|
786
|
+
"stage, or an explicit exemption. (silent mock-green 방지 — DEV-9184)"
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
|
|
703
790
|
def validate_report(
|
|
704
791
|
report_path: Path, required_agent_status_entries: list[str], failures: list[str]
|
|
705
792
|
) -> None:
|
|
@@ -904,6 +991,7 @@ PLANNING_REQUIRED_SECTIONS = (
|
|
|
904
991
|
"Option Candidates",
|
|
905
992
|
"Trade-off",
|
|
906
993
|
"Recommended Option",
|
|
994
|
+
"Stage Map",
|
|
907
995
|
"Stepwise Execution Order",
|
|
908
996
|
"Dependency",
|
|
909
997
|
"Validation Checklist",
|
|
@@ -918,6 +1006,7 @@ IMPLEMENTATION_REQUIRED_SECTIONS = (
|
|
|
918
1006
|
"Commit List",
|
|
919
1007
|
"Diff Summary",
|
|
920
1008
|
"Out-of-plan Edits",
|
|
1009
|
+
"Stage Sidecar Evidence",
|
|
921
1010
|
"Validation Evidence",
|
|
922
1011
|
"Verifier Results",
|
|
923
1012
|
"Rollback Verification",
|
|
@@ -1917,6 +2006,15 @@ def main() -> int:
|
|
|
1917
2006
|
validate_phase_boundary(task_type, report_path, failures)
|
|
1918
2007
|
if task_type:
|
|
1919
2008
|
validate_worker_results_audit(report_path, task_type, failures)
|
|
2009
|
+
if task_type in ("implementation", "final-verification"):
|
|
2010
|
+
_sp = None
|
|
2011
|
+
_pj = project_json_path(project_root)
|
|
2012
|
+
if _pj.is_file():
|
|
2013
|
+
try:
|
|
2014
|
+
_sp = (json.loads(_pj.read_text()).get("qaEnv") or {}).get("surfacePatterns")
|
|
2015
|
+
except (OSError, json.JSONDecodeError):
|
|
2016
|
+
_sp = None
|
|
2017
|
+
_validate_conformance(report_path, failures, surface_patterns=_sp)
|
|
1920
2018
|
if task_type == "improvement-discovery":
|
|
1921
2019
|
brief_relative = str(task_manifest.get("taskBriefPath") or "").strip()
|
|
1922
2020
|
brief_path = (
|
package/src/okstra-dirs.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const OKSTRA_DIR = ".okstra";
|
|
|
15
15
|
// Pre-v0.37 layout. `okstra migrate` reads this to find unmigrated projects.
|
|
16
16
|
// 코드 path 빌드에는 절대 사용 금지 — `OKSTRA_DIR` 만. 오로지 migration
|
|
17
17
|
// 탐지/안내 메시지 용도.
|
|
18
|
-
export const LEGACY_OKSTRA_DIR = "
|
|
18
|
+
export const LEGACY_OKSTRA_DIR = "./okstra";
|
|
19
19
|
|
|
20
20
|
export function okstraRoot(projectRoot) {
|
|
21
21
|
return join(projectRoot, OKSTRA_DIR);
|
package/src/migrate.mjs
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { buildPythonpath, resolvePaths } from "./paths.mjs";
|
|
2
|
-
import { LEGACY_OKSTRA_DIR, OKSTRA_DIR } from "./okstra-dirs.mjs";
|
|
3
|
-
import { runProcess } from "./_proc.mjs";
|
|
4
|
-
|
|
5
|
-
const USAGE = `okstra migrate — move project artifacts from ${LEGACY_OKSTRA_DIR}/ to ${OKSTRA_DIR}/
|
|
6
|
-
|
|
7
|
-
Run this once per project that was set up before v0.37. The migrator moves
|
|
8
|
-
the okstra-managed directory tree, rewrites the import line in your
|
|
9
|
-
project's CLAUDE.md, updates .gitignore, and refreshes okstra-home
|
|
10
|
-
registries (~/.okstra/{recent,active}.jsonl, ~/.okstra/worktrees/registry.json)
|
|
11
|
-
so the existing run history keeps resolving to the new path.
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
okstra migrate Dry-run by default. Prints the plan as JSON.
|
|
15
|
-
okstra migrate --apply Execute the move and ancillary updates.
|
|
16
|
-
okstra migrate --cwd <dir> Search starting point (default: cwd).
|
|
17
|
-
okstra migrate --quiet Suppress stdout on success; exit code reports.
|
|
18
|
-
|
|
19
|
-
Exit codes:
|
|
20
|
-
0 plan prepared (dry-run) or migration applied successfully
|
|
21
|
-
1 pre-condition rejected (e.g. ${OKSTRA_DIR}/ already exists, or no
|
|
22
|
-
${LEGACY_OKSTRA_DIR}/ to migrate); reason printed on stderr
|
|
23
|
-
2 internal error (python invocation failed, etc.)
|
|
24
|
-
|
|
25
|
-
The command is idempotent in spirit: re-running after a successful apply
|
|
26
|
-
exits 1 with "nothing to migrate". Safe to wire into CI as a one-shot
|
|
27
|
-
post-upgrade step.
|
|
28
|
-
`;
|
|
29
|
-
|
|
30
|
-
function parseArgs(args) {
|
|
31
|
-
const opts = { cwd: process.cwd(), apply: false, quiet: false };
|
|
32
|
-
for (let i = 0; i < args.length; i++) {
|
|
33
|
-
const a = args[i];
|
|
34
|
-
if (a === "--apply") opts.apply = true;
|
|
35
|
-
else if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
36
|
-
else if (a === "--cwd") {
|
|
37
|
-
const next = args[i + 1];
|
|
38
|
-
if (!next || next.startsWith("--")) throw new Error("--cwd requires a path");
|
|
39
|
-
opts.cwd = next;
|
|
40
|
-
i++;
|
|
41
|
-
} else {
|
|
42
|
-
throw new Error(`unknown argument '${a}'`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return opts;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function emit(opts, payload) {
|
|
49
|
-
if (opts.quiet) return;
|
|
50
|
-
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function run(args) {
|
|
54
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
55
|
-
process.stdout.write(USAGE);
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let opts;
|
|
60
|
-
try {
|
|
61
|
-
opts = parseArgs(args);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
64
|
-
return 2;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const paths = await resolvePaths();
|
|
68
|
-
|
|
69
|
-
// We resolve project root via the python resolver — the same one used by
|
|
70
|
-
// every other okstra command — to keep "what project this is" consistent
|
|
71
|
-
// across CLI surfaces. The resolver knows three lookup strategies; the
|
|
72
|
-
// migrator only needs whichever ancestor it returns.
|
|
73
|
-
const py = await runProcess(
|
|
74
|
-
"python3",
|
|
75
|
-
[
|
|
76
|
-
"-c",
|
|
77
|
-
[
|
|
78
|
-
"import json, sys",
|
|
79
|
-
"from pathlib import Path",
|
|
80
|
-
"from okstra_project.resolver import resolve_project_root, ResolverError",
|
|
81
|
-
"from okstra_ctl.migrate import (",
|
|
82
|
-
" prepare_migration_plan, apply_migration_plan, MigrationRefused,",
|
|
83
|
-
")",
|
|
84
|
-
"cwd, apply = sys.argv[1], sys.argv[2] == 'apply'",
|
|
85
|
-
"try:",
|
|
86
|
-
" # Resolver requires .okstra/project.json by default; for migrate",
|
|
87
|
-
" # we fall back to the cwd directly because the legacy layout has",
|
|
88
|
-
" # no .okstra/project.json yet.",
|
|
89
|
-
" try:",
|
|
90
|
-
" pr = resolve_project_root(explicit_root='', cwd=cwd)",
|
|
91
|
-
" except ResolverError:",
|
|
92
|
-
" pr = Path(cwd).resolve()",
|
|
93
|
-
" plan = prepare_migration_plan(pr)",
|
|
94
|
-
" if apply:",
|
|
95
|
-
" result = apply_migration_plan(plan, dry_run=False)",
|
|
96
|
-
" else:",
|
|
97
|
-
" result = apply_migration_plan(plan, dry_run=True)",
|
|
98
|
-
" print('OK')",
|
|
99
|
-
" print(json.dumps({'plan': plan.to_dict(), 'result': result.to_dict()}, ensure_ascii=False))",
|
|
100
|
-
"except MigrationRefused as e:",
|
|
101
|
-
" print('REFUSED')",
|
|
102
|
-
" print(str(e))",
|
|
103
|
-
" sys.exit(1)",
|
|
104
|
-
].join("\n"),
|
|
105
|
-
opts.cwd,
|
|
106
|
-
opts.apply ? "apply" : "dry",
|
|
107
|
-
],
|
|
108
|
-
{ PYTHONPATH: buildPythonpath(paths) },
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
if (py.code === 1) {
|
|
112
|
-
// Refused — pre-condition fail. Stderr message already on second line.
|
|
113
|
-
const reason = py.stdout.split("\n").slice(1).join("\n").trim() || py.stderr.trim();
|
|
114
|
-
if (!opts.quiet) process.stderr.write(`migrate refused: ${reason}\n`);
|
|
115
|
-
return 1;
|
|
116
|
-
}
|
|
117
|
-
if (py.code !== 0) {
|
|
118
|
-
process.stderr.write(`error: python invocation failed: ${py.stderr.trim() || py.stdout.trim()}\n`);
|
|
119
|
-
return 2;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const lines = py.stdout.split("\n");
|
|
123
|
-
if (lines[0]?.trim() !== "OK") {
|
|
124
|
-
process.stderr.write(`error: unexpected python output: ${py.stdout}\n`);
|
|
125
|
-
return 2;
|
|
126
|
-
}
|
|
127
|
-
let payload;
|
|
128
|
-
try {
|
|
129
|
-
payload = JSON.parse(lines.slice(1).join("\n"));
|
|
130
|
-
} catch (err) {
|
|
131
|
-
process.stderr.write(`error: failed to parse python output: ${err.message}\n`);
|
|
132
|
-
return 2;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
emit(opts, {
|
|
136
|
-
ok: true,
|
|
137
|
-
mode: opts.apply ? "apply" : "dry-run",
|
|
138
|
-
...payload,
|
|
139
|
-
});
|
|
140
|
-
if (!opts.apply && !opts.quiet) {
|
|
141
|
-
process.stderr.write(
|
|
142
|
-
"\nNo changes written. Re-run with --apply to perform the migration.\n",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
return 0;
|
|
146
|
-
}
|