@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.
- package/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- 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:]))
|