feat: rename bugfix-ship/requirement-flow to short-ship/long-ship with shared ship records
short-ship and long-ship share one ship-id record under .pouch/ship/<id>/. specifier writes PRDs into spec/; delivery-loop appends evidence pointers. Old names stay as deprecation stubs so existing installs keep working.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a .pouch/ship/<id>/ record from templates. Refuse to overwrite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ship_lib import check_ship_id, skill_dir
|
||||
from validate_ship import validate_ship_dir
|
||||
|
||||
|
||||
def _replace(text: str, mapping: dict[str, str]) -> str:
|
||||
for key, value in mapping.items():
|
||||
text = text.replace(f"<{key}>", value)
|
||||
text = text.replace(f"{{{{ {key} }}}}", value)
|
||||
return text
|
||||
|
||||
|
||||
def create_ship(
|
||||
project_root: Path,
|
||||
ship_id: str,
|
||||
kind: str,
|
||||
title: str,
|
||||
*,
|
||||
stop_at: str | None = None,
|
||||
source_kind: str = "user",
|
||||
source_ref: str | None = None,
|
||||
) -> Path:
|
||||
err = check_ship_id(ship_id)
|
||||
if err:
|
||||
raise SystemExit(err)
|
||||
if kind not in {"short", "long"}:
|
||||
raise SystemExit(f"kind 无效: {kind}")
|
||||
title = title.strip()
|
||||
if not title:
|
||||
raise SystemExit("title 不能为空")
|
||||
|
||||
dest = project_root / ".pouch" / "ship" / ship_id
|
||||
if dest.exists():
|
||||
raise SystemExit(f"拒绝覆盖已有路径: {dest}")
|
||||
|
||||
templates = skill_dir() / "templates"
|
||||
dest.mkdir(parents=True)
|
||||
mapping = {
|
||||
"ship-id": ship_id,
|
||||
"title": title,
|
||||
"kind": kind,
|
||||
"short title": title,
|
||||
}
|
||||
|
||||
current = _replace(
|
||||
(templates / "current.template.yaml").read_text(encoding="utf-8"),
|
||||
mapping,
|
||||
)
|
||||
current = current.replace("kind: short", f"kind: {kind}", 1)
|
||||
current = current.replace("status: draft", "status: draft", 1)
|
||||
if stop_at:
|
||||
current = current.replace("stopAt: null", f"stopAt: {stop_at}")
|
||||
if source_kind != "user":
|
||||
current = current.replace("kind: user", f"kind: {source_kind}", 1)
|
||||
if source_ref:
|
||||
current = current.replace("ref: null", f"ref: {source_ref}", 1)
|
||||
if kind == "long":
|
||||
current = current.replace("status: draft", "status: spec_review", 1)
|
||||
|
||||
(dest / "current.yaml").write_text(current, encoding="utf-8")
|
||||
(dest / "spec.md").write_text(
|
||||
_replace((templates / "spec.template.md").read_text(encoding="utf-8"), mapping),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(dest / "tasks.yaml").write_text(
|
||||
_replace(
|
||||
(templates / "tasks.template.yaml").read_text(encoding="utf-8"), mapping
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(dest / "status.md").write_text(
|
||||
_replace(
|
||||
(templates / "status.template.md").read_text(encoding="utf-8"),
|
||||
{**mapping, "status": "spec_review" if kind == "long" else "draft",
|
||||
"stopAt": stop_at or "null", "delivery.state": "change_ready"},
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
if kind == "long":
|
||||
spec_dir = dest / "spec"
|
||||
spec_dir.mkdir()
|
||||
(spec_dir / "brief.md").write_text(f"# {title}\n\n", encoding="utf-8")
|
||||
(spec_dir / "review.yaml").write_text("[]\n", encoding="utf-8")
|
||||
(spec_dir / "decisions.yaml").write_text("[]\n", encoding="utf-8")
|
||||
(spec_dir / "risks.yaml").write_text("[]\n", encoding="utf-8")
|
||||
|
||||
issues = validate_ship_dir(dest)
|
||||
if issues:
|
||||
shutil.rmtree(dest)
|
||||
for item in issues:
|
||||
print(f"✗ {item}", file=sys.stderr)
|
||||
raise SystemExit("新建后校验失败,已删除不完整目录")
|
||||
return dest
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="新建 ship 记录")
|
||||
parser.add_argument("--project-root", required=True)
|
||||
parser.add_argument("--ship-id", required=True)
|
||||
parser.add_argument("--kind", choices=("short", "long"), required=True)
|
||||
parser.add_argument("--title", required=True)
|
||||
parser.add_argument("--stop-at", default=None)
|
||||
parser.add_argument("--source-kind", default="user")
|
||||
parser.add_argument("--source-ref", default=None)
|
||||
args = parser.parse_args()
|
||||
dest = create_ship(
|
||||
Path(args.project_root).resolve(),
|
||||
args.ship_id,
|
||||
args.kind,
|
||||
args.title,
|
||||
stop_at=args.stop_at,
|
||||
source_kind=args.source_kind,
|
||||
source_ref=args.source_ref,
|
||||
)
|
||||
print(dest)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user