its-magic 0.1.2-9 → 0.1.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.
Files changed (232) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +21 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +981 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +284 -0
  9. package/scripts/doc_profile_lib.py +415 -0
  10. package/scripts/guard_installer_publish.py +88 -0
  11. package/scripts/intake_bug_routing_guard.py +67 -0
  12. package/scripts/intake_evidence_lib.py +802 -0
  13. package/scripts/intake_evidence_validate.py +73 -0
  14. package/scripts/materialize_codebase_map.py +184 -0
  15. package/scripts/remote_config_summary.py +243 -0
  16. package/template/.cursor/agents/curator.mdc +35 -0
  17. package/template/.cursor/agents/dev.mdc +29 -0
  18. package/template/.cursor/agents/po.mdc +141 -0
  19. package/template/.cursor/agents/qa.mdc +28 -0
  20. package/template/.cursor/agents/release.mdc +28 -0
  21. package/template/.cursor/agents/security.mdc +98 -0
  22. package/template/.cursor/agents/tech-lead.mdc +57 -0
  23. package/template/.cursor/commands/architecture.md +127 -0
  24. package/template/.cursor/commands/ask.md +55 -0
  25. package/template/.cursor/commands/auto.md +533 -0
  26. package/template/.cursor/commands/discovery.md +47 -0
  27. package/template/.cursor/commands/execute.md +343 -0
  28. package/template/.cursor/commands/intake.md +323 -0
  29. package/template/.cursor/commands/map-codebase.md +46 -0
  30. package/template/.cursor/commands/memory-audit.md +175 -0
  31. package/template/.cursor/commands/milestone-complete.md +51 -0
  32. package/template/.cursor/commands/milestone-start.md +59 -0
  33. package/template/.cursor/commands/pause.md +64 -0
  34. package/{.cursor/commands/gsd-phase-context.md → template/.cursor/commands/phase-context.md} +6 -2
  35. package/template/.cursor/commands/plan-verify.md +34 -0
  36. package/template/.cursor/commands/qa.md +226 -0
  37. package/template/.cursor/commands/quick.md +41 -0
  38. package/template/.cursor/commands/refresh-context.md +82 -0
  39. package/template/.cursor/commands/release.md +505 -0
  40. package/template/.cursor/commands/research.md +49 -0
  41. package/template/.cursor/commands/resume.md +67 -0
  42. package/template/.cursor/commands/security-review.md +81 -0
  43. package/template/.cursor/commands/sprint-plan.md +103 -0
  44. package/template/.cursor/commands/status-reconcile.md +95 -0
  45. package/template/.cursor/commands/verify-work.md +152 -0
  46. package/template/.cursor/dev-environment.json.example +22 -0
  47. package/{.cursor → template/.cursor}/hooks/README.md +3 -2
  48. package/{.cursor/hooks/gsd-hook.py → template/.cursor/hooks/hook.py} +15 -6
  49. package/template/.cursor/hooks.json +26 -0
  50. package/template/.cursor/remote.json +31 -0
  51. package/template/.cursor/rules/caveman.mdc +184 -0
  52. package/template/.cursor/rules/coding-standards.mdc +39 -0
  53. package/template/.cursor/rules/core.mdc +111 -0
  54. package/template/.cursor/rules/handoffs.mdc +27 -0
  55. package/template/.cursor/rules/quality.mdc +49 -0
  56. package/template/.cursor/scratchpad.local.example.md +323 -0
  57. package/template/.cursor/scratchpad.md +324 -0
  58. package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/SKILL.md +2 -2
  59. package/template/.cursorignore +6 -0
  60. package/template/.env.example +28 -0
  61. package/template/.github/workflows/ci.yml +194 -0
  62. package/{.github → template/.github}/workflows/deploy.yml +5 -2
  63. package/template/.its-magic-version +1 -0
  64. package/template/README.md +1602 -0
  65. package/template/decisions/DEC-0001.md +11 -0
  66. package/template/decisions/DEC-0002.md +11 -0
  67. package/template/docs/developer/README.md +44 -0
  68. package/template/docs/engineering/architecture.md +9 -0
  69. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  70. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  71. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  72. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  73. package/template/docs/engineering/compatibility-report.md +20 -0
  74. package/template/docs/engineering/compatibility-signals.md +7 -0
  75. package/template/docs/engineering/component-scope-report.md +16 -0
  76. package/template/docs/engineering/component-scope.md +19 -0
  77. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  78. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  79. package/template/docs/engineering/decisions.md +16 -0
  80. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  81. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  82. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  83. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  84. package/template/docs/engineering/phase-context.md +16 -0
  85. package/template/docs/engineering/release-targets.json +123 -0
  86. package/template/docs/engineering/research.md +29 -0
  87. package/template/docs/engineering/runbook.md +2320 -0
  88. package/template/docs/engineering/runtime-connectivity.md +81 -0
  89. package/template/docs/engineering/security-review.md +33 -0
  90. package/template/docs/engineering/spec-pack/README.md +20 -0
  91. package/template/docs/engineering/state-archive/README.md +15 -0
  92. package/template/docs/engineering/state.md +20 -0
  93. package/template/docs/engineering/status-normalization-report.md +19 -0
  94. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  95. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  96. package/template/docs/product/acceptance.md +1 -0
  97. package/template/docs/product/backlog.md +5 -0
  98. package/template/docs/product/vision.md +11 -0
  99. package/template/docs/user-guides/README.md +40 -0
  100. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  101. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  102. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  103. package/template/handoffs/release_notes.md +51 -0
  104. package/template/handoffs/release_queue.md +47 -0
  105. package/template/handoffs/release_to_dev.md +31 -0
  106. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  107. package/template/handoffs/resume_brief.md +14 -0
  108. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  109. package/template/handoffs/token_cost_runs/README.md +26 -0
  110. package/template/its_magic/.its-magic-version +1 -0
  111. package/template/its_magic/README.md +9 -0
  112. package/template/scripts/auto_outer_driver.py +521 -0
  113. package/template/scripts/caveman_compress_input.py +903 -0
  114. package/template/scripts/check_downstream_ci_guard.py +67 -0
  115. package/template/scripts/check_intake_template_parity.py +284 -0
  116. package/template/scripts/check_token_cost_parity.py +69 -0
  117. package/template/scripts/dev_environment_lib.py +463 -0
  118. package/template/scripts/doc_profile_lib.py +415 -0
  119. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  120. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  121. package/template/scripts/guard_installer_publish.py +88 -0
  122. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  123. package/template/scripts/intake_bug_routing_guard.py +67 -0
  124. package/template/scripts/intake_evidence_lib.py +802 -0
  125. package/template/scripts/intake_evidence_validate.py +73 -0
  126. package/template/scripts/materialize_codebase_map.py +184 -0
  127. package/template/scripts/pack_json_validate.py +130 -0
  128. package/template/scripts/project_readme_coverage_lib.py +417 -0
  129. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  130. package/template/scripts/remote_config_summary.py +243 -0
  131. package/template/scripts/sync_push_gates.py +198 -0
  132. package/template/scripts/token_cost_compare.py +40 -0
  133. package/template/scripts/token_cost_lib.py +108 -0
  134. package/template/scripts/uat_probe_lib.py +868 -0
  135. package/template/scripts/validate-and-push.ps1 +280 -0
  136. package/template/scripts/validate-and-push.sh +243 -0
  137. package/template/scripts/validate_doc_profile.py +103 -0
  138. package/template/scripts/validate_project_readme_coverage.py +151 -0
  139. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  140. package/template/sprints/S0001/progress.md +1 -0
  141. package/template/sprints/S0001/qa-findings.md +9 -0
  142. package/template/sprints/S0001/release-findings.md +24 -0
  143. package/template/sprints/S0001/sprint.md +9 -0
  144. package/template/sprints/S0001/summary.md +7 -0
  145. package/template/sprints/S0001/tasks.md +1 -0
  146. package/template/sprints/S0001/uat.json +6 -0
  147. package/template/sprints/S0001/uat.md +5 -0
  148. package/template/sprints/quick/Q0001/summary.md +1 -0
  149. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  150. package/.cursor/agents/curator.mdc +0 -21
  151. package/.cursor/agents/dev.mdc +0 -20
  152. package/.cursor/agents/po.mdc +0 -19
  153. package/.cursor/agents/qa.mdc +0 -19
  154. package/.cursor/agents/release.mdc +0 -19
  155. package/.cursor/agents/tech-lead.mdc +0 -21
  156. package/.cursor/commands/gsd-architecture.md +0 -29
  157. package/.cursor/commands/gsd-auto.md +0 -27
  158. package/.cursor/commands/gsd-discovery.md +0 -27
  159. package/.cursor/commands/gsd-execute.md +0 -32
  160. package/.cursor/commands/gsd-intake.md +0 -28
  161. package/.cursor/commands/gsd-map-codebase.md +0 -25
  162. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  163. package/.cursor/commands/gsd-milestone-start.md +0 -26
  164. package/.cursor/commands/gsd-pause.md +0 -25
  165. package/.cursor/commands/gsd-plan-verify.md +0 -26
  166. package/.cursor/commands/gsd-qa.md +0 -28
  167. package/.cursor/commands/gsd-quick.md +0 -24
  168. package/.cursor/commands/gsd-refresh-context.md +0 -26
  169. package/.cursor/commands/gsd-release.md +0 -29
  170. package/.cursor/commands/gsd-research.md +0 -28
  171. package/.cursor/commands/gsd-resume.md +0 -26
  172. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  173. package/.cursor/commands/gsd-verify-work.md +0 -25
  174. package/.cursor/hooks.json +0 -26
  175. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  176. package/.cursor/remote.json +0 -18
  177. package/.cursor/rules/gsd-core.mdc +0 -18
  178. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  179. package/.cursor/rules/gsd-quality.mdc +0 -15
  180. package/.cursor/scratchpad.md +0 -34
  181. package/.github/workflows/ci.yml +0 -47
  182. package/decisions/DEC-0001.md +0 -21
  183. package/decisions/DEC-0002.md +0 -21
  184. package/docs/engineering/architecture.md +0 -354
  185. package/docs/engineering/decisions.md +0 -6
  186. package/docs/engineering/research.md +0 -11
  187. package/docs/engineering/runbook.md +0 -32
  188. package/docs/engineering/state.md +0 -33
  189. package/docs/product/acceptance.md +0 -6
  190. package/docs/product/backlog.md +0 -7
  191. package/docs/product/vision.md +0 -46
  192. package/gsd-installer.ps1 +0 -189
  193. package/gsd-installer.py +0 -195
  194. package/gsd-installer.sh +0 -201
  195. package/handoffs/release_notes.md +0 -14
  196. package/handoffs/resume_brief.md +0 -8
  197. package/milestones/M0001/milestone.json +0 -7
  198. package/milestones/M0001/phases.json +0 -9
  199. package/milestones/M0001/progress.md +0 -3
  200. package/milestones/M0001/summary.md +0 -3
  201. package/scripts/generate-release-notes.ps1 +0 -74
  202. package/scripts/generate-release-notes.sh +0 -63
  203. package/scripts/release-all.ps1 +0 -423
  204. package/scripts/release-all.sh +0 -226
  205. package/sprints/S0001/progress.md +0 -4
  206. package/sprints/S0001/qa-findings.md +0 -113
  207. package/sprints/S0001/sprint.md +0 -70
  208. package/sprints/S0001/summary.md +0 -46
  209. package/sprints/S0001/tasks.md +0 -35
  210. package/sprints/S0001/uat.json +0 -8
  211. package/sprints/S0001/uat.md +0 -8
  212. package/sprints/quick/Q0001/summary.md +0 -3
  213. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  214. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  215. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  216. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  217. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  218. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  230. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  231. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  232. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
