@massa-ai/cursor-plugin 1.20.0 → 1.21.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/.cursor-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/agents/audit-specialist/SKILL.md +1 -1
- package/skills/agents/context-curator/SKILL.md +1 -1
- package/skills/agents/furps-analyst/SKILL.md +1 -1
- package/skills/agents/investigator/SKILL.md +1 -1
- package/skills/agents/mobile-specialist/SKILL.md +1 -1
- package/skills/agents/navigator/SKILL.md +1 -1
- package/skills/agents/requirements-analyst/SKILL.md +1 -1
- package/skills/agents/reviewer/SKILL.md +1 -1
- package/skills/agents/verification-agent/SKILL.md +1 -1
- package/skills/massa-ai/SKILL.md +10 -0
- package/skills/massa-ai/references/implementation-delivery.md +12 -1
- package/skills/massa-ai/references/spec-driven/coding-principles.md +16 -0
- package/skills/massa-ai/references/spec-driven/design.md +2 -2
- package/skills/massa-ai/references/spec-driven/discuss.md +35 -12
- package/skills/massa-ai/references/spec-driven/execute.md +41 -24
- package/skills/massa-ai/references/spec-driven/memory.md +12 -2
- package/skills/massa-ai/references/spec-driven/specify.md +30 -12
- package/skills/massa-ai/references/spec-driven/sub-agents.md +33 -6
- package/skills/massa-ai/references/spec-driven/tasks.md +8 -6
- package/skills/massa-ai/references/spec-driven/validate.md +15 -10
- package/skills/massa-ai/scripts/check_commit.py +128 -0
- package/skills/massa-ai/scripts/check_specs_delivered.py +137 -0
- package/skills/massa-ai/scripts/lessons.py +44 -4
- package/skills/massa-ai/scripts/validate_spec.py +272 -0
- package/skills/massa-ai/scripts/validate_state.py +183 -0
- package/skills/massa-ai/scripts/validate_tasks.py +302 -0
- package/skills/massa-ai/workflows/exploration.md +1 -1
- package/skills/massa-ai/workflows/spec-driven.md +5 -4
|
@@ -58,7 +58,7 @@ This step ALWAYS runs — there is no precondition. Decide which of two paths to
|
|
|
58
58
|
|
|
59
59
|
Before sampling tests or inferring anything, scan the project for documented quality and testing standards. Stack-agnostic sources to check (illustrative, not exhaustive):
|
|
60
60
|
|
|
61
|
-
- Agent/AI
|
|
61
|
+
- Agent/AI convention files, if the repo has any: `AGENTS.md` (the vendor-neutral standard) and any tool-specific rules file or rules directory the project happens to use
|
|
62
62
|
- Contributor guides: `CONTRIBUTING.md`, `docs/` (testing, quality, or standards subdocs), README testing section
|
|
63
63
|
- Tool configuration: coverage thresholds in the test runner config (e.g., `jest.config.*`, `vitest.config.*`, `pytest.ini`, `.nycrc`, `Makefile` coverage targets, CI coverage gates)
|
|
64
64
|
|
|
@@ -74,7 +74,7 @@ Before sampling tests or inferring anything, scan the project for documented qua
|
|
|
74
74
|
**How to infer (path 1 — existing tests):**
|
|
75
75
|
|
|
76
76
|
1. **Sample test files.** Locate 5–10 existing test files. Map each file's location relative to its source file to identify which code layers are exercised and at what level (unit, integration, e2e). Use these samples for style, location patterns, framework, and test type — and as a **floor** (never produce tests less thorough than existing ones for the same layer). Existing tests are NOT a ceiling on thoroughness; the thoroughness target comes from the spec ACs, listed edge cases, and guidelines (or strong default). The Coverage Expectation column captures the target per layer.
|
|
77
|
-
2. **Discover commands from the repo.** Do NOT invent commands and do NOT assume an ecosystem. Read the project's own build/task manifests, test config, and CI workflows to extract the actual commands — for example: `package.json` / `project.json` (JS/TS), `Makefile`, `pyproject.toml` / `tox.ini` / `pytest` (Python), `Cargo.toml` (Rust), `go test` invocations (Go), `pom.xml` / `build.gradle` (Java/Kotlin), `Gemfile` / `Rakefile` (Ruby), `composer.json` (PHP), `.github/workflows` / `.gitlab-ci.yml`. The list is illustrative; detect what this repo actually uses.
|
|
77
|
+
2. **Discover commands from the repo.** Do NOT invent commands and do NOT assume an ecosystem. Read the project's own build/task manifests, test config, and CI workflows to extract the actual commands — for example: `package.json` / `project.json` (JS/TS), `Makefile`, `pyproject.toml` / `tox.ini` / `pytest` (Python), `Cargo.toml` (Rust), `go test` invocations (Go), `pom.xml` / `build.gradle` (Java/Kotlin), `Gemfile` / `Rakefile` (Ruby), `composer.json` (PHP), `.github/workflows` / `.gitlab-ci.yml`. The list is illustrative; detect what this repo actually uses. Capture the **linter/formatter** command too (e.g. the configured `lint`/`format`/`typecheck` script, or a `.pre-commit-config`, `.golangci.yml`, `ruff`/`eslint`/`biome` config) — the Build gate runs it alongside the tests.
|
|
78
78
|
|
|
79
79
|
**Output contract — render these two sections verbatim into `tasks.md`** (the exact headings downstream phases reference):
|
|
80
80
|
|
|
@@ -157,6 +157,8 @@ This keeps phase boundaries meaningful while letting the packing hit its target
|
|
|
157
157
|
|
|
158
158
|
Before showing tasks to the user, run ALL three pre-approval checks. These are NOT optional — they are gates. If any check fails, restructure the tasks and re-run until all pass.
|
|
159
159
|
|
|
160
|
+
**Deterministic backing (run it, do not eyeball it):** `python3 skills/massa-ai/scripts/validate_tasks.py <feature> [--root .]` enforces the structural half of these checks so they cannot drift: it flags a `Where` that names multiple files (granularity smell, Check 1), a diagram edge with no matching `Depends on` within a phase and vice-versa (Check 2), a task missing its `Tests` or `Gate` field, a `Tests: none` to confirm against the matrix (Check 3), and any dependency pointing to a later phase. A non-zero exit means restructure before presenting. The script checks structure; the two tables below (the layer-to-test co-location judgment) are still yours. If no code-execution tool is available, run the same checks by reading the artifact (graceful degradation preserved).
|
|
161
|
+
|
|
160
162
|
**Check 1: Task Granularity** — verify each task is atomic (see Granularity Check section).
|
|
161
163
|
|
|
162
164
|
**Check 2: Diagram-Definition Cross-Check** — verify the execution diagram matches every task's `Depends on` field (see Diagram-Definition Cross-Check section). Build the cross-check table and include it in the output.
|
|
@@ -350,12 +352,12 @@ Execution is strictly sequential — there is no intra-phase parallelism. A sing
|
|
|
350
352
|
|
|
351
353
|
**How phase-based execution works:**
|
|
352
354
|
|
|
353
|
-
At Execute, the agent counts total tasks and packs phases into **task-budgeted batches** (~7 tasks per worker, whole phases — the benchmarked sweet spot is ~20 tasks → ~3 workers). A **phase** is the semantic/dependency unit; a **batch** is one or more *consecutive whole phases* assigned to one worker. The cut only ever lands on a phase boundary — a phase is never split across workers.
|
|
355
|
+
At Execute, the agent counts total tasks and packs phases into **task-budgeted batches** (~7 tasks per worker, whole phases — the benchmarked sweet spot is ~20 tasks → ~3 workers). A **phase** is the semantic/dependency unit; a **batch** is one or more *consecutive whole phases* assigned to one worker. The cut only ever lands on a phase boundary — a phase is never split across workers. **The sub-agent offer fires whenever the feature has more than 3 tasks** — a 4–8-task feature still packs into a single batch and is offered as one batch worker; only a feature with 3 or fewer tasks executes inline with no offer. Batches run sequentially: each worker executes ALL its tasks in order, then reports a compact summary before the next batch starts. This right-sizes the worker count by workload instead of by phase count (one-per-phase is too fragmented; expensive and slow). See `references/spec-driven/sub-agents.md` for the full model — packing algorithm, offer-then-confirm, worker payload, compact summary contract, failure handling, and context sizing guidance.
|
|
354
356
|
|
|
355
|
-
When the whole feature
|
|
357
|
+
When the whole feature has 3 or fewer tasks, execution happens inline in the main window with no sub-agents spawned and no offer made.
|
|
356
358
|
|
|
357
359
|
**The orchestrating agent's role during Execute:**
|
|
358
|
-
1. Count total tasks
|
|
360
|
+
1. Count total tasks — if more than 3, pack phases into ~7-task batches and offer batch sub-agents (even a single resulting batch is offered); wait for the user's choice
|
|
359
361
|
2. Dispatch the next batch (to a worker, or execute inline)
|
|
360
362
|
3. Receive the compact batch summary
|
|
361
363
|
4. Update tasks.md with results
|
|
@@ -514,4 +516,4 @@ Before sampling tests manually, prefer massa-ai tooling to read the codebase:
|
|
|
514
516
|
|
|
515
517
|
## Done
|
|
516
518
|
|
|
517
|
-
Tasks is done when every in-scope requirement maps to one or more executable tasks, dependencies are explicit, the Test Coverage Matrix and Gate Check Commands are present, project testing guideline scan and pre-approval checks are recorded, the three mandatory pre-approval tables (Granularity, Diagram-Definition Cross-Check, Test Co-location Validation) pass, and each task has deterministic validation with artifact-store evidence recorded.
|
|
519
|
+
Tasks is done when every in-scope requirement maps to one or more executable tasks, dependencies are explicit, the Test Coverage Matrix and Gate Check Commands are present, project testing guideline scan and pre-approval checks are recorded, the three mandatory pre-approval tables (Granularity, Diagram-Definition Cross-Check, Test Co-location Validation) pass, `validate_tasks.py` exits clean (or the no-code-execution-tool fallback was applied), and each task has deterministic validation with artifact-store evidence recorded.
|
|
@@ -27,7 +27,7 @@ Use this reference for the mandatory final Execute validation gate. This is not
|
|
|
27
27
|
|
|
28
28
|
Prefer a fresh read-only verifier agent or tool. The author must not verify their own work when independent verification tooling is available. When subagents are unavailable, run the standalone fresh-eyes fallback: re-read `spec.md`, changed files, tests, and diff from scratch before making a verdict.
|
|
29
29
|
|
|
30
|
-
The verifier is read-only against the real worktree. Discrimination sensor mutations run only in scratch state
|
|
30
|
+
The verifier is read-only against the real worktree. Discrimination sensor mutations run only in an isolated scratch state — a temporary git worktree (preferred) or temp file copies (fallback), never `git stash` — and must be reverted or discarded before verdict.
|
|
31
31
|
|
|
32
32
|
## Inputs
|
|
33
33
|
|
|
@@ -100,18 +100,20 @@ The sensor provides the empirical guarantee that the tests can actually detect r
|
|
|
100
100
|
|
|
101
101
|
**How it works:**
|
|
102
102
|
|
|
103
|
-
1. **Prepare
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
- A
|
|
107
|
-
2. **
|
|
103
|
+
1. **Prepare an isolated scratch.** Never mutate the real worktree. Choose one:
|
|
104
|
+
- Preferred: a temporary git worktree (`git worktree add <scratch-path> HEAD`), mutate and run tests there, then `git worktree remove --force <scratch-path>`.
|
|
105
|
+
- Fallback (no git / worktree unavailable): copy only the affected file(s) to a temp directory, mutate the copies, point the test runner at those copies (or restore originals from the copies' backups), then delete the temp directory.
|
|
106
|
+
- **Forbidden:** `git stash` / `git stash pop`. A stash records state *before* the mutation; popping it does not reverse a mutation applied afterward, and on a clean tree `git stash` creates no entry at all — so the fault is left in the real worktree.
|
|
107
|
+
2. **Capture a baseline.** Record `git status --porcelain` (or equivalent) of the real worktree *before* any sensor work. It must be unchanged after cleanup.
|
|
108
|
+
3. **Inject a behavior-level fault** into the scratch copy of the new code introduced by this feature. Choose a mutation proportional to the code's risk:
|
|
108
109
|
- Flip a boolean condition (`if (x)` → `if (!x)`, `>` → `>=`)
|
|
109
110
|
- Change a return value (return a wrong status code, wrong field, zero instead of a computed value)
|
|
110
111
|
- Off-by-one (shift a loop bound, change a slice index)
|
|
111
112
|
- Remove a required side effect (delete a method call that the spec requires)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
4. **Run the tests** that cover the mutated code (against the scratch). Use the Quick or Full gate command from tasks.md.
|
|
114
|
+
5. **Confirm the mutant is killed** (tests FAIL). Discard the scratch (remove worktree or delete temp copies).
|
|
115
|
+
6. **Verify isolation.** Re-run `git status --porcelain` on the real worktree and confirm it matches the baseline from step 2. If it differs, STOP — restore the real tree before continuing, and treat the sensor run as invalid.
|
|
116
|
+
7. **If a mutant survives** (tests still pass after the fault), the tests are not discriminating for that behavior — add a fix task to strengthen the assertion.
|
|
115
117
|
|
|
116
118
|
**Tiering (proportional, not optional):**
|
|
117
119
|
|
|
@@ -202,6 +204,8 @@ After all checks complete, the Verifier MUST:
|
|
|
202
204
|
1. **Write the persisted report** to `.specs/features/<slug>/validation.md` (see template below). This file is the evidence artifact — it survives the session and can be referenced by CI, reviewers, or future agents. Record in `.specs/project/STATE.md` (Decisions) that validation evidence is available at that path.
|
|
203
205
|
2. **Return a compact summary in chat** to the orchestrator (see Compact Chat Summary section below). The orchestrator surfaces it to the user and routes any ranked gaps to fix tasks.
|
|
204
206
|
|
|
207
|
+
**Deterministic backing (run it, do not eyeball it):** after writing the report, run `python3 skills/massa-ai/scripts/validate_state.py <feature> [--root .]`. It confirms the report is real — present, verdict filled to PASS, and backed by at least one `file:line` evidence citation — so a missing, hollow, placeholder, or FAIL report cannot slip through as done. A non-zero exit means the feature is NOT done: repair the report or route the FAIL gaps to fix tasks, then re-run. This is the closing gate of Execute and runs automatically, the same way the lessons layer runs at distillation — never a manual step. If no code-execution tool is available, run the same checks by reading the artifact (graceful degradation preserved).
|
|
208
|
+
|
|
205
209
|
### 10. Distill Lessons (MANDATORY when validation.md has signal)
|
|
206
210
|
|
|
207
211
|
This is the closing action of validation — not a separate phase. Immediately after the report is written, turn its grounded failures into reusable, project-local guidance by following [references/lessons.md](../lessons.md) and the stub at [references/spec-driven/lessons.md](lessons.md). In short: for each surviving mutant, spec-precision gap, failed/uncovered AC, or `// SPEC_DEVIATION`, record one terse general lesson via:
|
|
@@ -364,6 +368,7 @@ Update `.specs/features/<slug>/spec.md` requirement statuses and reflect verifie
|
|
|
364
368
|
## Summary
|
|
365
369
|
|
|
366
370
|
**Overall**: ✅ Ready | ⚠️ Issues | ❌ Not Ready
|
|
371
|
+
**Result**: PASS | FAIL
|
|
367
372
|
|
|
368
373
|
**Spec-anchored check**: [N/N ACs matched spec outcome | M spec-precision gaps]
|
|
369
374
|
**Sensor**: [N/N mutations killed]
|
|
@@ -382,7 +387,7 @@ Update `.specs/features/<slug>/spec.md` requirement statuses and reflect verifie
|
|
|
382
387
|
|
|
383
388
|
- **Validation is never prompted** — it always runs after the last task; do not ask the user whether to run it
|
|
384
389
|
- **Spec-anchored, not just covered** — "there is an assertion" is not enough; the assertion must target the spec-defined outcome
|
|
385
|
-
- **Sensor in scratch only** — never mutate the real tree;
|
|
390
|
+
- **Sensor in scratch only** — never mutate the real tree; use a temp worktree or file copies (never `git stash`), run, discard, then confirm porcelain matches the pre-sensor baseline
|
|
386
391
|
- **Surviving mutants are fix tasks** — do not mark the feature done if the sensor found weak tests
|
|
387
392
|
- **P1 first** — MVP must work before P2/P3
|
|
388
393
|
- **WHEN/THEN = Test** — Each criterion is a test case
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
check_commit.py - deterministic Conventional Commits validation.
|
|
4
|
+
|
|
5
|
+
The per-task atomic-commit rule mandates Conventional Commits 1.0.0. This makes
|
|
6
|
+
that rule checkable instead of trusting the model to remember the format. Pure
|
|
7
|
+
standard library, zero dependencies, agent-agnostic.
|
|
8
|
+
|
|
9
|
+
It reads the message from (in priority order): a positional file path, --message,
|
|
10
|
+
or stdin. The file-path form matches how git passes the message file to a
|
|
11
|
+
`commit-msg` hook, so this doubles as an optional git-level guard WITHOUT
|
|
12
|
+
coupling the skill to any AI agent:
|
|
13
|
+
|
|
14
|
+
ln -s skills/massa-ai/scripts/check_commit.py .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
|
|
15
|
+
|
|
16
|
+
What it checks:
|
|
17
|
+
ERROR - header does not match type(scope)!: description
|
|
18
|
+
(an optional leading `[KEY] ` Jira-style prefix is stripped first -
|
|
19
|
+
see massa-ai's `workflows/commit.md` §8, e.g. `[SA-142] feat(x): y`)
|
|
20
|
+
ERROR - type is not one of the allowed Conventional Commits types
|
|
21
|
+
ERROR - description is empty, starts uppercase, or ends with a period
|
|
22
|
+
ERROR - `!` breaking marker present but no `BREAKING CHANGE:` footer
|
|
23
|
+
WARN - header longer than 72 characters
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
python3 skills/massa-ai/scripts/check_commit.py [msgfile]
|
|
27
|
+
python3 skills/massa-ai/scripts/check_commit.py --message "feat(auth): add email validation"
|
|
28
|
+
python3 skills/massa-ai/scripts/check_commit.py --message "[SA-142] feat(auth): reject expired tokens"
|
|
29
|
+
echo "fix(cart): prevent negative quantity" | python3 skills/massa-ai/scripts/check_commit.py
|
|
30
|
+
|
|
31
|
+
Exit codes: 0 pass, 1 violation, 2 usage error.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
TYPES = ["feat", "fix", "refactor", "docs", "test", "style", "perf", "build", "ci", "chore"]
|
|
39
|
+
|
|
40
|
+
# massa-ai patch (D1c): an optional leading Jira-style key prefix, e.g.
|
|
41
|
+
# "[SA-142] feat(auth): reject expired tokens" (workflows/commit.md §8). The
|
|
42
|
+
# key shape mirrors the branch-key regex there: [A-Z][A-Z0-9]{1,9}-\d+.
|
|
43
|
+
PREFIX_RE = re.compile(r"^\[(?P<key>[A-Z][A-Z0-9]{1,9}-\d+)\]\s+(?P<rest>.+)$")
|
|
44
|
+
HEADER_RE = re.compile(r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<bang>!)?: (?P<desc>.+)$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_message(args):
|
|
48
|
+
if args.message is not None:
|
|
49
|
+
return args.message
|
|
50
|
+
if args.msgfile:
|
|
51
|
+
with open(args.msgfile, "r", encoding="utf-8") as f:
|
|
52
|
+
return f.read()
|
|
53
|
+
if not sys.stdin.isatty():
|
|
54
|
+
return sys.stdin.read()
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check(message):
|
|
59
|
+
errors, warnings = [], []
|
|
60
|
+
# Ignore comment lines (git puts '#' comments in the message file).
|
|
61
|
+
lines = [ln for ln in message.splitlines() if not ln.lstrip().startswith("#")]
|
|
62
|
+
# Trim leading blank lines.
|
|
63
|
+
while lines and not lines[0].strip():
|
|
64
|
+
lines.pop(0)
|
|
65
|
+
if not lines:
|
|
66
|
+
return (["empty commit message"], warnings)
|
|
67
|
+
|
|
68
|
+
header = lines[0].rstrip()
|
|
69
|
+
if len(header) > 72:
|
|
70
|
+
warnings.append(f"header is {len(header)} chars (>72): {header[:60]}...")
|
|
71
|
+
|
|
72
|
+
# Strip an optional massa-ai Jira-style `[KEY] ` prefix before matching the
|
|
73
|
+
# Conventional Commits header shape.
|
|
74
|
+
prefixed = PREFIX_RE.match(header)
|
|
75
|
+
header_body = prefixed.group("rest") if prefixed else header
|
|
76
|
+
|
|
77
|
+
m = HEADER_RE.match(header_body)
|
|
78
|
+
if not m:
|
|
79
|
+
errors.append(f"header does not match 'type(scope): description': {header!r}")
|
|
80
|
+
return (errors, warnings)
|
|
81
|
+
|
|
82
|
+
ctype = m.group("type")
|
|
83
|
+
desc = m.group("desc")
|
|
84
|
+
bang = m.group("bang")
|
|
85
|
+
|
|
86
|
+
if ctype not in TYPES:
|
|
87
|
+
errors.append(f"type '{ctype}' is not one of: {', '.join(TYPES)}")
|
|
88
|
+
if not desc.strip():
|
|
89
|
+
errors.append("description is empty")
|
|
90
|
+
else:
|
|
91
|
+
if desc[:1].isupper():
|
|
92
|
+
errors.append(f"description should start lowercase: '{desc[:30]}'")
|
|
93
|
+
if desc.rstrip().endswith("."):
|
|
94
|
+
errors.append("description should not end with a period")
|
|
95
|
+
|
|
96
|
+
body = "\n".join(lines[1:])
|
|
97
|
+
breaking_footer = bool(re.search(r"^BREAKING CHANGE:", body, re.MULTILINE))
|
|
98
|
+
if bang and not breaking_footer:
|
|
99
|
+
errors.append("'!' breaking marker present but no 'BREAKING CHANGE:' footer")
|
|
100
|
+
|
|
101
|
+
return (errors, warnings)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(argv=None):
|
|
105
|
+
p = argparse.ArgumentParser(prog="check_commit.py", description="Validate a Conventional Commits message.")
|
|
106
|
+
p.add_argument("msgfile", nargs="?", default=None, help="path to a commit message file (as git passes to commit-msg)")
|
|
107
|
+
p.add_argument("--message", default=None, help="the commit message as a string")
|
|
108
|
+
args = p.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
message = read_message(args)
|
|
111
|
+
if not message.strip():
|
|
112
|
+
print("check_commit: no message provided (pass a file, --message, or pipe via stdin).", file=sys.stderr)
|
|
113
|
+
return 2
|
|
114
|
+
|
|
115
|
+
errors, warnings = check(message)
|
|
116
|
+
for w in warnings:
|
|
117
|
+
print(f" WARN {w}")
|
|
118
|
+
for e in errors:
|
|
119
|
+
print(f" ERROR {e}")
|
|
120
|
+
if errors:
|
|
121
|
+
print("\ncheck_commit: FAIL - see https://www.conventionalcommits.org/en/v1.0.0/")
|
|
122
|
+
return 1
|
|
123
|
+
print("check_commit: OK")
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
check_specs_delivered.py - deterministic gate: .specs/ artifacts are
|
|
4
|
+
committed on the branch before a PR is opened (GATE-02).
|
|
5
|
+
|
|
6
|
+
New script, not a TLC port - massa-ai's `implementation-delivery.md` chain has
|
|
7
|
+
no gate requiring `.specs/` (spec/context/design/tasks/validation, project
|
|
8
|
+
state, handoff, features registry) to be committed before `gh pr create`. This
|
|
9
|
+
turns that gap into two conjunctive, deterministic checks:
|
|
10
|
+
|
|
11
|
+
1. `git status --porcelain -- .specs/` is empty - nothing under .specs/ is
|
|
12
|
+
modified-but-uncommitted or untracked.
|
|
13
|
+
2. The feature's `spec.md` (always required) plus any of
|
|
14
|
+
`{context,design,tasks,validation}.md` that exist on disk, plus
|
|
15
|
+
`.specs/project/STATE.md`, `.specs/HANDOFF.md`, and
|
|
16
|
+
`.specs/project/FEATURES.json`, are tracked on HEAD
|
|
17
|
+
(`git ls-tree -r --name-only HEAD`).
|
|
18
|
+
|
|
19
|
+
Check 2 exists because check 1 alone is not sufficient: a feature whose
|
|
20
|
+
`.specs/` artifacts were simply never written is porcelain-clean (nothing to
|
|
21
|
+
be dirty about) while still failing the actual requirement. Absence must fail,
|
|
22
|
+
not pass.
|
|
23
|
+
|
|
24
|
+
Pure `git` + standard library. No dependencies. Run from the project root (the
|
|
25
|
+
dir that contains .specs/), or pass --root.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
python3 skills/massa-ai/scripts/check_specs_delivered.py <feature> [--root DIR]
|
|
29
|
+
|
|
30
|
+
Exit codes: 0 all required paths clean + tracked, 1 a required path is dirty,
|
|
31
|
+
untracked, or not tracked on HEAD (paths named), 2 usage/git error.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import os
|
|
36
|
+
import subprocess
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
# Always required for the named feature.
|
|
40
|
+
FEATURE_REQUIRED = ["spec.md"]
|
|
41
|
+
# Required only when present on disk (not every feature reaches every phase).
|
|
42
|
+
FEATURE_OPTIONAL = ["context.md", "design.md", "tasks.md", "validation.md"]
|
|
43
|
+
|
|
44
|
+
STATE_FILES = [
|
|
45
|
+
os.path.join(".specs", "project", "STATE.md"),
|
|
46
|
+
os.path.join(".specs", "HANDOFF.md"),
|
|
47
|
+
os.path.join(".specs", "project", "FEATURES.json"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_git(args, root):
|
|
52
|
+
try:
|
|
53
|
+
proc = subprocess.run(
|
|
54
|
+
["git"] + args, cwd=root, capture_output=True, text=True, check=False
|
|
55
|
+
)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
print("check_specs_delivered: git not found on PATH", file=sys.stderr)
|
|
58
|
+
raise SystemExit(2)
|
|
59
|
+
if proc.returncode != 0:
|
|
60
|
+
print(
|
|
61
|
+
f"check_specs_delivered: git {' '.join(args)} failed: {proc.stderr.strip()}",
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
raise SystemExit(2)
|
|
65
|
+
return proc.stdout
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _porcelain_dirty_paths(root):
|
|
69
|
+
"""Lines from `git status --porcelain -- .specs/` (empty = clean)."""
|
|
70
|
+
out = _run_git(["status", "--porcelain", "--", ".specs/"], root)
|
|
71
|
+
return [ln for ln in out.splitlines() if ln.strip()]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tracked_on_head(root):
|
|
75
|
+
"""Set of every path tracked on HEAD, repo-root-relative, '/'-separated."""
|
|
76
|
+
out = _run_git(["ls-tree", "-r", "--name-only", "HEAD"], root)
|
|
77
|
+
return set(out.splitlines())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_feature_dir(root, feature):
|
|
81
|
+
if os.path.isabs(feature) or os.sep in feature:
|
|
82
|
+
return os.path.normpath(feature)
|
|
83
|
+
return os.path.join(root, ".specs", "features", feature)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def required_paths(root, feature):
|
|
87
|
+
"""Repo-root-relative, '/'-separated paths this feature must have tracked."""
|
|
88
|
+
fdir = _resolve_feature_dir(root, feature)
|
|
89
|
+
fdir_rel = os.path.relpath(fdir, root)
|
|
90
|
+
paths = [os.path.join(fdir_rel, name) for name in FEATURE_REQUIRED]
|
|
91
|
+
if os.path.isdir(fdir):
|
|
92
|
+
for name in FEATURE_OPTIONAL:
|
|
93
|
+
if os.path.isfile(os.path.join(fdir, name)):
|
|
94
|
+
paths.append(os.path.join(fdir_rel, name))
|
|
95
|
+
paths.extend(STATE_FILES)
|
|
96
|
+
return [p.replace(os.sep, "/") for p in paths]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check(root, feature):
|
|
100
|
+
"""Return (errors, checked_paths). errors empty = pass."""
|
|
101
|
+
errors = []
|
|
102
|
+
|
|
103
|
+
for ln in _porcelain_dirty_paths(root):
|
|
104
|
+
errors.append(f"uncommitted/untracked under .specs/: {ln.strip()}")
|
|
105
|
+
|
|
106
|
+
paths = required_paths(root, feature)
|
|
107
|
+
tracked = _tracked_on_head(root)
|
|
108
|
+
for p in paths:
|
|
109
|
+
if p not in tracked:
|
|
110
|
+
errors.append(f"not tracked on HEAD: {p}")
|
|
111
|
+
|
|
112
|
+
return errors, paths
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main(argv=None):
|
|
116
|
+
p = argparse.ArgumentParser(
|
|
117
|
+
prog="check_specs_delivered.py",
|
|
118
|
+
description="Gate: .specs/ artifacts committed on the branch before PR (GATE-02).",
|
|
119
|
+
)
|
|
120
|
+
p.add_argument("feature", help="Feature slug under <root>/.specs/features/, or a direct path")
|
|
121
|
+
p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
|
|
122
|
+
args = p.parse_args(argv)
|
|
123
|
+
root = os.path.abspath(args.root)
|
|
124
|
+
|
|
125
|
+
errors, checked = check(root, args.feature)
|
|
126
|
+
|
|
127
|
+
for e in errors:
|
|
128
|
+
print(f" ERROR {e}")
|
|
129
|
+
print(f"\ncheck_specs_delivered: checked {len(checked)} path(s):")
|
|
130
|
+
for c in checked:
|
|
131
|
+
print(f" - {c}")
|
|
132
|
+
print(f"check_specs_delivered: {len(errors)} error(s)")
|
|
133
|
+
return 1 if errors else 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
raise SystemExit(main())
|
|
@@ -24,6 +24,7 @@ Commands:
|
|
|
24
24
|
observe Ingest a JSON observation into the gitignored observations buffer.
|
|
25
25
|
export Export the lessons store as JSON (round-trips with import).
|
|
26
26
|
import Import lessons from JSON (merge by dedup key; best-effort massa-ai memory).
|
|
27
|
+
selftest Run stdlib regressions (normalization).
|
|
27
28
|
|
|
28
29
|
Exit codes: 0 ok, 2 usage/validation error (e.g. missing grounding).
|
|
29
30
|
"""
|
|
@@ -34,6 +35,7 @@ import json
|
|
|
34
35
|
import os
|
|
35
36
|
import re
|
|
36
37
|
import sys
|
|
38
|
+
import unicodedata
|
|
37
39
|
import urllib.request
|
|
38
40
|
|
|
39
41
|
STORE_REL = os.path.join(".specs", "lessons.json")
|
|
@@ -178,15 +180,50 @@ def _lesson_tags(lesson):
|
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
def _norm(text):
|
|
181
|
-
"""Normalized dedup key
|
|
183
|
+
"""Normalized dedup key for lesson text.
|
|
184
|
+
|
|
185
|
+
- casefold + NFD, strip combining marks (so Portuguese diacritics match ASCII peers)
|
|
186
|
+
- keep characters where str.isalnum() is true (any script) and whitespace
|
|
187
|
+
- drop other punctuation, collapse whitespace
|
|
188
|
+
|
|
182
189
|
Exact-after-normalization only - no semantic matching (stdlib-only limitation).
|
|
183
|
-
Phrase lessons tersely and canonically so recurrences actually merge.
|
|
184
|
-
|
|
185
|
-
t =
|
|
190
|
+
Phrase lessons tersely and canonically so recurrences actually merge.
|
|
191
|
+
"""
|
|
192
|
+
t = unicodedata.normalize("NFD", text.casefold())
|
|
193
|
+
t = "".join(c for c in t if unicodedata.category(c) != "Mn")
|
|
194
|
+
t = "".join(c if (c.isalnum() or c.isspace()) else " " for c in t)
|
|
186
195
|
t = re.sub(r"\s+", " ", t).strip()
|
|
187
196
|
return t
|
|
188
197
|
|
|
189
198
|
|
|
199
|
+
def _selftest_norm():
|
|
200
|
+
"""Regressions for #158: Portuguese diacritics + distinct non-Latin text."""
|
|
201
|
+
failures = []
|
|
202
|
+
|
|
203
|
+
def check(cond, msg):
|
|
204
|
+
if not cond:
|
|
205
|
+
failures.append(msg)
|
|
206
|
+
|
|
207
|
+
a = _norm("Não use datas locais")
|
|
208
|
+
b = _norm("Nao use datas locais")
|
|
209
|
+
check(a == b == "nao use datas locais", f"PT diacritics: {a!r} vs {b!r}")
|
|
210
|
+
|
|
211
|
+
jp1 = _norm("日本語の文です")
|
|
212
|
+
jp2 = _norm("別の日本語文")
|
|
213
|
+
check(jp1 != "", f"JP1 empty: {jp1!r}")
|
|
214
|
+
check(jp2 != "", f"JP2 empty: {jp2!r}")
|
|
215
|
+
check(jp1 != jp2, f"JP sentences collapsed: {jp1!r} == {jp2!r}")
|
|
216
|
+
|
|
217
|
+
check(_norm("café") == _norm("cafe") == "cafe", f"cafe: {_norm('café')!r}")
|
|
218
|
+
|
|
219
|
+
if failures:
|
|
220
|
+
for f in failures:
|
|
221
|
+
print(f"FAIL: {f}", file=sys.stderr)
|
|
222
|
+
return 1
|
|
223
|
+
print("selftest_norm: ok")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
190
227
|
def _key(signal, text):
|
|
191
228
|
return signal + "::" + _norm(text)
|
|
192
229
|
|
|
@@ -581,6 +618,9 @@ def main(argv=None):
|
|
|
581
618
|
sp = sub.add_parser("status", help="Print counts")
|
|
582
619
|
sp.set_defaults(fn=cmd_status)
|
|
583
620
|
|
|
621
|
+
sp = sub.add_parser("selftest", help="Run stdlib regressions (normalization)")
|
|
622
|
+
sp.set_defaults(fn=lambda root, args: _selftest_norm())
|
|
623
|
+
|
|
584
624
|
args = p.parse_args(argv)
|
|
585
625
|
root = os.path.abspath(args.root)
|
|
586
626
|
return args.fn(root, args)
|