its-magic 0.1.2-9 → 0.1.3-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.
Files changed (237) hide show
  1. package/README.md +1602 -755
  2. package/bin/its-magic.js +121 -11
  3. package/bin/postinstall.js +93 -0
  4. package/installer.ps1 +759 -0
  5. package/installer.py +1036 -0
  6. package/installer.sh +649 -0
  7. package/package.json +18 -14
  8. package/scripts/check_intake_template_parity.py +312 -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 +523 -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/CHANGELOG.md +11 -0
  65. package/template/README.md +1602 -0
  66. package/template/decisions/DEC-0001.md +11 -0
  67. package/template/decisions/DEC-0002.md +11 -0
  68. package/template/docs/developer/README.md +44 -0
  69. package/template/docs/engineering/architecture.md +9 -0
  70. package/template/docs/engineering/artifact-ordering-policy.md +48 -0
  71. package/template/docs/engineering/artifact-ownership-policy.md +57 -0
  72. package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
  73. package/{docs → template/docs}/engineering/codebase-map.md +0 -5
  74. package/template/docs/engineering/compatibility-report.md +20 -0
  75. package/template/docs/engineering/compatibility-signals.md +7 -0
  76. package/template/docs/engineering/component-scope-report.md +16 -0
  77. package/template/docs/engineering/component-scope.md +19 -0
  78. package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
  79. package/template/docs/engineering/context/readme-section-affinity.json +30 -0
  80. package/template/docs/engineering/decisions.md +16 -0
  81. package/template/docs/engineering/legacy-drift-audit.md +21 -0
  82. package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
  83. package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
  84. package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
  85. package/template/docs/engineering/phase-context.md +16 -0
  86. package/template/docs/engineering/release-targets.json +123 -0
  87. package/template/docs/engineering/research.md +29 -0
  88. package/template/docs/engineering/runbook.md +2391 -0
  89. package/template/docs/engineering/runtime-connectivity.md +81 -0
  90. package/template/docs/engineering/security-review.md +33 -0
  91. package/template/docs/engineering/spec-pack/README.md +20 -0
  92. package/template/docs/engineering/state-archive/README.md +15 -0
  93. package/template/docs/engineering/state.md +20 -0
  94. package/template/docs/engineering/status-normalization-report.md +19 -0
  95. package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
  96. package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
  97. package/template/docs/product/acceptance.md +1 -0
  98. package/template/docs/product/backlog.md +5 -0
  99. package/template/docs/product/vision.md +11 -0
  100. package/template/docs/user-guides/README.md +40 -0
  101. package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
  102. package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
  103. package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
  104. package/template/handoffs/release_notes.md +51 -0
  105. package/template/handoffs/release_queue.md +47 -0
  106. package/template/handoffs/release_to_dev.md +31 -0
  107. package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
  108. package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
  109. package/template/handoffs/resume_brief.md +14 -0
  110. package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
  111. package/template/handoffs/token_cost_runs/README.md +26 -0
  112. package/template/its_magic/.its-magic-version +1 -0
  113. package/template/its_magic/README.md +9 -0
  114. package/template/scripts/auto_outer_driver.py +521 -0
  115. package/template/scripts/caveman_compress_input.py +903 -0
  116. package/template/scripts/check_downstream_ci_guard.py +67 -0
  117. package/template/scripts/check_intake_template_parity.py +312 -0
  118. package/template/scripts/check_token_cost_parity.py +69 -0
  119. package/template/scripts/dev_environment_lib.py +601 -0
  120. package/template/scripts/doc_profile_lib.py +415 -0
  121. package/template/scripts/downstream_ci_guard_lib.py +222 -0
  122. package/template/scripts/enforce-triad-hot-surface.py +753 -0
  123. package/template/scripts/guard_installer_publish.py +88 -0
  124. package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
  125. package/template/scripts/intake_bug_routing_guard.py +67 -0
  126. package/template/scripts/intake_evidence_lib.py +802 -0
  127. package/template/scripts/intake_evidence_validate.py +73 -0
  128. package/template/scripts/materialize_codebase_map.py +184 -0
  129. package/template/scripts/pack_json_validate.py +130 -0
  130. package/template/scripts/project_readme_coverage_lib.py +417 -0
  131. package/template/scripts/readme_feature_coverage_lib.py +608 -0
  132. package/{scripts → template/scripts}/release-all.sh +27 -4
  133. package/template/scripts/release_changelog_backfill.py +153 -0
  134. package/template/scripts/release_changelog_lib.py +544 -0
  135. package/template/scripts/release_changelog_validate.py +134 -0
  136. package/template/scripts/remote_config_summary.py +243 -0
  137. package/template/scripts/sync_push_gates.py +198 -0
  138. package/template/scripts/token_cost_compare.py +40 -0
  139. package/template/scripts/token_cost_lib.py +108 -0
  140. package/template/scripts/uat_probe_lib.py +868 -0
  141. package/template/scripts/validate-and-push.ps1 +280 -0
  142. package/template/scripts/validate-and-push.sh +243 -0
  143. package/template/scripts/validate_doc_profile.py +103 -0
  144. package/template/scripts/validate_project_readme_coverage.py +151 -0
  145. package/template/scripts/validate_readme_feature_coverage.py +140 -0
  146. package/template/sprints/S0001/progress.md +1 -0
  147. package/template/sprints/S0001/qa-findings.md +9 -0
  148. package/template/sprints/S0001/release-findings.md +24 -0
  149. package/template/sprints/S0001/sprint.md +9 -0
  150. package/template/sprints/S0001/summary.md +7 -0
  151. package/template/sprints/S0001/tasks.md +1 -0
  152. package/template/sprints/S0001/uat.json +6 -0
  153. package/template/sprints/S0001/uat.md +5 -0
  154. package/template/sprints/quick/Q0001/summary.md +1 -0
  155. package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
  156. package/.cursor/agents/curator.mdc +0 -21
  157. package/.cursor/agents/dev.mdc +0 -20
  158. package/.cursor/agents/po.mdc +0 -19
  159. package/.cursor/agents/qa.mdc +0 -19
  160. package/.cursor/agents/release.mdc +0 -19
  161. package/.cursor/agents/tech-lead.mdc +0 -21
  162. package/.cursor/commands/gsd-architecture.md +0 -29
  163. package/.cursor/commands/gsd-auto.md +0 -27
  164. package/.cursor/commands/gsd-discovery.md +0 -27
  165. package/.cursor/commands/gsd-execute.md +0 -32
  166. package/.cursor/commands/gsd-intake.md +0 -28
  167. package/.cursor/commands/gsd-map-codebase.md +0 -25
  168. package/.cursor/commands/gsd-milestone-complete.md +0 -24
  169. package/.cursor/commands/gsd-milestone-start.md +0 -26
  170. package/.cursor/commands/gsd-pause.md +0 -25
  171. package/.cursor/commands/gsd-plan-verify.md +0 -26
  172. package/.cursor/commands/gsd-qa.md +0 -28
  173. package/.cursor/commands/gsd-quick.md +0 -24
  174. package/.cursor/commands/gsd-refresh-context.md +0 -26
  175. package/.cursor/commands/gsd-release.md +0 -29
  176. package/.cursor/commands/gsd-research.md +0 -28
  177. package/.cursor/commands/gsd-resume.md +0 -26
  178. package/.cursor/commands/gsd-sprint-plan.md +0 -30
  179. package/.cursor/commands/gsd-verify-work.md +0 -25
  180. package/.cursor/hooks.json +0 -26
  181. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
  182. package/.cursor/remote.json +0 -18
  183. package/.cursor/rules/gsd-core.mdc +0 -18
  184. package/.cursor/rules/gsd-handoffs.mdc +0 -10
  185. package/.cursor/rules/gsd-quality.mdc +0 -15
  186. package/.cursor/scratchpad.md +0 -34
  187. package/.github/workflows/ci.yml +0 -47
  188. package/decisions/DEC-0001.md +0 -21
  189. package/decisions/DEC-0002.md +0 -21
  190. package/docs/engineering/architecture.md +0 -354
  191. package/docs/engineering/decisions.md +0 -6
  192. package/docs/engineering/research.md +0 -11
  193. package/docs/engineering/runbook.md +0 -32
  194. package/docs/engineering/state.md +0 -33
  195. package/docs/product/acceptance.md +0 -6
  196. package/docs/product/backlog.md +0 -7
  197. package/docs/product/vision.md +0 -46
  198. package/gsd-installer.ps1 +0 -189
  199. package/gsd-installer.py +0 -195
  200. package/gsd-installer.sh +0 -201
  201. package/handoffs/release_notes.md +0 -14
  202. package/handoffs/resume_brief.md +0 -8
  203. package/milestones/M0001/milestone.json +0 -7
  204. package/milestones/M0001/phases.json +0 -9
  205. package/milestones/M0001/progress.md +0 -3
  206. package/milestones/M0001/summary.md +0 -3
  207. package/scripts/generate-release-notes.ps1 +0 -74
  208. package/scripts/generate-release-notes.sh +0 -63
  209. package/scripts/release-all.ps1 +0 -423
  210. package/sprints/S0001/progress.md +0 -4
  211. package/sprints/S0001/qa-findings.md +0 -113
  212. package/sprints/S0001/sprint.md +0 -70
  213. package/sprints/S0001/summary.md +0 -46
  214. package/sprints/S0001/tasks.md +0 -35
  215. package/sprints/S0001/uat.json +0 -8
  216. package/sprints/S0001/uat.md +0 -8
  217. package/sprints/quick/Q0001/summary.md +0 -3
  218. /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
  219. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
  220. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
  221. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
  222. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
  223. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
  224. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
  225. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
  226. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
  227. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
  228. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
  229. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
  230. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
  231. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
  232. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
  233. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
  234. /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
  235. /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
  236. /package/{docs → template/docs}/engineering/dependencies.json +0 -0
  237. /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