@@ -0,0 +1,417 @@
1
+ """
2
+ Project README coverage: sentinels, migration, bootstrap, and backlog predicate (US-0097 / DEC-0083).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional, Set, Tuple
12
+
13
+ import readme_feature_coverage_lib as rfc
14
+
15
+ CATALOG_MARKER = "<!-- project-readme-feature-catalog -->"
16
+ FRAMEWORK_CATALOG_MARKER = "<!-- readme-feature-coverage-catalog -->"
17
+ S1_H1_PATTERN = re.compile(
18
+ r"^#\s*its-magic\s*[—\-]\s*AI dev team\s*$", re.MULTILINE | re.IGNORECASE
19
+ )
20
+ S3_HEADING = "Feature coverage catalog (US-0091)"
21
+ US_ID = re.compile(r"\bUS-\d{4}\b")
22
+ BUG_ID = re.compile(r"\bBUG-\d{4}\b")
23
+
24
+ REASON_BLOCKED = "PROJECT_README_COVERAGE_BLOCKED"
25
+ REASON_GAP = "PROJECT_README_COVERAGE_GAP"
26
+ REASON_INPUT_INVALID = "PROJECT_README_INPUT_INVALID"
27
+ REASON_MIGRATION_AMBIGUOUS = "PROJECT_README_MIGRATION_AMBIGUOUS"
28
+ REASON_SENTINEL_CONFLICT = "PROJECT_README_SENTINEL_CONFLICT"
29
+ REASON_DELTA_SKIPPED = "PROJECT_README_DELTA_SKIPPED"
30
+ REASON_BOOTSTRAP_SKIPPED = "PROJECT_README_BOOTSTRAP_SKIPPED"
31
+ REASON_PLACEHOLDER_UNRESOLVED = "PROJECT_README_PLACEHOLDER_UNRESOLVED"
32
+ REASON_ENFORCE_SKIPPED = "PROJECT_README_ENFORCE_SKIPPED"
33
+
34
+ REPORT_SCHEMA_VERSION = 1
35
+ FRAMEWORK_PATHS_EXCLUDED = [
36
+ "its_magic/README.md",
37
+ "template/its_magic/README.md",
38
+ "docs/developer/README.md",
39
+ ]
40
+
41
+ SENTINEL_IDS = ("S1", "S2", "S3", "S4", "S5")
42
+
43
+
44
+ @dataclass
45
+ class SentinelResult:
46
+ matched: List[str] = field(default_factory=list)
47
+ verdict: str = "missing" # placeholder | operator_authored | project_scaffold | missing | kit_skip | ambiguous
48
+ reason_code: str = ""
49
+
50
+
51
+ def read_utf8(path: str) -> str:
52
+ with open(path, "r", encoding="utf-8") as f:
53
+ return f.read()
54
+
55
+
56
+ def write_utf8(path: str, content: str) -> None:
57
+ os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
58
+ with open(path, "w", encoding="utf-8", newline="\n") as f:
59
+ f.write(content)
60
+
61
+
62
+ def is_framework_kit_repo(merged: Optional[Dict[str, str]]) -> bool:
63
+ if not merged:
64
+ return False
65
+ return (merged.get("FRAMEWORK_KIT_REPO") or "0").strip() == "1"
66
+
67
+
68
+ def detect_sentinels(content: str, repo_root: str) -> List[str]:
69
+ matched: List[str] = []
70
+ if S1_H1_PATTERN.search(content):
71
+ matched.append("S1")
72
+ if FRAMEWORK_CATALOG_MARKER in content:
73
+ matched.append("S2")
74
+ if S3_HEADING in content:
75
+ matched.append("S3")
76
+ tpl_path = os.path.join(repo_root, "template", "README.md")
77
+ if os.path.isfile(tpl_path) and content == read_utf8(tpl_path):
78
+ matched.append("S4")
79
+ return matched
80
+
81
+
82
+ def _has_project_scaffold(content: str) -> bool:
83
+ return CATALOG_MARKER in content and "## For users" in content and "## Features" in content
84
+
85
+
86
+ def _has_custom_operator_prose(content: str, sentinels: List[str]) -> bool:
87
+ """True when non-framework prose coexists with placeholder sentinels (hybrid)."""
88
+ if not sentinels:
89
+ return False
90
+ lines = content.splitlines()
91
+ custom_lines = 0
92
+ for line in lines:
93
+ stripped = line.strip()
94
+ if not stripped or stripped.startswith("<!--"):
95
+ continue
96
+ if S1_H1_PATTERN.match(stripped):
97
+ continue
98
+ if FRAMEWORK_CATALOG_MARKER in stripped:
99
+ continue
100
+ if S3_HEADING in stripped:
101
+ continue
102
+ if stripped.startswith("#") and "its-magic" in stripped.lower():
103
+ continue
104
+ if stripped.startswith("*Framework workflow"):
105
+ continue
106
+ if re.match(r"^[-*]\s", stripped) and US_ID.search(stripped):
107
+ continue
108
+ if len(stripped) > 20 and not stripped.startswith("|"):
109
+ custom_lines += 1
110
+ return custom_lines >= 3
111
+
112
+
113
+ def classify_root_readme(
114
+ content: str,
115
+ repo_root: str,
116
+ framework_kit_repo: bool = False,
117
+ ) -> SentinelResult:
118
+ if framework_kit_repo:
119
+ return SentinelResult(verdict="kit_skip")
120
+ if not content.strip():
121
+ return SentinelResult(verdict="missing")
122
+ sentinels = detect_sentinels(content, repo_root)
123
+ if _has_project_scaffold(content) and not sentinels:
124
+ return SentinelResult(verdict="project_scaffold")
125
+ if sentinels:
126
+ if _has_custom_operator_prose(content, sentinels):
127
+ if len(sentinels) >= 2:
128
+ return SentinelResult(
129
+ matched=sentinels,
130
+ verdict="ambiguous",
131
+ reason_code=REASON_SENTINEL_CONFLICT,
132
+ )
133
+ return SentinelResult(
134
+ matched=sentinels,
135
+ verdict="ambiguous",
136
+ reason_code=REASON_MIGRATION_AMBIGUOUS,
137
+ )
138
+ return SentinelResult(matched=sentinels, verdict="placeholder")
139
+ return SentinelResult(matched=["S5"], verdict="operator_authored")
140
+
141
+
142
+ def extract_vision_h1_purpose(repo_root: str) -> Tuple[str, str]:
143
+ vision_path = os.path.join(repo_root, "docs", "product", "vision.md")
144
+ if not os.path.isfile(vision_path):
145
+ return "Project", "Describe your product purpose here."
146
+ text = read_utf8(vision_path)
147
+ h1 = "Project"
148
+ for line in text.splitlines():
149
+ if line.startswith("# "):
150
+ h1 = line[2:].strip()
151
+ break
152
+ purpose_parts: List[str] = []
153
+ in_section = False
154
+ for line in text.splitlines():
155
+ if line.strip() in ("## Problem", "## Value"):
156
+ in_section = True
157
+ continue
158
+ if in_section and line.startswith("## "):
159
+ in_section = False
160
+ if in_section and line.strip() and not line.startswith("#"):
161
+ purpose_parts.append(line.strip())
162
+ if len(purpose_parts) >= 3:
163
+ break
164
+ purpose = " ".join(purpose_parts[:3]) if purpose_parts else "Describe your product purpose here."
165
+ if len(purpose) > 400:
166
+ purpose = purpose[:397] + "..."
167
+ return h1, purpose
168
+
169
+
170
+ def materialize_project_scaffold(repo_root: str) -> str:
171
+ h1, purpose = extract_vision_h1_purpose(repo_root)
172
+ return (
173
+ f"# {h1}\n\n"
174
+ f"{purpose}\n\n"
175
+ "## For users\n\n"
176
+ "## For developers\n\n"
177
+ "## Features\n\n"
178
+ f"{CATALOG_MARKER}\n\n"
179
+ "*Framework workflow commands: see [its_magic/README.md](its_magic/README.md).*\n"
180
+ )
181
+
182
+
183
+ def extract_catalog_section(readme_text: str) -> str:
184
+ if CATALOG_MARKER not in readme_text:
185
+ return ""
186
+ after = readme_text.split(CATALOG_MARKER, 1)[1]
187
+ lines: List[str] = []
188
+ for line in after.splitlines():
189
+ if line.startswith("## ") and not line.startswith("### "):
190
+ break
191
+ lines.append(line)
192
+ return "\n".join(lines)
193
+
194
+
195
+ def has_catalog_coverage(catalog_text: str, item_id: str) -> bool:
196
+ if item_id not in catalog_text:
197
+ return False
198
+ for line in catalog_text.splitlines():
199
+ if item_id in line and re.match(r"^[-*]\s", line.strip()):
200
+ return True
201
+ return bool(re.search(rf"\b{re.escape(item_id)}\b", catalog_text))
202
+
203
+
204
+ def classify_project_item(item: rfc.WorkItem, enforce: bool) -> rfc.PredicateResult:
205
+ if item.status != "DONE":
206
+ return rfc.PredicateResult(False, "status:not_done")
207
+ if item.user_visible is False:
208
+ return rfc.PredicateResult(False, "explicit:false")
209
+ if item.user_visible is True:
210
+ return rfc.PredicateResult(True, "explicit:true")
211
+ if enforce:
212
+ return rfc.PredicateResult(
213
+ False,
214
+ "unset:enforce",
215
+ input_invalid=True,
216
+ invalid_reason=f"{item.item_id}: user_visible unset with PROJECT_README_ENFORCE=1",
217
+ )
218
+ return rfc.PredicateResult(False, "heuristic:out")
219
+
220
+
221
+ def run_migration(
222
+ repo_root: str,
223
+ merged: Optional[Dict[str, str]] = None,
224
+ dry_run: bool = False,
225
+ ) -> Tuple[str, List[str]]:
226
+ """Execute M1–M5; return (status, messages). status: ok | skip | error."""
227
+ messages: List[str] = []
228
+ if is_framework_kit_repo(merged):
229
+ messages.append("M1: FRAMEWORK_KIT_REPO=1 — skip consumer migration")
230
+ return "skip", messages
231
+
232
+ root_path = os.path.join(repo_root, "README.md")
233
+ its_magic_path = os.path.join(repo_root, "its_magic", "README.md")
234
+ root_content = read_utf8(root_path) if os.path.isfile(root_path) else ""
235
+ classification = classify_root_readme(root_content, repo_root)
236
+
237
+ if classification.verdict == "ambiguous":
238
+ messages.append(f"M5: {classification.reason_code}")
239
+ return "error", messages
240
+
241
+ if classification.verdict == "operator_authored":
242
+ messages.append("M2: operator-authored root (S5) — preserve root")
243
+ if not os.path.isfile(its_magic_path) and root_content.strip():
244
+ if not dry_run:
245
+ os.makedirs(os.path.dirname(its_magic_path), exist_ok=True)
246
+ write_utf8(its_magic_path, root_content)
247
+ messages.append("M2: copied root → its_magic/README.md")
248
+ return "ok", messages
249
+
250
+ if classification.verdict in ("missing", "placeholder", "project_scaffold"):
251
+ if classification.verdict == "placeholder" and root_content.strip():
252
+ if not os.path.isfile(its_magic_path) or not read_utf8(its_magic_path).strip():
253
+ if not dry_run:
254
+ os.makedirs(os.path.dirname(its_magic_path), exist_ok=True)
255
+ write_utf8(its_magic_path, root_content)
256
+ messages.append("M3: lifted root → its_magic/README.md")
257
+ if classification.verdict == "placeholder":
258
+ scaffold = materialize_project_scaffold(repo_root)
259
+ if root_content != scaffold:
260
+ if not dry_run:
261
+ write_utf8(root_path, scaffold)
262
+ messages.append("M4: replaced root with project scaffold")
263
+ elif classification.verdict == "missing":
264
+ if not dry_run:
265
+ write_utf8(root_path, materialize_project_scaffold(repo_root))
266
+ messages.append("M4: materialized project scaffold (root missing)")
267
+ return "ok", messages
268
+
269
+ if classification.verdict == "kit_skip":
270
+ return "skip", messages
271
+ messages.append(f"M5: {REASON_PLACEHOLDER_UNRESOLVED}")
272
+ return "error", messages
273
+
274
+
275
+ def build_report(
276
+ repo_root: str,
277
+ backlog_path: str,
278
+ enforce: bool,
279
+ merged: Optional[Dict[str, str]] = None,
280
+ no_kit_skip: bool = False,
281
+ ) -> Tuple[Dict[str, Any], List[str]]:
282
+ stderr: List[str] = []
283
+ kit_skip = is_framework_kit_repo(merged) and not no_kit_skip
284
+
285
+ if kit_skip:
286
+ report = {
287
+ "catalog_marker_present": False,
288
+ "coverage_missing": [],
289
+ "coverage_present": [],
290
+ "coverage_total": 0,
291
+ "framework_paths_excluded": list(FRAMEWORK_PATHS_EXCLUDED),
292
+ "gaps": [],
293
+ "kit_repo_skipped": True,
294
+ "report_schema_version": REPORT_SCHEMA_VERSION,
295
+ "repo_root": ".",
296
+ "status": "PASS",
297
+ }
298
+ return report, stderr
299
+
300
+ backlog_text = read_utf8(backlog_path)
301
+ items = rfc.parse_backlog(backlog_text)
302
+ root_path = os.path.join(repo_root, "README.md")
303
+ if not os.path.isfile(root_path):
304
+ stderr.append(f"{REASON_INPUT_INVALID}: root README.md missing")
305
+ report = _empty_fail_report()
306
+ return report, stderr
307
+
308
+ root_readme = read_utf8(root_path)
309
+ classification = classify_root_readme(root_readme, repo_root)
310
+ if classification.verdict == "ambiguous":
311
+ stderr.append(classification.reason_code or REASON_MIGRATION_AMBIGUOUS)
312
+
313
+ catalog_text = extract_catalog_section(root_readme)
314
+ marker_present = CATALOG_MARKER in root_readme
315
+
316
+ gaps: List[Dict[str, Any]] = []
317
+ present: List[str] = []
318
+ missing: List[str] = []
319
+ total = 0
320
+
321
+ for item in sorted(items, key=lambda x: x.item_id):
322
+ pred = classify_project_item(item, enforce)
323
+ if pred.input_invalid:
324
+ stderr.append(f"{REASON_INPUT_INVALID}: {pred.invalid_reason}")
325
+ continue
326
+ if not pred.in_scope:
327
+ continue
328
+ total += 1
329
+ if has_catalog_coverage(catalog_text, item.item_id):
330
+ present.append(item.item_id)
331
+ else:
332
+ missing.append(item.item_id)
333
+ gaps.append(
334
+ {
335
+ "id": item.item_id,
336
+ "kind": item.kind,
337
+ "predicate_source": pred.predicate_source,
338
+ "user_visible": item.user_visible if item.user_visible is not None else True,
339
+ }
340
+ )
341
+ stderr.append(f"{REASON_GAP}:{item.item_id}")
342
+
343
+ status = "PASS" if not missing and not any(
344
+ c in (REASON_MIGRATION_AMBIGUOUS, REASON_SENTINEL_CONFLICT) for c in stderr
345
+ ) else "FAIL"
346
+ if missing:
347
+ status = "FAIL"
348
+
349
+ report = {
350
+ "catalog_marker_present": marker_present,
351
+ "coverage_missing": sorted(missing),
352
+ "coverage_present": sorted(present),
353
+ "coverage_total": total,
354
+ "framework_paths_excluded": list(FRAMEWORK_PATHS_EXCLUDED),
355
+ "gaps": sorted(gaps, key=lambda g: g["id"]),
356
+ "kit_repo_skipped": False,
357
+ "report_schema_version": REPORT_SCHEMA_VERSION,
358
+ "repo_root": ".",
359
+ "status": status,
360
+ }
361
+ return report, stderr
362
+
363
+
364
+ def _empty_fail_report() -> Dict[str, Any]:
365
+ return {
366
+ "catalog_marker_present": False,
367
+ "coverage_missing": [],
368
+ "coverage_present": [],
369
+ "coverage_total": 0,
370
+ "framework_paths_excluded": list(FRAMEWORK_PATHS_EXCLUDED),
371
+ "gaps": [],
372
+ "kit_repo_skipped": False,
373
+ "report_schema_version": REPORT_SCHEMA_VERSION,
374
+ "repo_root": ".",
375
+ "status": "FAIL",
376
+ }
377
+
378
+
379
+ def canonical_json(obj: Dict[str, Any]) -> str:
380
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
381
+
382
+
383
+ def self_test_sentinel_matrix() -> None:
384
+ repo = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
385
+ s1 = "# its-magic — AI dev team\n"
386
+ assert "S1" in detect_sentinels(s1, repo)
387
+ s2 = f"body\n{FRAMEWORK_CATALOG_MARKER}\n"
388
+ assert "S2" in detect_sentinels(s2, repo)
389
+ s3 = f"## {S3_HEADING}\n"
390
+ assert "S3" in detect_sentinels(s3, repo)
391
+ r = classify_root_readme(s1, repo)
392
+ assert r.verdict == "placeholder"
393
+ scaffold = materialize_project_scaffold(repo)
394
+ assert CATALOG_MARKER in scaffold
395
+ assert "## For users" in scaffold
396
+
397
+
398
+ def self_test_report_schema() -> None:
399
+ required = {
400
+ "report_schema_version",
401
+ "status",
402
+ "repo_root",
403
+ "catalog_marker_present",
404
+ "coverage_present",
405
+ "coverage_missing",
406
+ "coverage_total",
407
+ "gaps",
408
+ "framework_paths_excluded",
409
+ "kit_repo_skipped",
410
+ }
411
+ report, _ = build_report(
412
+ os.path.normpath(os.path.join(os.path.dirname(__file__), "..")),
413
+ os.path.join(os.path.dirname(__file__), "..", "docs", "product", "backlog.md"),
414
+ enforce=False,
415
+ merged={"FRAMEWORK_KIT_REPO": "1"},
416
+ )
417
+ assert required <= set(report.keys())