@massa-ai/cursor-plugin 1.21.0 → 1.23.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/massa-ai/SKILL.md +9 -0
- package/skills/massa-ai/references/evidence-gate.md +1 -1
- package/skills/massa-ai/references/figma-pre-analysis.md +69 -0
- package/skills/massa-ai/references/hook-enforcement.md +2 -2
- package/skills/massa-ai/references/implementation-delivery.md +17 -3
- package/skills/massa-ai/references/lessons.md +9 -10
- package/skills/massa-ai/references/mcp-tools.md +1 -1
- package/skills/massa-ai/references/mobile-context.md +19 -0
- package/skills/massa-ai/references/mobile-diagnosis.md +1 -1
- package/skills/massa-ai/references/project-context.md +1 -1
- package/skills/massa-ai/references/spec-driven/artifact-store.md +7 -8
- package/skills/massa-ai/references/spec-driven/design.md +1 -1
- package/skills/massa-ai/references/spec-driven/execute.md +5 -5
- package/skills/massa-ai/references/spec-driven/specify.md +6 -6
- package/skills/massa-ai/references/spec-driven/sub-agents.md +1 -1
- package/skills/massa-ai/references/spec-driven/tasks.md +2 -2
- package/skills/massa-ai/references/spec-driven/validate.md +3 -3
- package/skills/massa-ai/scripts/check_commit.ts +231 -0
- package/skills/massa-ai/scripts/check_specs_delivered.ts +209 -0
- package/skills/massa-ai/scripts/lessons.ts +907 -0
- package/skills/massa-ai/scripts/validate_spec.ts +413 -0
- package/skills/massa-ai/scripts/validate_state.ts +276 -0
- package/skills/massa-ai/scripts/validate_tasks.ts +498 -0
- package/skills/massa-ai/workflows/architecture/architecture-fix.md +1 -1
- package/skills/massa-ai/workflows/bugs/bugs-fix.md +1 -1
- package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +1 -1
- package/skills/massa-ai/workflows/debug.md +1 -1
- package/skills/massa-ai/workflows/design.md +1 -1
- package/skills/massa-ai/workflows/feature.md +2 -2
- package/skills/massa-ai/workflows/general.md +2 -2
- package/skills/massa-ai/workflows/implementation/implementation-fix.md +1 -1
- package/skills/massa-ai/workflows/maestro/maestro-fix.md +1 -1
- package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +1 -0
- package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +2 -1
- package/skills/massa-ai/workflows/refactor.md +1 -1
- package/skills/massa-ai/workflows/requirements/requirements-fix.md +1 -1
- package/skills/massa-ai/workflows/security/security-fix.md +1 -1
- package/skills/massa-ai/workflows/spec-driven.md +6 -6
- package/skills/massa-ai/workflows/tests/tests-fix.md +1 -1
- package/skills/massa-ai/scripts/check_commit.py +0 -128
- package/skills/massa-ai/scripts/check_specs_delivered.py +0 -137
- package/skills/massa-ai/scripts/lessons.py +0 -630
- package/skills/massa-ai/scripts/validate_spec.py +0 -272
- package/skills/massa-ai/scripts/validate_state.py +0 -183
- package/skills/massa-ai/scripts/validate_tasks.py +0 -302
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* check_commit.ts - 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. Bun
|
|
7
|
+
* builtins only, 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.ts .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
|
+
* bun skills/massa-ai/scripts/check_commit.ts [msgfile]
|
|
27
|
+
* bun skills/massa-ai/scripts/check_commit.ts --message "feat(auth): add email validation"
|
|
28
|
+
* bun skills/massa-ai/scripts/check_commit.ts --message "[SA-142] feat(auth): reject expired tokens"
|
|
29
|
+
* echo "fix(cart): prevent negative quantity" | bun skills/massa-ai/scripts/check_commit.ts
|
|
30
|
+
*
|
|
31
|
+
* Exit codes: 0 pass, 1 violation, 2 usage error.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
35
|
+
|
|
36
|
+
const TYPES = ["feat", "fix", "refactor", "docs", "test", "style", "perf", "build", "ci", "chore"];
|
|
37
|
+
|
|
38
|
+
// massa-ai patch (D1c): an optional leading Jira-style key prefix, e.g.
|
|
39
|
+
// "[SA-142] feat(auth): reject expired tokens" (workflows/commit.md §8). The
|
|
40
|
+
// key shape mirrors the branch-key regex there: [A-Z][A-Z0-9]{1,9}-\d+.
|
|
41
|
+
const PREFIX_RE = /^\[(?<key>[A-Z][A-Z0-9]{1,9}-\d+)\]\s+(?<rest>.+)$/;
|
|
42
|
+
const HEADER_RE = /^(?<type>\w+)(?:\((?<scope>[^)]+)\))?(?<bang>!)?: (?<desc>.+)$/;
|
|
43
|
+
|
|
44
|
+
const USAGE = "usage: check_commit.ts [-h] [--message MESSAGE] [msgfile]";
|
|
45
|
+
const HELP = `${USAGE}
|
|
46
|
+
|
|
47
|
+
Validate a Conventional Commits message.
|
|
48
|
+
|
|
49
|
+
positional arguments:
|
|
50
|
+
msgfile path to a commit message file (as git passes to commit-msg)
|
|
51
|
+
|
|
52
|
+
options:
|
|
53
|
+
-h, --help show this help message and exit
|
|
54
|
+
--message MESSAGE the commit message as a string`;
|
|
55
|
+
|
|
56
|
+
interface Args {
|
|
57
|
+
msgfile: string | null;
|
|
58
|
+
message: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Mirrors Python's str.splitlines(): universal newline split, no trailing empty element. */
|
|
62
|
+
function splitLines(text: string): string[] {
|
|
63
|
+
if (text === "") return [];
|
|
64
|
+
const result: string[] = [];
|
|
65
|
+
const lineBreakRe = /\r\n|\r|\n/g;
|
|
66
|
+
let start = 0;
|
|
67
|
+
let match: RegExpExecArray | null;
|
|
68
|
+
while ((match = lineBreakRe.exec(text)) !== null) {
|
|
69
|
+
result.push(text.slice(start, match.index));
|
|
70
|
+
start = match.index + match[0].length;
|
|
71
|
+
}
|
|
72
|
+
if (start < text.length) {
|
|
73
|
+
result.push(text.slice(start));
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Mirrors Python's repr() for plain-text strings (single-quoted, backslash/quote/control escapes). */
|
|
79
|
+
function pyRepr(s: string): string {
|
|
80
|
+
const hasSingle = s.includes("'");
|
|
81
|
+
const hasDouble = s.includes('"');
|
|
82
|
+
const quote = hasSingle && !hasDouble ? '"' : "'";
|
|
83
|
+
let out = quote;
|
|
84
|
+
for (const ch of s) {
|
|
85
|
+
if (ch === "\\") out += "\\\\";
|
|
86
|
+
else if (ch === quote) out += "\\" + quote;
|
|
87
|
+
else if (ch === "\n") out += "\\n";
|
|
88
|
+
else if (ch === "\r") out += "\\r";
|
|
89
|
+
else if (ch === "\t") out += "\\t";
|
|
90
|
+
else {
|
|
91
|
+
const code = ch.codePointAt(0)!;
|
|
92
|
+
if (code < 0x20 || code === 0x7f) {
|
|
93
|
+
out += "\\x" + code.toString(16).padStart(2, "0");
|
|
94
|
+
} else {
|
|
95
|
+
out += ch;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out += quote;
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function printUsageError(msg: string): void {
|
|
104
|
+
process.stderr.write(`${USAGE}\ncheck_commit.ts: error: ${msg}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseArgs(argv: string[]): Args | null {
|
|
108
|
+
let message: string | null = null;
|
|
109
|
+
const positionals: string[] = [];
|
|
110
|
+
for (let i = 0; i < argv.length; i++) {
|
|
111
|
+
const a = argv[i];
|
|
112
|
+
if (a === "--message") {
|
|
113
|
+
if (i + 1 >= argv.length) {
|
|
114
|
+
printUsageError("argument --message: expected one argument");
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
message = argv[++i];
|
|
118
|
+
} else if (a.startsWith("--message=")) {
|
|
119
|
+
message = a.slice("--message=".length);
|
|
120
|
+
} else if (a === "-h" || a === "--help") {
|
|
121
|
+
console.log(HELP);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
124
|
+
printUsageError(`unrecognized arguments: ${a}`);
|
|
125
|
+
return null;
|
|
126
|
+
} else {
|
|
127
|
+
positionals.push(a);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (positionals.length > 1) {
|
|
131
|
+
printUsageError(`unrecognized arguments: ${positionals.slice(1).join(" ")}`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return { msgfile: positionals[0] ?? null, message };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readMessage(args: Args): string {
|
|
138
|
+
if (args.message !== null) return args.message;
|
|
139
|
+
if (args.msgfile) {
|
|
140
|
+
return readFileSync(args.msgfile, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
if (!process.stdin.isTTY) {
|
|
143
|
+
try {
|
|
144
|
+
return readFileSync(0, "utf-8");
|
|
145
|
+
} catch {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function check(message: string): { errors: string[]; warnings: string[] } {
|
|
153
|
+
const errors: string[] = [];
|
|
154
|
+
const warnings: string[] = [];
|
|
155
|
+
// Ignore comment lines (git puts '#' comments in the message file).
|
|
156
|
+
let lines = splitLines(message).filter((ln) => !ln.trimStart().startsWith("#"));
|
|
157
|
+
// Trim leading blank lines.
|
|
158
|
+
while (lines.length && !lines[0]!.trim()) {
|
|
159
|
+
lines.shift();
|
|
160
|
+
}
|
|
161
|
+
if (!lines.length) {
|
|
162
|
+
return { errors: ["empty commit message"], warnings };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const header = lines[0]!.replace(/\s+$/, "");
|
|
166
|
+
if (header.length > 72) {
|
|
167
|
+
warnings.push(`header is ${header.length} chars (>72): ${header.slice(0, 60)}...`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Strip an optional massa-ai Jira-style `[KEY] ` prefix before matching the
|
|
171
|
+
// Conventional Commits header shape.
|
|
172
|
+
const prefixed = PREFIX_RE.exec(header);
|
|
173
|
+
const headerBody = prefixed?.groups?.rest ?? header;
|
|
174
|
+
|
|
175
|
+
const m = HEADER_RE.exec(headerBody);
|
|
176
|
+
if (!m || !m.groups) {
|
|
177
|
+
errors.push(`header does not match 'type(scope): description': ${pyRepr(header)}`);
|
|
178
|
+
return { errors, warnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ctype = m.groups.type!;
|
|
182
|
+
const desc = m.groups.desc!;
|
|
183
|
+
const bang = m.groups.bang;
|
|
184
|
+
|
|
185
|
+
if (!TYPES.includes(ctype)) {
|
|
186
|
+
errors.push(`type '${ctype}' is not one of: ${TYPES.join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
if (!desc.trim()) {
|
|
189
|
+
errors.push("description is empty");
|
|
190
|
+
} else {
|
|
191
|
+
if (/\p{Lu}/u.test(desc[0]!)) {
|
|
192
|
+
errors.push(`description should start lowercase: '${desc.slice(0, 30)}'`);
|
|
193
|
+
}
|
|
194
|
+
if (desc.replace(/\s+$/, "").endsWith(".")) {
|
|
195
|
+
errors.push("description should not end with a period");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const body = lines.slice(1).join("\n");
|
|
200
|
+
const breakingFooter = /^BREAKING CHANGE:/m.test(body);
|
|
201
|
+
if (bang && !breakingFooter) {
|
|
202
|
+
errors.push("'!' breaking marker present but no 'BREAKING CHANGE:' footer");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { errors, warnings };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function main(argv: string[]): number {
|
|
209
|
+
const args = parseArgs(argv);
|
|
210
|
+
if (args === null) return 2;
|
|
211
|
+
|
|
212
|
+
const message = readMessage(args);
|
|
213
|
+
if (!message.trim()) {
|
|
214
|
+
console.error("check_commit: no message provided (pass a file, --message, or pipe via stdin).");
|
|
215
|
+
return 2;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { errors, warnings } = check(message);
|
|
219
|
+
for (const w of warnings) console.log(` WARN ${w}`);
|
|
220
|
+
for (const e of errors) console.log(` ERROR ${e}`);
|
|
221
|
+
if (errors.length) {
|
|
222
|
+
console.log("\ncheck_commit: FAIL - see https://www.conventionalcommits.org/en/v1.0.0/");
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
console.log("check_commit: OK");
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (import.meta.main) {
|
|
230
|
+
process.exit(main(process.argv.slice(2)));
|
|
231
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* check_specs_delivered.ts - deterministic gate: .specs/ artifacts are
|
|
4
|
+
* committed on the branch before a PR is opened (GATE-02).
|
|
5
|
+
*
|
|
6
|
+
* massa-ai's `implementation-delivery.md` chain has no gate requiring
|
|
7
|
+
* `.specs/` (spec/context/design/tasks/validation, project state, handoff,
|
|
8
|
+
* features registry) to be committed before `gh pr create`. This turns that
|
|
9
|
+
* 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
|
+
* Bun builtins only, no new dependencies (D2 - `Bun.spawnSync` for the git
|
|
25
|
+
* calls). Run from the project root (the dir that contains .specs/), or pass
|
|
26
|
+
* --root.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* bun skills/massa-ai/scripts/check_specs_delivered.ts <feature> [--root DIR]
|
|
30
|
+
*
|
|
31
|
+
* Exit codes: 0 all required paths clean + tracked, 1 a required path is dirty,
|
|
32
|
+
* untracked, or not tracked on HEAD (paths named), 2 usage/git error.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync, statSync } from "node:fs";
|
|
36
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
37
|
+
|
|
38
|
+
// Always required for the named feature.
|
|
39
|
+
const FEATURE_REQUIRED = ["spec.md"];
|
|
40
|
+
// Required only when present on disk (not every feature reaches every phase).
|
|
41
|
+
const FEATURE_OPTIONAL = ["context.md", "design.md", "tasks.md", "validation.md"];
|
|
42
|
+
|
|
43
|
+
const STATE_FILES = [
|
|
44
|
+
join(".specs", "project", "STATE.md"),
|
|
45
|
+
join(".specs", "HANDOFF.md"),
|
|
46
|
+
join(".specs", "project", "FEATURES.json"),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Mirrors Python's str.splitlines(): universal newline split, no trailing empty element. */
|
|
50
|
+
function splitLines(text: string): string[] {
|
|
51
|
+
if (text === "") return [];
|
|
52
|
+
const result: string[] = [];
|
|
53
|
+
const lineBreakRe = /\r\n|\r|\n/g;
|
|
54
|
+
let start = 0;
|
|
55
|
+
let match: RegExpExecArray | null;
|
|
56
|
+
while ((match = lineBreakRe.exec(text)) !== null) {
|
|
57
|
+
result.push(text.slice(start, match.index));
|
|
58
|
+
start = match.index + match[0].length;
|
|
59
|
+
}
|
|
60
|
+
if (start < text.length) {
|
|
61
|
+
result.push(text.slice(start));
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runGit(args: string[], root: string): string {
|
|
67
|
+
let proc: ReturnType<typeof Bun.spawnSync>;
|
|
68
|
+
try {
|
|
69
|
+
proc = Bun.spawnSync(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
|
|
70
|
+
} catch {
|
|
71
|
+
console.error("check_specs_delivered: git not found on PATH");
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
const exitCode = proc.exitCode ?? -1;
|
|
75
|
+
if (exitCode !== 0) {
|
|
76
|
+
const stderrText = proc.stderr.toString().trim();
|
|
77
|
+
console.error(`check_specs_delivered: git ${args.join(" ")} failed: ${stderrText}`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
return proc.stdout.toString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Lines from `git status --porcelain -- .specs/` (empty = clean). */
|
|
84
|
+
function porcelainDirtyPaths(root: string): string[] {
|
|
85
|
+
const out = runGit(["status", "--porcelain", "--", ".specs/"], root);
|
|
86
|
+
return splitLines(out).filter((ln) => ln.trim() !== "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Set of every path tracked on HEAD, repo-root-relative, '/'-separated. */
|
|
90
|
+
function trackedOnHead(root: string): Set<string> {
|
|
91
|
+
const out = runGit(["ls-tree", "-r", "--name-only", "HEAD"], root);
|
|
92
|
+
return new Set(splitLines(out));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveFeatureDir(root: string, feature: string): string {
|
|
96
|
+
if (isAbsolute(feature) || feature.includes(sep)) {
|
|
97
|
+
return resolve(feature);
|
|
98
|
+
}
|
|
99
|
+
return join(root, ".specs", "features", feature);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Repo-root-relative, '/'-separated paths this feature must have tracked. */
|
|
103
|
+
function requiredPaths(root: string, feature: string): string[] {
|
|
104
|
+
const fdir = resolveFeatureDir(root, feature);
|
|
105
|
+
const fdirRel = relative(root, fdir);
|
|
106
|
+
const paths = FEATURE_REQUIRED.map((name) => join(fdirRel, name));
|
|
107
|
+
if (existsSync(fdir) && statSync(fdir).isDirectory()) {
|
|
108
|
+
for (const name of FEATURE_OPTIONAL) {
|
|
109
|
+
const candidate = join(fdir, name);
|
|
110
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
111
|
+
paths.push(join(fdirRel, name));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
paths.push(...STATE_FILES);
|
|
116
|
+
return paths.map((p) => p.split(sep).join("/"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Return { errors, checked }. errors empty = pass. */
|
|
120
|
+
function check(root: string, feature: string): { errors: string[]; checked: string[] } {
|
|
121
|
+
const errors: string[] = [];
|
|
122
|
+
|
|
123
|
+
for (const ln of porcelainDirtyPaths(root)) {
|
|
124
|
+
errors.push(`uncommitted/untracked under .specs/: ${ln.trim()}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const paths = requiredPaths(root, feature);
|
|
128
|
+
const tracked = trackedOnHead(root);
|
|
129
|
+
for (const p of paths) {
|
|
130
|
+
if (!tracked.has(p)) {
|
|
131
|
+
errors.push(`not tracked on HEAD: ${p}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { errors, checked: paths };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const USAGE = "usage: check_specs_delivered.ts [-h] [--root ROOT] feature";
|
|
139
|
+
const HELP = `${USAGE}
|
|
140
|
+
|
|
141
|
+
Gate: .specs/ artifacts committed on the branch before PR (GATE-02).
|
|
142
|
+
|
|
143
|
+
positional arguments:
|
|
144
|
+
feature Feature slug under <root>/.specs/features/, or a direct path
|
|
145
|
+
|
|
146
|
+
options:
|
|
147
|
+
-h, --help show this help message and exit
|
|
148
|
+
--root ROOT Project root containing .specs/ (default: current dir)`;
|
|
149
|
+
|
|
150
|
+
interface Args {
|
|
151
|
+
feature: string;
|
|
152
|
+
root: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printUsageError(msg: string): void {
|
|
156
|
+
process.stderr.write(`${USAGE}\ncheck_specs_delivered.ts: error: ${msg}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseArgs(argv: string[]): Args | null {
|
|
160
|
+
let root = ".";
|
|
161
|
+
const positionals: string[] = [];
|
|
162
|
+
for (let i = 0; i < argv.length; i++) {
|
|
163
|
+
const a = argv[i]!;
|
|
164
|
+
if (a === "--root") {
|
|
165
|
+
if (i + 1 >= argv.length) {
|
|
166
|
+
printUsageError("argument --root: expected one argument");
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
root = argv[++i]!;
|
|
170
|
+
} else if (a.startsWith("--root=")) {
|
|
171
|
+
root = a.slice("--root=".length);
|
|
172
|
+
} else if (a === "-h" || a === "--help") {
|
|
173
|
+
console.log(HELP);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
176
|
+
printUsageError(`unrecognized arguments: ${a}`);
|
|
177
|
+
return null;
|
|
178
|
+
} else {
|
|
179
|
+
positionals.push(a);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (positionals.length === 0) {
|
|
183
|
+
printUsageError("the following arguments are required: feature");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (positionals.length > 1) {
|
|
187
|
+
printUsageError(`unrecognized arguments: ${positionals.slice(1).join(" ")}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return { feature: positionals[0]!, root };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function main(argv: string[]): number {
|
|
194
|
+
const args = parseArgs(argv);
|
|
195
|
+
if (args === null) return 2;
|
|
196
|
+
const root = resolve(args.root);
|
|
197
|
+
|
|
198
|
+
const { errors, checked } = check(root, args.feature);
|
|
199
|
+
|
|
200
|
+
for (const e of errors) console.log(` ERROR ${e}`);
|
|
201
|
+
console.log(`\ncheck_specs_delivered: checked ${checked.length} path(s):`);
|
|
202
|
+
for (const c of checked) console.log(` - ${c}`);
|
|
203
|
+
console.log(`check_specs_delivered: ${errors.length} error(s)`);
|
|
204
|
+
return errors.length ? 1 : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (import.meta.main) {
|
|
208
|
+
process.exit(main(process.argv.slice(2)));
|
|
209
|
+
}
|