@arbiterforge/ca-pi 0.6.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 (206) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +558 -0
  3. package/package.json +35 -0
  4. package/plugins/ca-pi/CHANGELOG.md +1030 -0
  5. package/plugins/ca-pi/COMMANDS.md +90 -0
  6. package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
  7. package/plugins/ca-pi/SKILLS.md +47 -0
  8. package/plugins/ca-pi/SPRINT.md +142 -0
  9. package/plugins/ca-pi/agents/INDEX.md +31 -0
  10. package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
  11. package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
  12. package/plugins/ca-pi/agents/backend-author.md +60 -0
  13. package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
  14. package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
  15. package/plugins/ca-pi/agents/decision-challenger.md +116 -0
  16. package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
  17. package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
  18. package/plugins/ca-pi/agents/finding-triage.md +86 -0
  19. package/plugins/ca-pi/agents/frontend-author.md +64 -0
  20. package/plugins/ca-pi/agents/grader.md +173 -0
  21. package/plugins/ca-pi/agents/infra-author.md +64 -0
  22. package/plugins/ca-pi/agents/map-deps.md +35 -0
  23. package/plugins/ca-pi/agents/map-structure.md +37 -0
  24. package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
  25. package/plugins/ca-pi/agents/scout.md +127 -0
  26. package/plugins/ca-pi/agents/security-reviewer.md +72 -0
  27. package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
  28. package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
  29. package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
  30. package/plugins/ca-pi/generated/command-catalog.json +197 -0
  31. package/plugins/ca-pi/generated/roles.json +213 -0
  32. package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
  33. package/plugins/ca-pi/hooks/_activationlib.py +196 -0
  34. package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
  35. package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
  36. package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
  37. package/plugins/ca-pi/hooks/_boxlib.py +131 -0
  38. package/plugins/ca-pi/hooks/_colorlib.py +304 -0
  39. package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
  40. package/plugins/ca-pi/hooks/_entrylib.py +41 -0
  41. package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
  42. package/plugins/ca-pi/hooks/_gitexec.py +45 -0
  43. package/plugins/ca-pi/hooks/_githooks.py +920 -0
  44. package/plugins/ca-pi/hooks/_gitlib.py +110 -0
  45. package/plugins/ca-pi/hooks/_hooklib.py +595 -0
  46. package/plugins/ca-pi/hooks/_host.py +115 -0
  47. package/plugins/ca-pi/hooks/_intentlib.py +242 -0
  48. package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
  49. package/plugins/ca-pi/hooks/_metricslib.py +709 -0
  50. package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
  51. package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
  52. package/plugins/ca-pi/hooks/_previewlib.py +193 -0
  53. package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
  54. package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
  55. package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
  56. package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
  57. package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
  58. package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
  59. package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
  60. package/plugins/ca-pi/hooks/_scopelib.py +262 -0
  61. package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
  62. package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
  63. package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
  64. package/plugins/ca-pi/hooks/_sloplib.py +244 -0
  65. package/plugins/ca-pi/hooks/_standuplib.py +214 -0
  66. package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
  67. package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
  68. package/plugins/ca-pi/hooks/_updatelib.py +278 -0
  69. package/plugins/ca-pi/hooks/babysit.py +47 -0
  70. package/plugins/ca-pi/hooks/boardsync.py +129 -0
  71. package/plugins/ca-pi/hooks/doctor.py +420 -0
  72. package/plugins/ca-pi/hooks/git-enforce.py +325 -0
  73. package/plugins/ca-pi/hooks/hostapi.py +460 -0
  74. package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
  75. package/plugins/ca-pi/hooks/metrics.py +62 -0
  76. package/plugins/ca-pi/hooks/migration-pass.py +129 -0
  77. package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
  78. package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
  79. package/plugins/ca-pi/hooks/pre-bash.py +90 -0
  80. package/plugins/ca-pi/hooks/pre-edit.py +284 -0
  81. package/plugins/ca-pi/hooks/pre-read.py +81 -0
  82. package/plugins/ca-pi/hooks/pre-write.py +217 -0
  83. package/plugins/ca-pi/hooks/preview.py +69 -0
  84. package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
  85. package/plugins/ca-pi/hooks/releasehash.py +216 -0
  86. package/plugins/ca-pi/hooks/security-pass.py +139 -0
  87. package/plugins/ca-pi/hooks/session-start.py +1218 -0
  88. package/plugins/ca-pi/hooks/statusline.py +736 -0
  89. package/plugins/ca-pi/hooks/taskwrite.py +351 -0
  90. package/plugins/ca-pi/hooks/update-refresh.py +51 -0
  91. package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
  92. package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
  93. package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
  94. package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
  95. package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
  96. package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
  97. package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
  98. package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
  99. package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
  100. package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
  101. package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
  102. package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
  103. package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
  104. package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
  105. package/plugins/ca-pi/includes/compaction-charter.md +16 -0
  106. package/plugins/ca-pi/includes/cut-docs.md +16 -0
  107. package/plugins/ca-pi/includes/dev-mode.md +30 -0
  108. package/plugins/ca-pi/includes/farm.md +237 -0
  109. package/plugins/ca-pi/includes/fresh-verification.md +14 -0
  110. package/plugins/ca-pi/includes/harvest.md +69 -0
  111. package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
  112. package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
  113. package/plugins/ca-pi/includes/redirect.md +69 -0
  114. package/plugins/ca-pi/includes/reference-map.md +22 -0
  115. package/plugins/ca-pi/includes/review-matrix.md +14 -0
  116. package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
  117. package/plugins/ca-pi/includes/routing-table.md +47 -0
  118. package/plugins/ca-pi/includes/security-gate-record.md +22 -0
  119. package/plugins/ca-pi/includes/smarts/core.md +90 -0
  120. package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
  121. package/plugins/ca-pi/routines/INDEX.md +32 -0
  122. package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
  123. package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
  124. package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
  125. package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
  126. package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
  127. package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
  128. package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
  129. package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
  130. package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
  131. package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
  132. package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
  133. package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
  134. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
  135. package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
  136. package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
  137. package/plugins/ca-pi/routines/release/SKILL.md +315 -0
  138. package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
  139. package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
  140. package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
  141. package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
  142. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
  143. package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
  144. package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
  145. package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
  146. package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
  147. package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
  148. package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
  149. package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
  150. package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
  151. package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
  152. package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
  153. package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
  154. package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
  155. package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
  156. package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
  157. package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
  158. package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
  159. package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
  160. package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
  161. package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
  162. package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
  163. package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
  164. package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
  165. package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
  166. package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
  167. package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
  168. package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
  169. package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
  170. package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
  171. package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
  172. package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
  173. package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
  174. package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
  175. package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
  176. package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
  177. package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
  178. package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
  179. package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
  180. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
  181. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
  182. package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
  183. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
  184. package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
  185. package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
  186. package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
  187. package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
  188. package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
  189. package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
  190. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
  191. package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
  192. package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
  193. package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
  194. package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
  195. package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
  196. package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
  197. package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
  198. package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
  199. package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
  200. package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
  201. package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
  202. package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
  203. package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
  204. package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
  205. package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
  206. package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Thin Pi payload adapter for the shared codeArbiter Python core."""
3
+
4
+ import os
5
+ import sys
6
+
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+ import hostapi # noqa: E402
9
+
10
+
11
+ class PiHost(hostapi.Host):
12
+ name = "pi"
13
+ command_noun = "command"
14
+ has_statusline = False
15
+ has_read_tool = True
16
+ has_prunable_transcript = True
17
+
18
+ TOOL_MAP = {
19
+ "bash": "EXEC",
20
+ "codearbiter_dispatch": "EXEC",
21
+ "codearbiter_farm_preview": "EXEC",
22
+ "write": "WRITE",
23
+ "edit": "EDIT",
24
+ "read": "READ",
25
+ }
26
+
27
+ def __init__(self, project_dir=None):
28
+ self._project_dir = project_dir
29
+
30
+ def cmd_ref(self, name):
31
+ return "/ca-" + name
32
+
33
+ def project_root(self, payload=None):
34
+ if self._project_dir and os.path.isdir(self._project_dir):
35
+ return self._project_dir
36
+ if isinstance(payload, dict):
37
+ cwd = payload.get("cwd")
38
+ if isinstance(cwd, str) and os.path.isdir(cwd):
39
+ return hostapi.git_toplevel(cwd) or cwd
40
+ return hostapi.git_toplevel() or os.getcwd()
41
+
42
+ def plugin_root(self):
43
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44
+
45
+ def manifest_relpath(self):
46
+ return "package.json"
47
+
48
+ def mcp_config_sources(self, project_root):
49
+ # #270: Pi's MCP configuration surface is not source-verified against
50
+ # a supported Pi release in this repo, so this host claims NO sources
51
+ # and doctor's MCP line degrades to silence. Inheriting the base
52
+ # (Claude) implementation would report ~/.claude.json — a DIFFERENT
53
+ # host's configuration — as this install's. Fill this in only with a
54
+ # version-pinned source reference, the way TOOL_MAP is pinned.
55
+ return []
56
+
57
+ def normalize_tool_input(self, tool_name, tool_input):
58
+ value = tool_input if isinstance(tool_input, dict) else {}
59
+ if tool_name == "read":
60
+ return {"file_path": value.get("path", value.get("file_path", ""))}
61
+ if tool_name == "write":
62
+ return {"file_path": value.get("path", ""), "content": value.get("content", "")}
63
+ if tool_name == "edit":
64
+ edits = value.get("edits", [])
65
+ if not edits and isinstance(value.get("oldText"), str) and isinstance(value.get("newText"), str):
66
+ edits = [{"oldText": value["oldText"], "newText": value["newText"]}]
67
+ return {
68
+ "file_path": value.get("path", ""),
69
+ "edits": [
70
+ {"old_string": item.get("oldText", ""), "new_string": item.get("newText", "")}
71
+ for item in edits if isinstance(item, dict)
72
+ ],
73
+ }
74
+ return value
75
+
76
+ def iter_file_ops(self, payload):
77
+ payload = payload if isinstance(payload, dict) else {}
78
+ tool = payload.get("tool_name", "")
79
+ native = payload.get("tool_input", {})
80
+ value = self.normalize_tool_input(tool, native)
81
+ if tool == "edit":
82
+ fpath = value.get("file_path", "")
83
+ edits = value.get("edits", [])
84
+ batched = len(edits) > 1
85
+ return [
86
+ {
87
+ "file_path": fpath,
88
+ "kind": "edit",
89
+ "content": None,
90
+ "added_text": item.get("new_string", ""),
91
+ "added_lines": item.get("new_string", "").splitlines(),
92
+ "old_string": item.get("old_string", ""),
93
+ "replace_all": False,
94
+ "batched": batched,
95
+ "notebook": False,
96
+ }
97
+ for item in edits
98
+ ]
99
+ if tool == "write":
100
+ content = value.get("content", "")
101
+ return [{
102
+ "file_path": value.get("file_path", ""),
103
+ "kind": "write",
104
+ "content": content,
105
+ "added_text": content,
106
+ "added_lines": content.splitlines(),
107
+ "old_string": "",
108
+ "replace_all": False,
109
+ "batched": False,
110
+ "notebook": False,
111
+ }]
112
+ return []
113
+
114
+
115
+ HOST = PiHost()
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter -- spec-intent coverage backstop (#566).
3
+ #
4
+ # `writing-plans` Phase 4 proves BIJECTION between a plan's tasks and its
5
+ # `AC-NN` ledger: every criterion has a task, every task has a criterion.
6
+ # That proves the two AGREE with each other -- it says nothing about
7
+ # whether the ledger itself is COMPLETE relative to what the spec (and its
8
+ # linked issue) actually asked for. A criterion missed by BOTH sides --
9
+ # never drafted in `brainstorming`, so never assigned a task in
10
+ # `writing-plans` either -- passes bijection cleanly while the deliverable
11
+ # it would have named goes unbuilt. Observed twice in one sprint: the
12
+ # `release-portable-fixture` spec's own central artifact (a scope bullet
13
+ # no criterion cited), and #563's end-to-end portability property (an
14
+ # issue acceptance checkbox that never became a criterion at all).
15
+ #
16
+ # This module is the MECHANICAL half of the fix, not the whole of it -- a
17
+ # citation check, never a completeness proof. It flags an in-scope bullet
18
+ # or an issue acceptance checkbox that no acceptance criterion's text
19
+ # CITES (shares a distinctive word with). It cannot catch a bullet that IS
20
+ # cited but only PARTIALLY satisfied by the criteria that cite it -- a
21
+ # scope bullet reading "fix all three contaminated skills" against
22
+ # criteria for two of the three passes this check, because the bullet IS
23
+ # cited. That half is judgment: the skill asks it explicitly as "if every
24
+ # criterion passed and nothing else changed, what would still be broken?"
25
+ # and this module has no part in answering it.
26
+ #
27
+ # Stdlib only (ADR-0004). Every function below is pure -- no filesystem,
28
+ # no git, no network -- so the whole surface is fixture-testable with
29
+ # synthetic spec/issue text. The CLI wrapper at the bottom is the only I/O,
30
+ # reading two already-resolved files so the skill never pipes untrusted
31
+ # text through a shell substitution.
32
+ #
33
+ # Public API:
34
+ # uncovered_intent(spec_text, issue_body=None) -> list[str]
35
+ # Every in-scope bullet / issue acceptance checkbox no criterion
36
+ # cites, each prefixed by its kind (`[UNCOVERED-SCOPE]` /
37
+ # `[UNCOVERED-CHECKBOX]`), in source order. Empty when everything
38
+ # found is cited, including when there is nothing to check (no
39
+ # `## Scope` section, no issue body).
40
+ #
41
+ # CLI:
42
+ # _intentlib.py uncovered-intent <spec-file> [--issue-body <file>]
43
+ # exit 0 -- empty (nothing uncovered); 1 -- one or more findings,
44
+ # printed one per line to stdout; 2 -- bad invocation or an unreadable
45
+ # file.
46
+
47
+ import re
48
+ import sys
49
+
50
+ _STOPWORDS = frozenset({
51
+ "the", "a", "an", "and", "or", "but", "of", "to", "in", "on", "for",
52
+ "with", "is", "are", "was", "were", "be", "been", "being", "this",
53
+ "that", "these", "those", "it", "its", "as", "at", "by", "from",
54
+ "into", "than", "then", "so", "not", "no", "never", "always",
55
+ "when", "while", "if", "each", "every", "any", "all", "one", "two",
56
+ "will", "shall", "should", "would", "can", "could", "may", "might",
57
+ "must", "have", "has", "had", "do", "does", "did", "done", "also",
58
+ "only", "just", "still", "yet", "over", "under", "out", "up",
59
+ "down", "about", "after", "before", "between", "through", "per",
60
+ "your", "you", "we", "our", "their", "them", "which", "what",
61
+ "where", "there", "here", "such", "some", "more", "most", "own",
62
+ })
63
+
64
+ _TOKEN_RE = re.compile(r"[a-z0-9]+")
65
+ _HEADING_RE = re.compile(r"^(#{1,6})\s+(.*\S)\s*$")
66
+ _BULLET_RE = re.compile(r"^\s*(?:[-*]|\d+[.)])\s+(.*\S)\s*$")
67
+ _CHECKBOX_RE = re.compile(r"^\s*[-*]\s+\[[ xX]\]\s+(.*\S)\s*$")
68
+ _OUT_OF_SCOPE_RE = re.compile(r"out.of.scope|out.scope|not\s+build(?:ing)?", re.IGNORECASE)
69
+
70
+ MIN_TOKEN_LEN = 4
71
+
72
+
73
+ def _tokens(text):
74
+ """Distinctive lowercase words in `text`: stdlib-tokenized, short words
75
+ and a small stopword set dropped, order-preserving and de-duplicated.
76
+ A word carrying no distinctive token (e.g. "Fast." alone) yields []."""
77
+ seen = []
78
+ for word in _TOKEN_RE.findall(text.lower()):
79
+ if len(word) < MIN_TOKEN_LEN or word in _STOPWORDS:
80
+ continue
81
+ if word not in seen:
82
+ seen.append(word)
83
+ return seen
84
+
85
+
86
+ def _section(text, heading_pattern):
87
+ """Lines of the FIRST heading whose text matches `heading_pattern`
88
+ (case-insensitive, anywhere in the heading text), up to the next
89
+ heading at the same or a shallower level. [] when no such heading
90
+ exists -- an absent section names nothing to check, not an error."""
91
+ lines = text.splitlines()
92
+ start = level = None
93
+ for i, line in enumerate(lines):
94
+ m = _HEADING_RE.match(line)
95
+ if not m:
96
+ continue
97
+ this_level = len(m.group(1))
98
+ if start is None:
99
+ if re.search(heading_pattern, m.group(2), re.IGNORECASE):
100
+ start, level = i + 1, this_level
101
+ continue
102
+ if this_level <= level:
103
+ return lines[start:i]
104
+ return lines[start:] if start is not None else []
105
+
106
+
107
+ def _in_scope_bullets(spec_text):
108
+ """Bullet lines under the first `## Scope`-ish heading, stopping at an
109
+ "out of scope" marker line (the line NAMING the boundary, not a bullet
110
+ beneath it). Best-effort: a spec that never separates in- from
111
+ out-of-scope this way is scanned in full, which can over-flag rather
112
+ than under-flag -- the skill's negative-judgment question is the
113
+ backstop for what this heuristic gets wrong, not a reason to drop it."""
114
+ section = _section(spec_text, r"scope")
115
+ bullets = []
116
+ for line in section:
117
+ m = _BULLET_RE.match(line)
118
+ if m is None and _OUT_OF_SCOPE_RE.search(line):
119
+ break
120
+ if m:
121
+ bullets.append(m.group(1))
122
+ return bullets
123
+
124
+
125
+ def _criteria_texts(spec_text):
126
+ """Every numbered or bulleted criterion line under the first
127
+ "acceptance criteria"-ish heading. A non-list line (closing prose, a
128
+ reviewer's note, anything after the numbered list that isn't itself a
129
+ criterion) is NOT a criterion and contributes no citation tokens --
130
+ counting it would let an unrelated sentence's vocabulary falsely
131
+ "cite" a scope bullet or checkbox it never actually addresses."""
132
+ section = _section(spec_text, r"acceptance criteria")
133
+ out = []
134
+ for line in section:
135
+ m = _BULLET_RE.match(line)
136
+ if m:
137
+ out.append(m.group(1))
138
+ return out
139
+
140
+
141
+ def _checkboxes(issue_body):
142
+ if not issue_body:
143
+ return []
144
+ out = []
145
+ for line in issue_body.splitlines():
146
+ m = _CHECKBOX_RE.match(line)
147
+ if m:
148
+ out.append(m.group(1))
149
+ return out
150
+
151
+
152
+ def _cited(item_text, criteria_tokens):
153
+ """True when `item_text` shares a distinctive token with the criteria
154
+ set, OR when `item_text` carries no distinctive token of its own to
155
+ check (nothing extractable is never flagged as missing)."""
156
+ tokens = _tokens(item_text)
157
+ if not tokens:
158
+ return True
159
+ return any(t in criteria_tokens for t in tokens)
160
+
161
+
162
+ def uncovered_intent(spec_text, issue_body=None):
163
+ """Every in-scope bullet / issue acceptance checkbox that no
164
+ acceptance criterion's text cites, each prefixed by its kind, scope
165
+ bullets first then checkboxes, both in source order.
166
+
167
+ A CITATION check, not a completeness proof -- see the module
168
+ docstring for what it cannot catch."""
169
+ if not isinstance(spec_text, str):
170
+ spec_text = ""
171
+ criteria_tokens = set()
172
+ for criterion in _criteria_texts(spec_text):
173
+ criteria_tokens.update(_tokens(criterion))
174
+
175
+ findings = []
176
+ for bullet in _in_scope_bullets(spec_text):
177
+ if not _cited(bullet, criteria_tokens):
178
+ findings.append(f"[UNCOVERED-SCOPE] {bullet}")
179
+ for box in _checkboxes(issue_body):
180
+ if not _cited(box, criteria_tokens):
181
+ findings.append(f"[UNCOVERED-CHECKBOX] {box}")
182
+ return findings
183
+
184
+
185
+ def main(argv):
186
+ """CLI dispatch.
187
+
188
+ uncovered-intent <spec-file> [--issue-body <file>]
189
+ prints one finding per line (see
190
+ `uncovered_intent`'s docstring for the
191
+ two prefixes). exit 0 nothing uncovered -
192
+ 1 one or more findings - 2 bad invocation
193
+ or an unreadable file.
194
+ """
195
+ if not argv or argv[0] != "uncovered-intent":
196
+ sys.stderr.write(
197
+ "usage: _intentlib.py uncovered-intent <spec-file> [--issue-body <file>]\n")
198
+ return 2
199
+
200
+ rest = argv[1:]
201
+ issue_body_path = None
202
+ positional = []
203
+ i = 0
204
+ while i < len(rest):
205
+ if rest[i] == "--issue-body":
206
+ if i + 1 >= len(rest):
207
+ sys.stderr.write("_intentlib.py: --issue-body requires a path\n")
208
+ return 2
209
+ issue_body_path = rest[i + 1]
210
+ i += 2
211
+ continue
212
+ positional.append(rest[i])
213
+ i += 1
214
+ if len(positional) != 1:
215
+ sys.stderr.write(
216
+ "usage: _intentlib.py uncovered-intent <spec-file> [--issue-body <file>]\n")
217
+ return 2
218
+
219
+ try:
220
+ with open(positional[0], encoding="utf-8") as fh:
221
+ spec_text = fh.read()
222
+ except OSError as exc:
223
+ sys.stderr.write(f"_intentlib.py: cannot read {positional[0]}: {exc}\n")
224
+ return 2
225
+
226
+ issue_body = None
227
+ if issue_body_path is not None:
228
+ try:
229
+ with open(issue_body_path, encoding="utf-8") as fh:
230
+ issue_body = fh.read()
231
+ except OSError as exc:
232
+ sys.stderr.write(f"_intentlib.py: cannot read {issue_body_path}: {exc}\n")
233
+ return 2
234
+
235
+ findings = uncovered_intent(spec_text, issue_body)
236
+ for finding in findings:
237
+ print(finding)
238
+ return 1 if findings else 0
239
+
240
+
241
+ if __name__ == "__main__":
242
+ sys.exit(main(sys.argv[1:]))