@devcxl/opencode-cabbage 1.3.0 → 2.0.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/.codex-plugin/plugin.json +1 -1
- package/README.md +35 -23
- package/README.zh.md +22 -15
- package/assets/agents/dev-lifecycle.md +6 -0
- package/assets/prompts/bootstrap.md +4 -0
- package/assets/skills/flow-change/SKILL.md +109 -0
- package/assets/skills/flow-design/SKILL.md +15 -0
- package/assets/skills/flow-docs/SKILL.md +112 -0
- package/assets/skills/flow-incident/SKILL.md +98 -0
- package/assets/skills/flow-research/SKILL.md +9 -0
- package/assets/skills/flow-setup/SKILL.md +13 -0
- package/codex-skills/agent-dev-lifecycle/SKILL.md +2 -0
- package/codex-skills/flow-change/SKILL.md +109 -0
- package/codex-skills/flow-design/SKILL.md +18 -3
- package/codex-skills/flow-docs/SKILL.md +112 -0
- package/codex-skills/flow-incident/SKILL.md +98 -0
- package/codex-skills/flow-research/SKILL.md +12 -3
- package/codex-skills/flow-setup/SKILL.md +13 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/plugin/agents.d.ts +20 -0
- package/dist/plugin/agents.js +79 -0
- package/dist/plugin/agents.js.map +1 -1
- package/dist/plugin/cabbage-cli.d.ts +28 -0
- package/dist/plugin/cabbage-cli.js +92 -0
- package/dist/plugin/cabbage-cli.js.map +1 -0
- package/dist/plugin/cabbage-tool.d.ts +103 -0
- package/dist/plugin/cabbage-tool.js +169 -0
- package/dist/plugin/cabbage-tool.js.map +1 -0
- package/dist/plugin/commands.d.ts +10 -0
- package/dist/plugin/commands.js +1 -1
- package/dist/plugin/commands.js.map +1 -1
- package/dist/plugin/continuation.d.ts +22 -0
- package/dist/plugin/continuation.js +233 -0
- package/dist/plugin/continuation.js.map +1 -0
- package/dist/plugin/goal-store.d.ts +42 -49
- package/dist/plugin/goal-store.js +35 -32
- package/dist/plugin/goal-store.js.map +1 -1
- package/dist/plugin/goal.d.ts +92 -28
- package/dist/plugin/goal.js +201 -158
- package/dist/plugin/goal.js.map +1 -1
- package/dist/plugin/install-dir.d.ts +13 -0
- package/dist/plugin/install-dir.js +46 -0
- package/dist/plugin/install-dir.js.map +1 -0
- package/dist/plugin/server.d.ts +4 -13
- package/dist/plugin/server.js +278 -423
- package/dist/plugin/server.js.map +1 -1
- package/dist/plugin/session-abort.d.ts +5 -0
- package/dist/plugin/session-abort.js +26 -0
- package/dist/plugin/session-abort.js.map +1 -0
- package/dist/plugin/skills.d.ts +15 -0
- package/dist/plugin/skills.js +47 -50
- package/dist/plugin/skills.js.map +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +6 -2
- package/dist/plugin.js.map +1 -1
- package/dist/util/paths.d.ts +0 -6
- package/dist/util/paths.js +0 -15
- package/dist/util/paths.js.map +1 -1
- package/package.json +4 -7
- package/vendor/cabbage/MANIFEST.json +8 -0
- package/vendor/cabbage/cabbage_cli/__init__.py +1 -0
- package/vendor/cabbage/cabbage_cli/__main__.py +2 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/.vitepress/config.ts +78 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/README.md +31 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/package.json +16 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/pnpm-lock.yaml +2504 -0
- package/vendor/cabbage/cabbage_cli/assets/integrations/cabbage.yml +127 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/adr.md +47 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/api-design.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/change-record.md +21 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/database-design.md +37 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/generic.md +20 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/impact.md +41 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/incident.md +48 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/postmortem.md +43 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/prd.md +71 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/release-plan.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/rfc.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/security-review.md +35 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tasks.md +62 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tech-spec.md +87 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/test-plan.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/change.yaml +11 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/incident.yaml +4 -0
- package/vendor/cabbage/cabbage_cli/cli.py +283 -0
- package/vendor/cabbage/cabbage_cli/core.py +606 -0
- package/vendor/cabbage/cabbage_cli/scaffold.py +313 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import shutil
|
|
3
|
+
from importlib import resources
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
from .core import (CabbageError, dump_yaml, load_config, load_yaml, change_dir, now_iso,
|
|
7
|
+
project_root, workflow, condition_enabled, write_text_atomic, CABBAGE_DIR)
|
|
8
|
+
|
|
9
|
+
IMPACT_FIELD_LABELS={
|
|
10
|
+
"product":"Product","design":"Design","architecture":"Architecture","rfc":"RFC","api":"API","database":"Database",
|
|
11
|
+
"security":"Security","testing":"Testing","deployment":"Deployment","operations":"Operations",
|
|
12
|
+
"performance":"Performance"
|
|
13
|
+
}
|
|
14
|
+
IMPACT_FIELDS=list(IMPACT_FIELD_LABELS)
|
|
15
|
+
|
|
16
|
+
ADOPTION_IGNORED_DIRS={".git",".cabbage",".github",".idea",".vscode",".venv","venv","node_modules","__pycache__","dist","build","target","coverage","site",".cache",".temp",".vitepress",".vuepress","vendor"}
|
|
17
|
+
|
|
18
|
+
ADOPTION_CATEGORY_RULES=[
|
|
19
|
+
("adr",{"adr"}),
|
|
20
|
+
("rfc",{"rfc","proposal","proposals"}),
|
|
21
|
+
("incident",{"incident","incidents","postmortem","postmortems","retrospective","retro"}),
|
|
22
|
+
("api",{"api","apis","openapi","swagger","webhook","webhooks"}),
|
|
23
|
+
("data",{"database","databases","db","schema","schemas","migration","migrations","erd","datamodel","domain","domains"}),
|
|
24
|
+
("security",{"security","threat","threats"}),
|
|
25
|
+
("testing",{"test","tests","testing","qa","e2e"}),
|
|
26
|
+
("infrastructure",{"infrastructure","infra","kubernetes","k8s","terraform","helm","ansible"}),
|
|
27
|
+
("ci-cd",{"ci","cd","pipeline","pipelines","jenkins"}),
|
|
28
|
+
("release",{"release","releases","changelog","changelogs"}),
|
|
29
|
+
("operations",{"runbook","runbooks","oncall","operations","ops","monitoring","alerting","observability","slo","sli"}),
|
|
30
|
+
("performance",{"performance","benchmark","benchmarks","load"}),
|
|
31
|
+
("product",{"prd","requirement","requirements","user-story","user-stories","product","roadmap"}),
|
|
32
|
+
("architecture",{"architecture","system-design","tech-design","design","design-doc"}),
|
|
33
|
+
("development",{"development","contributing","onboarding","setup","installation","getting-started"}),
|
|
34
|
+
("compliance",{"compliance","policy","policies","governance"}),
|
|
35
|
+
("overview",{"overview","about","introduction","readme"}),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
ADOPTION_TARGET_BY_CATEGORY={
|
|
39
|
+
"adr":"docs/03-architecture/adr","rfc":"docs/03-architecture/rfc","incident":"docs/15-incidents",
|
|
40
|
+
"product":"docs/01-product","architecture":"docs/03-architecture","data":"docs/04-domain","api":"docs/05-api",
|
|
41
|
+
"development":"docs/06-development","testing":"docs/08-testing","security":"docs/09-security",
|
|
42
|
+
"infrastructure":"docs/10-infrastructure","ci-cd":"docs/11-ci-cd","release":"docs/12-release",
|
|
43
|
+
"operations":"docs/13-operations","performance":"docs/14-performance","compliance":"docs/17-compliance",
|
|
44
|
+
"overview":"docs/00-overview",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ADOPTION_HISTORICAL={"adr","rfc","incident"}
|
|
48
|
+
|
|
49
|
+
ADOPTION_CONFORMING_AREAS={
|
|
50
|
+
"00":"overview","01":"product","02":"design","03":"architecture","04":"data","05":"api",
|
|
51
|
+
"06":"development","07":"standards","08":"testing","09":"security","10":"infrastructure",
|
|
52
|
+
"11":"ci-cd","12":"release","13":"operations","14":"performance","15":"incident","16":"dependencies","17":"compliance",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ALL_CONFORMING_DIRS = [
|
|
56
|
+
"00-overview", "01-product", "02-design", "03-architecture/adr", "03-architecture/rfc",
|
|
57
|
+
"03-architecture/system-design", "04-domain", "05-api", "06-development", "07-standards",
|
|
58
|
+
"08-testing", "09-security", "10-infrastructure", "11-ci-cd", "12-release",
|
|
59
|
+
"13-operations", "14-performance", "15-incidents", "16-dependencies", "17-compliance"
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def asset(path: str):
|
|
63
|
+
return resources.files("cabbage_cli").joinpath("assets", path)
|
|
64
|
+
|
|
65
|
+
def copy_asset_tree(src_rel: str, dst: Path, overwrite: bool=False):
|
|
66
|
+
src=asset(src_rel)
|
|
67
|
+
dst.mkdir(parents=True,exist_ok=True)
|
|
68
|
+
for item in src.iterdir():
|
|
69
|
+
target=dst/item.name
|
|
70
|
+
if item.is_dir(): copy_asset_tree(f"{src_rel}/{item.name}", target, overwrite)
|
|
71
|
+
elif overwrite or not target.exists():
|
|
72
|
+
target.write_bytes(item.read_bytes())
|
|
73
|
+
|
|
74
|
+
def init_project(root: Path, force: bool=False, vendor_cli: bool=True):
|
|
75
|
+
d=root/CABBAGE_DIR
|
|
76
|
+
if (d/"config.yaml").exists() and not force:
|
|
77
|
+
raise CabbageError(f"{CABBAGE_DIR} already initialized; use --force to refresh missing scaffold files")
|
|
78
|
+
d.mkdir(parents=True,exist_ok=True)
|
|
79
|
+
cfg={
|
|
80
|
+
"version":1,
|
|
81
|
+
"impact_fields":IMPACT_FIELDS,
|
|
82
|
+
"ci":{
|
|
83
|
+
"require_change_for_code":True,
|
|
84
|
+
"require_current_state_docs":True,
|
|
85
|
+
"exclude_prefixes":["docs/",".cabbage/",".github/","README","LICENSE"],
|
|
86
|
+
"current_state_rules":{
|
|
87
|
+
"product":["docs/01-product/"],
|
|
88
|
+
"architecture":["docs/03-architecture/"],
|
|
89
|
+
"api":["docs/05-api/"],
|
|
90
|
+
"database":["docs/04-domain/"],
|
|
91
|
+
"security":["docs/09-security/"],
|
|
92
|
+
"testing":["docs/08-testing/"],
|
|
93
|
+
"deployment":["docs/10-infrastructure/","docs/11-ci-cd/","docs/12-release/"],
|
|
94
|
+
"operations":["docs/13-operations/"],
|
|
95
|
+
"performance":["docs/14-performance/"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"docs":{"dir":"docs"}
|
|
99
|
+
}
|
|
100
|
+
dump_yaml(d/"config.yaml",cfg)
|
|
101
|
+
copy_asset_tree("workflows",d/"workflows",overwrite=force)
|
|
102
|
+
(d/"changes").mkdir(exist_ok=True); (d/"archive").mkdir(exist_ok=True)
|
|
103
|
+
copy_asset_tree("docs-site",root/"docs",overwrite=False)
|
|
104
|
+
# current-state doc skeleton
|
|
105
|
+
for name in ALL_CONFORMING_DIRS:
|
|
106
|
+
(root/"docs"/name).mkdir(parents=True,exist_ok=True)
|
|
107
|
+
# CI template
|
|
108
|
+
gh=root/".github/workflows"; gh.mkdir(parents=True,exist_ok=True)
|
|
109
|
+
ci=asset("integrations/cabbage.yml").read_text(encoding="utf-8")
|
|
110
|
+
cip=gh/"cabbage.yml"
|
|
111
|
+
if not cip.exists(): cip.write_text(ci,encoding="utf-8")
|
|
112
|
+
gi=root/".gitignore"
|
|
113
|
+
ignores=["docs/node_modules/","docs/.vitepress/cache/","docs/.vitepress/dist/"]
|
|
114
|
+
existing=gi.read_text(encoding="utf-8") if gi.exists() else ""
|
|
115
|
+
add=[x for x in ignores if x not in existing.splitlines()]
|
|
116
|
+
if add:
|
|
117
|
+
with gi.open("a",encoding="utf-8") as f:
|
|
118
|
+
if existing and not existing.endswith("\n"): f.write("\n")
|
|
119
|
+
f.write("\n# cabbage / VitePress\n"+"\n".join(add)+"\n")
|
|
120
|
+
if vendor_cli:
|
|
121
|
+
sync_vendored_cli(root)
|
|
122
|
+
|
|
123
|
+
def sync_vendored_cli(root: Path):
|
|
124
|
+
"""Regenerate the repository-local CLI from the installed source package."""
|
|
125
|
+
pkg = Path(__file__).resolve().parent
|
|
126
|
+
target = root / CABBAGE_DIR / "tooling/cabbage_cli"
|
|
127
|
+
if pkg == target.resolve():
|
|
128
|
+
raise CabbageError("cannot refresh vendored CLI from itself; use the source package")
|
|
129
|
+
if target.exists():
|
|
130
|
+
shutil.rmtree(target)
|
|
131
|
+
shutil.copytree(pkg, target, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
|
132
|
+
|
|
133
|
+
def render_template(stage: dict, change_id: str, change_type: str) -> str:
|
|
134
|
+
template=stage.get("template","generic.md")
|
|
135
|
+
txt=asset(f"templates/{template}").read_text(encoding="utf-8")
|
|
136
|
+
return txt.replace("{{CHANGE_ID}}",change_id).replace("{{STAGE_ID}}",stage["id"]).replace("{{CHANGE_TYPE}}",change_type)
|
|
137
|
+
|
|
138
|
+
def sync_impact_document(root: Path, change_id: str):
|
|
139
|
+
p=change_dir(root,change_id)/"impact.md"
|
|
140
|
+
if not p.exists(): return
|
|
141
|
+
spec=load_yaml(change_dir(root,change_id)/"change.yaml")
|
|
142
|
+
text=p.read_text(encoding="utf-8")
|
|
143
|
+
for key,label in IMPACT_FIELD_LABELS.items():
|
|
144
|
+
val="Yes" if spec.get("impact",{}).get(key,False) else "No"
|
|
145
|
+
pattern=rf"^(\|\s*{re.escape(label)}\s*\|)\s*(Yes|No)\s*(\|.*)$"
|
|
146
|
+
text=re.sub(pattern,rf"\1 {val} \3",text,flags=re.M|re.I)
|
|
147
|
+
write_text_atomic(p,text,encoding="utf-8")
|
|
148
|
+
|
|
149
|
+
def ensure_artifacts(root: Path, change_id: str):
|
|
150
|
+
spec=load_yaml(change_dir(root,change_id)/"change.yaml")
|
|
151
|
+
wf,_=workflow(root,spec["type"])
|
|
152
|
+
for st in wf.get("stages",[]):
|
|
153
|
+
if condition_enabled(st,spec) and st.get("artifact"):
|
|
154
|
+
p=change_dir(root,change_id)/st["artifact"]
|
|
155
|
+
if not p.exists():
|
|
156
|
+
p.parent.mkdir(parents=True,exist_ok=True)
|
|
157
|
+
p.write_text(render_template(st,change_id,spec["type"]),encoding="utf-8")
|
|
158
|
+
sync_impact_document(root,change_id)
|
|
159
|
+
|
|
160
|
+
def new_change(root: Path, change_type: str, change_id: str):
|
|
161
|
+
cfg=load_config(root)
|
|
162
|
+
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*",change_id):
|
|
163
|
+
raise CabbageError("change id must be kebab-case")
|
|
164
|
+
workflow(root,change_type) # raises `unknown change type` when missing
|
|
165
|
+
d=change_dir(root,change_id)
|
|
166
|
+
if d.exists(): raise CabbageError(f"change already exists: {change_id}")
|
|
167
|
+
d.mkdir(parents=True)
|
|
168
|
+
spec={"id":change_id,"type":change_type,"status":"active","impact":{k:False for k in cfg.get("impact_fields",[])}}
|
|
169
|
+
# Defaults declare only impacts the workflow can produce evidence for; the rest stay false for explicit opt-in.
|
|
170
|
+
# Legacy types keep their defaults so existing projects behave exactly as before the new-project model.
|
|
171
|
+
if change_type!="incident": spec["impact"]["testing"]=True
|
|
172
|
+
if change_type=="change" or change_type=="feature": spec["impact"]["product"]=True
|
|
173
|
+
if change_type=="architecture": spec["impact"]["architecture"]=True
|
|
174
|
+
if change_type in {"migration"}: spec["impact"]["database"]=True; spec["impact"]["deployment"]=True
|
|
175
|
+
if change_type=="integration": spec["impact"]["api"]=True
|
|
176
|
+
dump_yaml(d/"change.yaml",spec)
|
|
177
|
+
ensure_artifacts(root,change_id)
|
|
178
|
+
|
|
179
|
+
def iter_adoption_docs(root: Path):
|
|
180
|
+
docs_name=load_config(root).get("docs",{}).get("dir","docs")
|
|
181
|
+
for p in sorted(root.rglob("*.md")):
|
|
182
|
+
rel=p.relative_to(root)
|
|
183
|
+
parts=rel.parts
|
|
184
|
+
if not parts or parts[0] in ADOPTION_IGNORED_DIRS or parts[0].startswith("."):
|
|
185
|
+
continue
|
|
186
|
+
if parts[0]==docs_name:
|
|
187
|
+
if conforming_area(rel,docs_name):
|
|
188
|
+
yield rel
|
|
189
|
+
continue
|
|
190
|
+
if any(part in ADOPTION_IGNORED_DIRS for part in parts[:-1]):
|
|
191
|
+
continue
|
|
192
|
+
if p.name.upper()=="README.md" and len(parts)==1:
|
|
193
|
+
continue
|
|
194
|
+
yield rel
|
|
195
|
+
|
|
196
|
+
def classify_adoption_doc(rel: Path) -> tuple[str,str|None,str|None]:
|
|
197
|
+
name_parts=[p.lower() for p in rel.with_suffix("").parts]
|
|
198
|
+
category=next((c for c,hints in ADOPTION_CATEGORY_RULES if any(h in name_parts for h in hints)),None)
|
|
199
|
+
if category is None:
|
|
200
|
+
lowered="/".join(name_parts)
|
|
201
|
+
category=next((c for c,hints in ADOPTION_CATEGORY_RULES
|
|
202
|
+
if any(re.search(rf"(^|[-_/]){re.escape(h)}([-_/]|$)",lowered) for h in hints)),None)
|
|
203
|
+
if category is None:
|
|
204
|
+
return "review",None,None
|
|
205
|
+
action="import" if category in ADOPTION_HISTORICAL else "migrate"
|
|
206
|
+
return action,category,ADOPTION_TARGET_BY_CATEGORY[category]
|
|
207
|
+
|
|
208
|
+
def conforming_area(rel: Path, docs_name: str|None=None) -> str|None:
|
|
209
|
+
parts=rel.parts
|
|
210
|
+
if docs_name and parts and parts[0]==docs_name:
|
|
211
|
+
parts=parts[1:]
|
|
212
|
+
first=parts[0] if parts else ""
|
|
213
|
+
m=re.match(r"^(\d{2})-",first)
|
|
214
|
+
if m and m.group(1) in ADOPTION_CONFORMING_AREAS:
|
|
215
|
+
return ADOPTION_CONFORMING_AREAS[m.group(1)]
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def discard_change(root: Path, change_id: str) -> None:
|
|
219
|
+
d = change_dir(root, change_id)
|
|
220
|
+
if not d.exists():
|
|
221
|
+
raise CabbageError(f"change `{change_id}` does not exist")
|
|
222
|
+
shutil.rmtree(d)
|
|
223
|
+
|
|
224
|
+
def adopt_project(root: Path, apply: bool = False) -> dict:
|
|
225
|
+
project_root(root)
|
|
226
|
+
load_config(root)
|
|
227
|
+
rows=[]; counts={"migrate":0,"import":0,"keep":0,"review":0}
|
|
228
|
+
docs_name=load_config(root).get("docs",{}).get("dir","docs")
|
|
229
|
+
for rel in iter_adoption_docs(root):
|
|
230
|
+
area=conforming_area(rel,docs_name)
|
|
231
|
+
if area:
|
|
232
|
+
action,category,target="keep",area,None
|
|
233
|
+
else:
|
|
234
|
+
action,category,target=classify_adoption_doc(rel)
|
|
235
|
+
counts[action]+=1
|
|
236
|
+
rows.append({"path":str(rel).replace("\\","/"),"action":action,"category":category,"target":target})
|
|
237
|
+
|
|
238
|
+
applied = []
|
|
239
|
+
warnings = []
|
|
240
|
+
if apply:
|
|
241
|
+
for r in rows:
|
|
242
|
+
if r["action"] in {"migrate", "import"} and r["target"]:
|
|
243
|
+
src_path = root / r["path"]
|
|
244
|
+
dst_dir = root / r["target"]
|
|
245
|
+
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
dst_path = dst_dir / src_path.name
|
|
247
|
+
if not dst_path.exists():
|
|
248
|
+
shutil.move(str(src_path), str(dst_path))
|
|
249
|
+
if r["action"] == "migrate":
|
|
250
|
+
try:
|
|
251
|
+
text = dst_path.read_text(encoding="utf-8")
|
|
252
|
+
if not text.startswith("---\n"):
|
|
253
|
+
fm = f"---\ncategory: {r['category']}\nadopted_at: {now_iso()}\n---\n\n"
|
|
254
|
+
dst_path.write_text(fm + text, encoding="utf-8")
|
|
255
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
256
|
+
warnings.append(f"{dst_path.relative_to(root)}: frontmatter not added ({e})")
|
|
257
|
+
applied.append({"from": r["path"], "to": str(dst_path.relative_to(root)).replace("\\", "/")})
|
|
258
|
+
|
|
259
|
+
if applied:
|
|
260
|
+
# Re-scan after applying
|
|
261
|
+
res = adopt_project(root, apply=False)
|
|
262
|
+
res["applied"] = applied
|
|
263
|
+
res["warnings"] = warnings + res.get("warnings", [])
|
|
264
|
+
return res
|
|
265
|
+
|
|
266
|
+
report=render_adoption_report(root,rows)
|
|
267
|
+
out=root/CABBAGE_DIR/"adoption-report.md"
|
|
268
|
+
out.parent.mkdir(parents=True,exist_ok=True)
|
|
269
|
+
out.write_text(report,encoding="utf-8")
|
|
270
|
+
return {"report":str(out.relative_to(root)),"counts":counts,"documents":rows, "applied": applied, "warnings": warnings}
|
|
271
|
+
|
|
272
|
+
def render_adoption_report(root: Path, rows: list[dict]) -> str:
|
|
273
|
+
docs_name=load_config(root).get("docs",{}).get("dir","docs")
|
|
274
|
+
lines=[
|
|
275
|
+
"---",
|
|
276
|
+
"cabbage_stage: adoption",
|
|
277
|
+
f"generated_at: {now_iso()}",
|
|
278
|
+
"---",
|
|
279
|
+
"",
|
|
280
|
+
"# Adoption report",
|
|
281
|
+
"",
|
|
282
|
+
"Inventory of existing documentation. This report is advisory only; no files were moved.",
|
|
283
|
+
"",
|
|
284
|
+
"## Action legend",
|
|
285
|
+
"",
|
|
286
|
+
"| Action | Meaning |",
|
|
287
|
+
"|---|---|",
|
|
288
|
+
"| keep | Already inside the standard current-state tree; no move needed |",
|
|
289
|
+
"| migrate | Current-state document; move into the standard tree during adoption |",
|
|
290
|
+
"| import | Historical record (ADR/RFC/incident); archive as-is under the standard tree |",
|
|
291
|
+
"| review | Unclassified; a human decides whether to migrate, import, or leave it |",
|
|
292
|
+
"",
|
|
293
|
+
"## Documents",
|
|
294
|
+
"",
|
|
295
|
+
"| Path | Action | Category | Suggested target |",
|
|
296
|
+
"|---|---|---|---|",
|
|
297
|
+
]
|
|
298
|
+
for r in rows:
|
|
299
|
+
lines.append(f"| `{r['path']}` | {r['action']} | {r['category'] or '-'} | `{r['target'] or '-'}` |")
|
|
300
|
+
lines+=[
|
|
301
|
+
"",
|
|
302
|
+
"## Adoption steps",
|
|
303
|
+
"",
|
|
304
|
+
"1. Resolve each `review` row by hand, then re-run this command.",
|
|
305
|
+
"2. Move `migrate` rows into their suggested target and fix intra-project links.",
|
|
306
|
+
"3. Import `import` rows as immutable history: do not rewrite their content.",
|
|
307
|
+
"4. Record completed moves in a change record (`cabbage new change adopt-existing-docs`).",
|
|
308
|
+
"5. Verify the current-state site (`cabbage docs build`), then enable CI gates.",
|
|
309
|
+
"",
|
|
310
|
+
f"The standard current-state tree is `{docs_name}/`; see the cabbage-docs skill for the layout.",
|
|
311
|
+
"",
|
|
312
|
+
]
|
|
313
|
+
return "\n".join(lines)
|