okstra 0.54.0 → 0.56.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/bin/okstra +24 -7
- 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/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/usage.sh +5 -0
- package/runtime/bin/okstra-inject-report-index.py +66 -0
- package/runtime/bin/okstra.sh +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +23 -2
- package/runtime/prompts/profiles/final-verification.md +1 -0
- package/runtime/prompts/profiles/implementation-planning.md +4 -0
- package/runtime/prompts/profiles/improvement-discovery.md +1 -0
- package/runtime/python/okstra_ctl/clarification_items.py +10 -1
- package/runtime/python/okstra_ctl/conformance.py +270 -0
- package/runtime/python/okstra_ctl/paths.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +221 -2
- package/runtime/python/okstra_ctl/report_views.py +23 -4
- package/runtime/python/okstra_ctl/run.py +29 -0
- package/runtime/skills/okstra-run/SKILL.md +12 -0
- package/runtime/skills/okstra-setup/SKILL.md +35 -0
- package/runtime/templates/reports/i18n/en.json +6 -0
- package/runtime/templates/reports/i18n/ko.json +6 -0
- package/runtime/validators/lib/fixtures.sh +9 -0
- package/runtime/validators/validate-implementation-plan-stages.py +28 -3
- package/runtime/validators/validate-run.py +136 -1
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/src/okstra-dirs.mjs +1 -1
- package/src/migrate.mjs +0 -146
|
@@ -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
|
|
|
@@ -616,6 +625,19 @@ def _scan_token_usage_summary(content: str, failures: list[str]) -> None:
|
|
|
616
625
|
# a section heading line (not as inline text inside a paragraph or table).
|
|
617
626
|
_VERDICT_CARD_HEADING_RE = re.compile(r"^##[ \t]+Verdict Card\b", re.MULTILINE)
|
|
618
627
|
|
|
628
|
+
# Top-of-report Index block. The renderer
|
|
629
|
+
# (scripts/okstra_ctl/render_final_report.py) injects `<a id="report-index">`
|
|
630
|
+
# into the index heading; a missing anchor means the markdown was produced
|
|
631
|
+
# outside the renderer or hand-edited. Language-independent (the heading text
|
|
632
|
+
# itself is localized "Index" / "목차").
|
|
633
|
+
_REPORT_INDEX_ANCHOR_RE = re.compile(r'<a id="report-index"')
|
|
634
|
+
|
|
635
|
+
# An ID-defining table row: `| **FU-001**…` or `| C-001 |`. After the
|
|
636
|
+
# renderer's anchor pass the leading token becomes `<a id="…">…`, so a row
|
|
637
|
+
# still matching this (ID is the bare leading token) is one the renderer
|
|
638
|
+
# never anchored — i.e. an un-anchored ID that the Index cannot link to.
|
|
639
|
+
_UNANCHORED_ID_ROW_RE = re.compile(r"^\|[ \t]*\*{0,2}([A-Z]{1,4}-\d{3,})\b", re.MULTILINE)
|
|
640
|
+
|
|
619
641
|
# Reading Confirmation heading must NOT appear in the final-report — it
|
|
620
642
|
# belongs in the worker audit sidecar (`<worker>-audit-<task-type>-<seq>.md`).
|
|
621
643
|
_READING_CONFIRMATION_HEADING_RE = re.compile(
|
|
@@ -687,6 +709,84 @@ _DEPRECATED_FINAL_REPORT_PATTERNS: tuple[tuple[re.Pattern, str], ...] = (
|
|
|
687
709
|
)
|
|
688
710
|
|
|
689
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
|
+
|
|
690
790
|
def validate_report(
|
|
691
791
|
report_path: Path, required_agent_status_entries: list[str], failures: list[str]
|
|
692
792
|
) -> None:
|
|
@@ -723,6 +823,32 @@ def validate_report(
|
|
|
723
823
|
"the corresponding cells in `## 7. Final Verdict` and `## 3.` first item."
|
|
724
824
|
)
|
|
725
825
|
|
|
826
|
+
# Top-of-report Index (목차 / Index) is mandatory in every final-report so
|
|
827
|
+
# the reader can jump to any section / tracked ID. Schema task-types get it
|
|
828
|
+
# from render_final_report.py; `improvement-discovery` (authored free-form)
|
|
829
|
+
# gets it from the `okstra-inject-report-index.py` post-step. A missing
|
|
830
|
+
# index anchor means that injection never ran.
|
|
831
|
+
if _REPORT_INDEX_ANCHOR_RE.search(content) is None:
|
|
832
|
+
failures.append(
|
|
833
|
+
"final report is missing the top-of-report Index block "
|
|
834
|
+
'(`## Index` / `## 목차` carrying `<a id="report-index">`). It is '
|
|
835
|
+
"injected by scripts/okstra_ctl/render_final_report.py (schema "
|
|
836
|
+
"task-types) or scripts/okstra-inject-report-index.py "
|
|
837
|
+
"(improvement-discovery); a missing index means that step never ran."
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Every ID-defining table row (FU-/E-/S-/C-/R-/I-/… in the first cell) must
|
|
841
|
+
# carry a scroll anchor so the Index can link to it. A row still matching
|
|
842
|
+
# the bare-leading-token shape is one the injector never anchored.
|
|
843
|
+
for match in _UNANCHORED_ID_ROW_RE.finditer(content):
|
|
844
|
+
failures.append(
|
|
845
|
+
f"final report has an ID-defining table row for `{match.group(1)}` "
|
|
846
|
+
'without a scroll anchor (`<a id="…">`). IDs must be anchored so the '
|
|
847
|
+
"top-of-report Index can link to them — run the index injector "
|
|
848
|
+
"(render_final_report.py / okstra-inject-report-index.py) instead of "
|
|
849
|
+
"hand-editing."
|
|
850
|
+
)
|
|
851
|
+
|
|
726
852
|
# Reading Confirmation belongs in the worker audit sidecar, not the
|
|
727
853
|
# user-facing final-report.
|
|
728
854
|
if _READING_CONFIRMATION_HEADING_RE.search(content) is not None:
|
|
@@ -1870,14 +1996,23 @@ def main() -> int:
|
|
|
1870
1996
|
# data.json is well-formed, the rendered markdown is guaranteed to
|
|
1871
1997
|
# contain every required section. Substring checks below are a
|
|
1872
1998
|
# safety net for hand-edited or pre-v1.0 reports.
|
|
1999
|
+
task_type = effective_run_task_type(run_manifest, task_manifest)
|
|
1873
2000
|
validate_final_report_data(report_path, failures)
|
|
1874
2001
|
validate_report(report_path, contract["required_agent_status_entries"], failures)
|
|
1875
2002
|
validate_team_state_usage(team_state, failures)
|
|
1876
2003
|
|
|
1877
|
-
task_type = effective_run_task_type(run_manifest, task_manifest)
|
|
1878
2004
|
validate_phase_boundary(task_type, report_path, failures)
|
|
1879
2005
|
if task_type:
|
|
1880
2006
|
validate_worker_results_audit(report_path, task_type, failures)
|
|
2007
|
+
if task_type in ("implementation", "final-verification"):
|
|
2008
|
+
_sp = None
|
|
2009
|
+
_pj = project_json_path(project_root)
|
|
2010
|
+
if _pj.is_file():
|
|
2011
|
+
try:
|
|
2012
|
+
_sp = (json.loads(_pj.read_text()).get("qaEnv") or {}).get("surfacePatterns")
|
|
2013
|
+
except (OSError, json.JSONDecodeError):
|
|
2014
|
+
_sp = None
|
|
2015
|
+
_validate_conformance(report_path, failures, surface_patterns=_sp)
|
|
1881
2016
|
if task_type == "improvement-discovery":
|
|
1882
2017
|
brief_relative = str(task_manifest.get("taskBriefPath") or "").strip()
|
|
1883
2018
|
brief_path = (
|
|
@@ -30,6 +30,10 @@ _NEXT_PHASES = ("requirements-discovery", "implementation-planning", "error-anal
|
|
|
30
30
|
_CAND_ID_RE = re.compile(r"^I-\d{3}$")
|
|
31
31
|
_SOURCE_WORKER_RE = re.compile(r"^([a-z-]+):([A-Za-z0-9._-]+)$")
|
|
32
32
|
_CONSENSUS_VALUES = ("full", "partial", "contested", "worker-unique")
|
|
33
|
+
# The index/anchor post-pass (okstra-inject-report-index.py) injects an empty
|
|
34
|
+
# scroll anchor into ID-defining first cells (`<a id="i-001"></a>I-001`). Strip
|
|
35
|
+
# it during cell normalization so `_CAND_ID_RE` still matches the bare `I-NNN`.
|
|
36
|
+
_CELL_ANCHOR_RE = re.compile(r'<a id="[^"]*"></a>')
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
@dataclass
|
|
@@ -53,7 +57,7 @@ def _read_section_table(body: str, heading: str) -> list[list[str]]:
|
|
|
53
57
|
s = line.strip()
|
|
54
58
|
if not s.startswith("|") or not s.endswith("|"):
|
|
55
59
|
continue
|
|
56
|
-
cells = [c.strip() for c in s.strip("|").split("|")]
|
|
60
|
+
cells = [_CELL_ANCHOR_RE.sub("", c).strip() for c in s.strip("|").split("|")]
|
|
57
61
|
if all(set(c) <= set("-: ") for c in cells):
|
|
58
62
|
continue
|
|
59
63
|
rows.append(cells)
|
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
|
-
}
|