@@ -0,0 +1,608 @@
1
+ """
2
+ README feature coverage predicate, backlog parser, and affinity resolver (US-0091 / DEC-0074).
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
+ US_BLOCK_HEADER = re.compile(r"^## (US-\d{4})\s*[—-]\s*(.*)\s*$")
14
+ BUG_BLOCK_HEADER = re.compile(r"^### (BUG-\d{4})\s*[—-]\s*(.*)\s*$")
15
+ BUG_SECTION_HEADER = "## Bug issues (canonical)"
16
+ STATUS_LINE = re.compile(r"^-\s*Status:\s*(OPEN|DONE)\s*$", re.IGNORECASE)
17
+ USER_VISIBLE_LINE = re.compile(
18
+ r"^-\s*user_visible:\s*(true|false)\s*$", re.IGNORECASE
19
+ )
20
+ FIELD_LINE = re.compile(r"^-\s*([A-Za-z_][A-Za-z0-9_]*):\s*(.*)\s*$")
21
+ SLASH_CMD = re.compile(r"(?:^|\s)(/[a-z][a-z0-9-]*)")
22
+ SCRATCHPAD_KEY = re.compile(r"(?:^|[\s`'])([A-Z][A-Z0-9_]+)(?:=|\b)")
23
+ PYTHON_SCRIPT = re.compile(
24
+ r"python\s+scripts/[a-z0-9_./-]+\.py", re.IGNORECASE
25
+ )
26
+ US_ID = re.compile(r"\bUS-\d{4}\b")
27
+ BUG_ID = re.compile(r"\bBUG-\d{4}\b")
28
+
29
+ H5_KEYWORDS = (
30
+ "archiver",
31
+ "hot-surface rollover",
32
+ "intake evidence schema",
33
+ "template parity guard",
34
+ "triad",
35
+ )
36
+
37
+ REASON_BLOCKED = "README_FEATURE_COVERAGE_BLOCKED"
38
+ REASON_GAP = "README_FEATURE_COVERAGE_GAP"
39
+ REASON_PARITY_FAIL = "README_FEATURE_COVERAGE_PARITY_FAIL"
40
+ REASON_INPUT_INVALID = "README_FEATURE_COVERAGE_INPUT_INVALID"
41
+ REASON_PROFILE_VIOLATION = "README_FEATURE_COVERAGE_PROFILE_VIOLATION"
42
+
43
+ REPORT_SCHEMA_VERSION = 1
44
+
45
+ DEFAULT_AFFINITY: Dict[str, Any] = {
46
+ "affinity_version": 1,
47
+ "rules": [
48
+ {
49
+ "tag": "slash_command",
50
+ "root_h2": "Commands and workflow",
51
+ "dev_h2": "Workflow",
52
+ },
53
+ {
54
+ "tag": "scratchpad_mode",
55
+ "root_h2": "Other useful capabilities",
56
+ "dev_h2": "Quality gates",
57
+ },
58
+ {
59
+ "tag": "distribution",
60
+ "root_h2": "Features",
61
+ "dev_h2": "Architecture notes",
62
+ },
63
+ {
64
+ "tag": "release_gate",
65
+ "root_h2": "Commands and workflow",
66
+ "dev_h2": "Quality gates",
67
+ },
68
+ {
69
+ "tag": "governance",
70
+ "root_h2": "Other useful capabilities",
71
+ "dev_h2": "Engineering decisions",
72
+ },
73
+ ],
74
+ }
75
+
76
+
77
+ @dataclass
78
+ class WorkItem:
79
+ item_id: str
80
+ kind: str # US | BUG
81
+ title: str
82
+ status: Optional[str]
83
+ summary: str
84
+ user_visible: Optional[bool]
85
+ body_text: str
86
+ fields: Dict[str, str] = field(default_factory=dict)
87
+
88
+
89
+ @dataclass
90
+ class PredicateResult:
91
+ in_scope: bool
92
+ predicate_source: str
93
+ input_invalid: bool = False
94
+ invalid_reason: str = ""
95
+
96
+
97
+ @dataclass
98
+ class AffinityTarget:
99
+ tag: str
100
+ root_h2: str
101
+ dev_h2: str
102
+
103
+
104
+ def read_utf8(path: str) -> str:
105
+ with open(path, "r", encoding="utf-8") as f:
106
+ return f.read()
107
+
108
+
109
+ def load_affinity_manifest(repo_root: str) -> Dict[str, Any]:
110
+ path = os.path.join(
111
+ repo_root, "docs", "engineering", "context", "readme-section-affinity.json"
112
+ )
113
+ if not os.path.isfile(path):
114
+ return dict(DEFAULT_AFFINITY)
115
+ with open(path, "r", encoding="utf-8") as f:
116
+ data = json.load(f)
117
+ if data.get("affinity_version") != 1:
118
+ raise ValueError("affinity_version must be 1")
119
+ return data
120
+
121
+
122
+ def extract_bug_section(text: str) -> Optional[str]:
123
+ m_hdr = re.search(r"^## Bug issues \(canonical\)\s*$", text, re.MULTILINE)
124
+ if not m_hdr:
125
+ return None
126
+ start = m_hdr.end()
127
+ rest = text[start:]
128
+ m = re.search(r"\n## [^\n]+\n", rest)
129
+ if m:
130
+ return rest[: m.start()].strip()
131
+ return rest.strip()
132
+
133
+
134
+ def _parse_block_fields(block_lines: List[str]) -> Tuple[Optional[str], Dict[str, str], str]:
135
+ status: Optional[str] = None
136
+ fields: Dict[str, str] = {}
137
+ body_parts: List[str] = []
138
+ for raw in block_lines[1:]:
139
+ line = raw.rstrip()
140
+ body_parts.append(line)
141
+ sm = STATUS_LINE.match(line.strip())
142
+ if sm:
143
+ status = sm.group(1).upper()
144
+ continue
145
+ fm = FIELD_LINE.match(line.strip())
146
+ if fm:
147
+ key = fm.group(1).lower()
148
+ val = fm.group(2).strip()
149
+ fields[key] = val
150
+ return status, fields, "\n".join(body_parts)
151
+
152
+
153
+ def parse_us_blocks(text: str) -> List[WorkItem]:
154
+ lines = text.splitlines()
155
+ items: List[WorkItem] = []
156
+ i = 0
157
+ while i < len(lines):
158
+ m = US_BLOCK_HEADER.match(lines[i].strip())
159
+ if not m:
160
+ i += 1
161
+ continue
162
+ item_id, title = m.group(1), m.group(2).strip()
163
+ block_start = i
164
+ i += 1
165
+ while i < len(lines):
166
+ nxt = lines[i].strip()
167
+ if US_BLOCK_HEADER.match(nxt) or nxt == BUG_SECTION_HEADER:
168
+ break
169
+ if nxt.startswith("## ") and not nxt.startswith("### "):
170
+ break
171
+ i += 1
172
+ block_lines = lines[block_start:i]
173
+ status, fields, body_text = _parse_block_fields(block_lines)
174
+ uv_raw = None
175
+ for raw in block_lines[1:]:
176
+ uvm = USER_VISIBLE_LINE.match(raw.strip())
177
+ if uvm:
178
+ uv_raw = uvm.group(1).lower() == "true"
179
+ break
180
+ items.append(
181
+ WorkItem(
182
+ item_id=item_id,
183
+ kind="US",
184
+ title=title,
185
+ status=status,
186
+ summary=fields.get("summary", ""),
187
+ user_visible=uv_raw,
188
+ body_text=body_text,
189
+ fields=fields,
190
+ )
191
+ )
192
+ return items
193
+
194
+
195
+ def parse_bug_blocks(text: str) -> List[WorkItem]:
196
+ section = extract_bug_section(text)
197
+ if not section:
198
+ return []
199
+ lines = section.splitlines()
200
+ items: List[WorkItem] = []
201
+ i = 0
202
+ while i < len(lines):
203
+ m = BUG_BLOCK_HEADER.match(lines[i].strip())
204
+ if not m:
205
+ i += 1
206
+ continue
207
+ item_id, title = m.group(1), m.group(2).strip()
208
+ block_start = i
209
+ i += 1
210
+ while i < len(lines) and not BUG_BLOCK_HEADER.match(lines[i].strip()):
211
+ i += 1
212
+ block_lines = lines[block_start:i]
213
+ status, fields, body_text = _parse_block_fields(block_lines)
214
+ uv_raw = None
215
+ for raw in block_lines[1:]:
216
+ uvm = USER_VISIBLE_LINE.match(raw.strip())
217
+ if uvm:
218
+ uv_raw = uvm.group(1).lower() == "true"
219
+ break
220
+ items.append(
221
+ WorkItem(
222
+ item_id=item_id,
223
+ kind="BUG",
224
+ title=title,
225
+ status=status,
226
+ summary=fields.get("summary", title),
227
+ user_visible=uv_raw,
228
+ body_text=body_text,
229
+ fields=fields,
230
+ )
231
+ )
232
+ return items
233
+
234
+
235
+ def parse_backlog(backlog_text: str) -> List[WorkItem]:
236
+ return parse_us_blocks(backlog_text) + parse_bug_blocks(backlog_text)
237
+
238
+
239
+ def _heuristic_signals(text: str) -> Dict[str, bool]:
240
+ low = text.lower()
241
+ h1 = bool(SLASH_CMD.search(text))
242
+ h2 = bool(SCRATCHPAD_KEY.search(text)) or bool(
243
+ re.search(r"`[A-Z][A-Z0-9_]+`", text)
244
+ )
245
+ h3 = bool(PYTHON_SCRIPT.search(text))
246
+ h4 = False
247
+ if "expected" in low or "actual" in low:
248
+ h4 = True
249
+ h5 = any(kw in low for kw in H5_KEYWORDS)
250
+ return {"h1": h1, "h2": h2, "h3": h3, "h4": h4, "h5": h5}
251
+
252
+
253
+ def migration_heuristic(item: WorkItem) -> str:
254
+ """Return in_scope | out | ambiguous per DEC-0074 H1-H8."""
255
+ text = f"{item.title}\n{item.summary}\n{item.body_text}"
256
+ sig = _heuristic_signals(text)
257
+ if sig["h5"] and any(sig[k] for k in ("h1", "h2", "h3", "h4")):
258
+ return "in_scope"
259
+ if sig["h1"]:
260
+ return "in_scope"
261
+ if sig["h2"]:
262
+ return "in_scope"
263
+ if sig["h3"]:
264
+ return "in_scope"
265
+ if item.kind == "BUG" and sig["h4"]:
266
+ return "in_scope"
267
+ if sig["h5"]:
268
+ return "out"
269
+ if item.kind == "BUG":
270
+ return "out"
271
+ return "ambiguous"
272
+
273
+
274
+ def classify_item(item: WorkItem, enforce: bool) -> PredicateResult:
275
+ if item.status != "DONE":
276
+ return PredicateResult(False, "status:not_done")
277
+ if item.user_visible is False:
278
+ return PredicateResult(False, "explicit:false")
279
+ if item.user_visible is True:
280
+ return PredicateResult(True, "explicit:true")
281
+ if enforce:
282
+ return PredicateResult(
283
+ False,
284
+ "unset:enforce",
285
+ input_invalid=True,
286
+ invalid_reason=f"{item.item_id}: user_visible unset with README_FEATURE_COVERAGE_ENFORCE=1",
287
+ )
288
+ h = migration_heuristic(item)
289
+ if h == "in_scope":
290
+ src = _heuristic_source(item)
291
+ return PredicateResult(True, src)
292
+ if h == "out":
293
+ return PredicateResult(False, "heuristic:out")
294
+ return PredicateResult(
295
+ False,
296
+ "heuristic:ambiguous",
297
+ input_invalid=True,
298
+ invalid_reason=f"{item.item_id}: H7 ambiguous — set user_visible explicitly",
299
+ )
300
+
301
+
302
+ def _heuristic_source(item: WorkItem) -> str:
303
+ text = f"{item.title}\n{item.summary}\n{item.body_text}"
304
+ sig = _heuristic_signals(text)
305
+ if sig["h1"]:
306
+ return "heuristic:H1"
307
+ if sig["h2"]:
308
+ return "heuristic:H2"
309
+ if sig["h3"]:
310
+ return "heuristic:H3"
311
+ if item.kind == "BUG" and sig["h4"]:
312
+ return "heuristic:H4"
313
+ if sig["h5"] and any(sig[k] for k in ("h1", "h2", "h3", "h4")):
314
+ return "heuristic:H6"
315
+ return "heuristic:H1"
316
+
317
+
318
+ def resolve_affinity(item: WorkItem, manifest: Dict[str, Any]) -> AffinityTarget:
319
+ rules = manifest.get("rules", DEFAULT_AFFINITY["rules"])
320
+ by_tag = {r["tag"]: r for r in rules}
321
+ text = f"{item.title}\n{item.summary}\n{item.body_text}"
322
+ low = text.lower()
323
+ if SLASH_CMD.search(text) and "slash_command" in by_tag:
324
+ r = by_tag["slash_command"]
325
+ return AffinityTarget("slash_command", r["root_h2"], r["dev_h2"])
326
+ if any(k in low for k in ("npm", "chocolatey", "homebrew", "publish", "distribution")):
327
+ r = by_tag.get("distribution", rules[0])
328
+ return AffinityTarget("distribution", r["root_h2"], r["dev_h2"])
329
+ if any(k in low for k in ("/release", "release gate", "uat", "verify-work")):
330
+ r = by_tag.get("release_gate", rules[0])
331
+ return AffinityTarget("release_gate", r["root_h2"], r["dev_h2"])
332
+ if SCRATCHPAD_KEY.search(text) or re.search(r"`[A-Z][A-Z0-9_]+`", text):
333
+ r = by_tag.get("scratchpad_mode", rules[0])
334
+ return AffinityTarget("scratchpad_mode", r["root_h2"], r["dev_h2"])
335
+ r = by_tag.get("governance", rules[-1])
336
+ return AffinityTarget("governance", r["root_h2"], r["dev_h2"])
337
+
338
+
339
+ def split_h2_sections(markdown: str) -> Dict[str, str]:
340
+ sections: Dict[str, str] = {}
341
+ current: Optional[str] = None
342
+ buf: List[str] = []
343
+ for line in markdown.splitlines():
344
+ if line.startswith("## ") and not line.startswith("### "):
345
+ if current is not None:
346
+ sections[current] = "\n".join(buf)
347
+ current = line[3:].strip()
348
+ buf = []
349
+ elif current is not None:
350
+ buf.append(line)
351
+ if current is not None:
352
+ sections[current] = "\n".join(buf)
353
+ return sections
354
+
355
+
356
+ def _h2_match(section_title: str, wanted: str) -> bool:
357
+ if section_title == wanted:
358
+ return True
359
+ return section_title.startswith(wanted + " ") or section_title.startswith(wanted + "(")
360
+
361
+
362
+ def section_body(markdown: str, h2_wanted: str) -> str:
363
+ for title, body in split_h2_sections(markdown).items():
364
+ if _h2_match(title, h2_wanted):
365
+ return body
366
+ return ""
367
+
368
+
369
+ def has_root_coverage(section_text: str, item: WorkItem) -> bool:
370
+ if SLASH_CMD.search(section_text):
371
+ for m in SLASH_CMD.finditer(f"{item.title}\n{item.summary}"):
372
+ if m.group(1) in section_text:
373
+ return True
374
+ for m in SCRATCHPAD_KEY.finditer(f"{item.title}\n{item.summary}\n{item.body_text}"):
375
+ if m.group(1) in section_text:
376
+ return True
377
+ if item.item_id in section_text:
378
+ return True
379
+ if re.search(rf"\b{re.escape(item.item_id)}\b", section_text):
380
+ return True
381
+ return False
382
+
383
+
384
+ def has_dev_coverage(section_text: str, item: WorkItem) -> bool:
385
+ needle = item.item_id
386
+ for line in section_text.splitlines():
387
+ if needle not in line:
388
+ continue
389
+ if re.search(rf"\*\*{re.escape(needle)}\*\*", line):
390
+ return True
391
+ if "traceability:" in line.lower() and needle in line:
392
+ return True
393
+ return False
394
+
395
+
396
+ def check_template_parity(repo_root: str) -> List[str]:
397
+ errors: List[str] = []
398
+ pairs = (
399
+ ("its_magic/README.md", "template/its_magic/README.md"),
400
+ (
401
+ "scripts/readme_feature_coverage_lib.py",
402
+ "template/scripts/readme_feature_coverage_lib.py",
403
+ ),
404
+ (
405
+ "scripts/validate_readme_feature_coverage.py",
406
+ "template/scripts/validate_readme_feature_coverage.py",
407
+ ),
408
+ )
409
+ for active_rel, tpl_rel in pairs:
410
+ a = os.path.join(repo_root, active_rel.replace("/", os.sep))
411
+ t = os.path.join(repo_root, tpl_rel.replace("/", os.sep))
412
+ if not os.path.isfile(a) or not os.path.isfile(t):
413
+ errors.append(f"{REASON_PARITY_FAIL}: missing {active_rel} or {tpl_rel}")
414
+ continue
415
+ if read_utf8(a) != read_utf8(t):
416
+ errors.append(f"{REASON_PARITY_FAIL}: {active_rel} != {tpl_rel}")
417
+ return errors
418
+
419
+
420
+ def check_profile_budget(repo_root: str, merged: Dict[str, str]) -> List[str]:
421
+ try:
422
+ import doc_profile_lib
423
+ except ImportError:
424
+ return []
425
+ errs = doc_profile_lib.validate_repo_doc_profile(
426
+ repo_root, merged, os.path.join(repo_root, "template")
427
+ )
428
+ out: List[str] = []
429
+ for e in errs:
430
+ if "DOC_SECTION_BUDGET_EXCEEDED" in e or "DOC_SECTION_MISSING" in e:
431
+ out.append(f"{REASON_PROFILE_VIOLATION}: {e}")
432
+ return out
433
+
434
+
435
+ def build_report(
436
+ repo_root: str,
437
+ backlog_path: str,
438
+ enforce: bool,
439
+ merged: Optional[Dict[str, str]] = None,
440
+ skip_parity: bool = False,
441
+ ) -> Tuple[Dict[str, Any], List[str]]:
442
+ """Return (report_dict, stderr_lines)."""
443
+ stderr: List[str] = []
444
+ backlog_text = read_utf8(backlog_path)
445
+ items = parse_backlog(backlog_text)
446
+ manifest = load_affinity_manifest(repo_root)
447
+ root_readme = read_utf8(os.path.join(repo_root, "its_magic", "README.md"))
448
+ dev_readme = read_utf8(os.path.join(repo_root, "docs", "developer", "README.md"))
449
+
450
+ if not skip_parity:
451
+ for e in check_template_parity(repo_root):
452
+ stderr.append(e)
453
+
454
+ if merged is not None:
455
+ for e in check_profile_budget(repo_root, merged):
456
+ stderr.append(e)
457
+
458
+ gaps: List[Dict[str, Any]] = []
459
+ present: List[str] = []
460
+ missing: List[str] = []
461
+ total = 0
462
+
463
+ for item in sorted(items, key=lambda x: x.item_id):
464
+ pred = classify_item(item, enforce)
465
+ if pred.input_invalid:
466
+ stderr.append(f"{REASON_INPUT_INVALID}: {pred.invalid_reason}")
467
+ continue
468
+ if not pred.in_scope:
469
+ continue
470
+ total += 1
471
+ aff = resolve_affinity(item, manifest)
472
+ root_sec = section_body(root_readme, aff.root_h2)
473
+ dev_sec = section_body(dev_readme, aff.dev_h2)
474
+ root_ok = has_root_coverage(root_sec, item)
475
+ dev_ok = has_dev_coverage(dev_sec, item)
476
+ if root_ok and dev_ok:
477
+ present.append(item.item_id)
478
+ else:
479
+ missing.append(item.item_id)
480
+ gaps.append(
481
+ {
482
+ "dev_h2": aff.dev_h2,
483
+ "id": item.item_id,
484
+ "kind": item.kind,
485
+ "predicate_source": pred.predicate_source,
486
+ "root_h2": aff.root_h2,
487
+ "user_visible": item.user_visible if item.user_visible is not None else True,
488
+ }
489
+ )
490
+ stderr.append(f"{REASON_GAP}:{item.item_id}")
491
+
492
+ status = "PASS" if not stderr and not missing else "FAIL"
493
+ if missing or any(REASON_GAP in s for s in stderr):
494
+ status = "FAIL"
495
+
496
+ report = {
497
+ "coverage_missing": sorted(missing),
498
+ "coverage_present": sorted(present),
499
+ "coverage_total": total,
500
+ "gaps": sorted(gaps, key=lambda g: g["id"]),
501
+ "report_schema_version": REPORT_SCHEMA_VERSION,
502
+ "repo_root": ".",
503
+ "status": status,
504
+ }
505
+ return report, stderr
506
+
507
+
508
+ def canonical_json(obj: Dict[str, Any]) -> str:
509
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
510
+
511
+
512
+ def self_test_predicate_matrix() -> None:
513
+ cases = [
514
+ (
515
+ WorkItem("US-0001", "US", "t", "DONE", "workflow", True, "", {}),
516
+ False,
517
+ True,
518
+ "explicit:true",
519
+ ),
520
+ (
521
+ WorkItem("US-0002", "US", "t", "DONE", "internal", False, "", {}),
522
+ False,
523
+ False,
524
+ "explicit:false",
525
+ ),
526
+ (
527
+ WorkItem("US-0003", "US", "t", "DONE", "Use /release for ship", None, "", {}),
528
+ False,
529
+ True,
530
+ "heuristic:H1",
531
+ ),
532
+ (
533
+ WorkItem(
534
+ "US-0004",
535
+ "US",
536
+ "t",
537
+ "DONE",
538
+ "template parity guard triad archiver",
539
+ None,
540
+ "",
541
+ {},
542
+ ),
543
+ False,
544
+ False,
545
+ "heuristic:out",
546
+ ),
547
+ (
548
+ WorkItem(
549
+ "US-0005",
550
+ "US",
551
+ "t",
552
+ "DONE",
553
+ "triad archiver /intake",
554
+ None,
555
+ "",
556
+ {},
557
+ ),
558
+ False,
559
+ True,
560
+ None,
561
+ ),
562
+ (
563
+ WorkItem("US-0006", "US", "t", "DONE", "pure internal refactor", None, "", {}),
564
+ False,
565
+ False,
566
+ "heuristic:ambiguous",
567
+ True,
568
+ ),
569
+ (
570
+ WorkItem("BUG-0001", "BUG", "t", "DONE", "silent", None, "", {}),
571
+ False,
572
+ False,
573
+ "heuristic:out",
574
+ ),
575
+ ]
576
+ for case in cases:
577
+ item, enforce, want_in, want_src, *extra = case
578
+ want_invalid = bool(extra and extra[0] is True)
579
+ pred = classify_item(item, enforce)
580
+ if want_invalid:
581
+ assert pred.input_invalid, item.item_id
582
+ else:
583
+ assert pred.in_scope == want_in, (item.item_id, pred)
584
+ if want_in and want_src is not None:
585
+ assert pred.predicate_source == want_src, (
586
+ item.item_id,
587
+ pred.predicate_source,
588
+ )
589
+
590
+ # enforce=1 unset fails
591
+ unset = WorkItem("US-0099", "US", "t", "DONE", "x", None, "", {})
592
+ pred = classify_item(unset, True)
593
+ assert pred.input_invalid
594
+
595
+
596
+ def self_test_report_schema() -> None:
597
+ sample = {
598
+ "coverage_missing": [],
599
+ "coverage_present": ["US-0001"],
600
+ "coverage_total": 1,
601
+ "gaps": [],
602
+ "report_schema_version": 1,
603
+ "repo_root": ".",
604
+ "status": "PASS",
605
+ }
606
+ a = canonical_json(sample)
607
+ b = canonical_json(sample)
608
+ assert a == b
@@ -41,6 +41,9 @@ done
41
41
  REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
42
42
  cd "$REPO_ROOT"
43
43
 
44
+ # Node 22+: trust OS CA store when TLS inspection adds system CAs (Windows/antivirus/corp proxy).
45
+ export NODE_USE_SYSTEM_CA="${NODE_USE_SYSTEM_CA:-1}"
46
+
44
47
  log() { echo -e "\033[36m[release]\033[0m $1"; }
45
48
  warn() { echo -e "\033[33m[release]\033[0m $1"; }
46
49
  err() { echo -e "\033[31m[release]\033[0m $1"; exit 1; }
@@ -88,12 +91,32 @@ if [[ "$NEW_VERSION" == *-* ]]; then
88
91
  GH_PRERELEASE="--prerelease"
89
92
  fi
90
93
 
94
+ RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES="${RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES:-0}"
95
+ VERSION_NOTES="handoffs/releases/${NEW_VERSION}-release-notes.md"
96
+
91
97
  if command -v gh &>/dev/null; then
92
98
  log "Creating GitHub release $TAG_NAME ..."
99
+ if [[ ! -f "$VERSION_NOTES" ]]; then
100
+ log "Version notes missing — deriving via release_changelog_backfill --ensure-version ..."
101
+ if ! $DRY_RUN; then
102
+ python scripts/release_changelog_backfill.py --repo "$REPO_ROOT" --ensure-version "$NEW_VERSION" || true
103
+ fi
104
+ fi
93
105
  if $DRY_RUN; then
94
- log "(dry-run) would run: gh release create $TAG_NAME --generate-notes $GH_PRERELEASE"
95
- else
106
+ if [[ -f "$VERSION_NOTES" ]]; then
107
+ log "(dry-run) would run: python scripts/release_changelog_validate.py --repo . --enforce"
108
+ log "(dry-run) would run: gh release create $TAG_NAME -F $VERSION_NOTES --title $TAG_NAME $GH_PRERELEASE"
109
+ else
110
+ log "(dry-run) would fail-closed: RELEASE_CHANGELOG_VERSION_DOC_MISSING ($VERSION_NOTES)"
111
+ fi
112
+ elif [[ -f "$VERSION_NOTES" ]]; then
113
+ python scripts/release_changelog_validate.py --repo "$REPO_ROOT" --enforce
114
+ gh release create "$TAG_NAME" -F "$VERSION_NOTES" --title "$TAG_NAME" $GH_PRERELEASE || warn "gh release create failed – continuing"
115
+ elif [[ "$RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES" == "1" ]]; then
116
+ warn "RELEASE_CHANGELOG_VERSION_DOC_MISSING: $VERSION_NOTES — falling back to --generate-notes (opt-in)"
96
117
  gh release create "$TAG_NAME" --generate-notes --title "$TAG_NAME" $GH_PRERELEASE || warn "gh release create failed – continuing"
118
+ else
119
+ err "RELEASE_CHANGELOG_VERSION_DOC_MISSING: $VERSION_NOTES (set RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES=1 to opt into --generate-notes)"
97
120
  fi
98
121
  REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
99
122
  if [[ "$REMOTE_URL" =~ github\.com[:/](.+?)(\.git)?$ ]]; then
@@ -128,7 +151,7 @@ if ! $SKIP_CHOCO; then
128
151
 
129
152
  # Compute checksum
130
153
  if [[ -n "$ZIP_URL" ]]; then
131
- TMP_ZIP="/tmp/gsd-$TAG_NAME.zip"
154
+ TMP_ZIP="/tmp/its-magic-$TAG_NAME.zip"
132
155
  if curl -fsSL "$ZIP_URL" -o "$TMP_ZIP" 2>/dev/null; then
133
156
  SHA=$(shasum -a 256 "$TMP_ZIP" | awk '{print $1}')
134
157
  sed -i.bak "s|^\\$checksum[[:space:]]*=.*$|\\$checksum = '$SHA'|" "$CHOCO_INSTALL"
@@ -192,7 +215,7 @@ if ! $SKIP_BREW; then
192
215
 
193
216
  # Compute sha256
194
217
  if [[ -n "$TAR_URL" ]]; then
195
- TMP_TAR="/tmp/gsd-$TAG_NAME.tar.gz"
218
+ TMP_TAR="/tmp/its-magic-$TAG_NAME.tar.gz"
196
219
  if curl -fsSL "$TAR_URL" -o "$TMP_TAR" 2>/dev/null; then
197
220
  SHA=$(shasum -a 256 "$TMP_TAR" | awk '{print $1}')
198
221
  # Replace both PLACEHOLDER and any previous sha256 value