@whitehatd/crag 0.0.1 → 0.2.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/README.md +864 -15
- package/bin/crag.js +7 -0
- package/package.json +18 -4
- package/src/cli.js +102 -0
- package/src/commands/analyze.js +513 -0
- package/src/commands/check.js +55 -0
- package/src/commands/compile.js +104 -0
- package/src/commands/diff.js +289 -0
- package/src/commands/init.js +112 -0
- package/src/commands/upgrade.js +64 -0
- package/src/commands/workspace.js +94 -0
- package/src/compile/agents-md.js +58 -0
- package/src/compile/atomic-write.js +32 -0
- package/src/compile/cline.js +83 -0
- package/src/compile/cody.js +82 -0
- package/src/compile/continue.js +78 -0
- package/src/compile/copilot.js +70 -0
- package/src/compile/cursor-rules.js +66 -0
- package/src/compile/gemini-md.js +58 -0
- package/src/compile/github-actions.js +165 -0
- package/src/compile/husky.js +66 -0
- package/src/compile/pre-commit.js +50 -0
- package/src/compile/windsurf.js +76 -0
- package/src/compile/zed.js +86 -0
- package/src/crag-agent.md +254 -0
- package/src/governance/gate-to-shell.js +28 -0
- package/src/governance/parse.js +182 -0
- package/src/skills/post-start-validation.md +297 -0
- package/src/skills/pre-start-context.md +506 -0
- package/src/update/integrity.js +131 -0
- package/src/update/skill-sync.js +116 -0
- package/src/update/version-check.js +156 -0
- package/src/workspace/detect.js +190 -0
- package/src/workspace/enumerate.js +270 -0
- package/src/workspace/governance.js +119 -0
- package/cli.js +0 -15
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crag-project
|
|
3
|
+
description: Generate Claude Code infrastructure from an interactive interview. Installs universal skills, generates project-specific governance, hooks, and agents.
|
|
4
|
+
tools:
|
|
5
|
+
- Bash
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Grep
|
|
9
|
+
- Glob
|
|
10
|
+
- Edit
|
|
11
|
+
model: opus
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Crag — Project Generator
|
|
15
|
+
|
|
16
|
+
**START IMMEDIATELY.** When the user sends any message (even "go", "start", or empty), begin the interview. Do not wait for instructions. Do not explain what you are — just start asking questions.
|
|
17
|
+
|
|
18
|
+
You are crag's interview agent. Interview the user about their project, then generate the infrastructure layer — governance rules, hooks, agents, and settings. The universal skills (pre-start, post-start) are already installed by the CLI.
|
|
19
|
+
|
|
20
|
+
## What You Generate vs What Ships
|
|
21
|
+
|
|
22
|
+
**Ships with crag (universal, same for everyone):**
|
|
23
|
+
- `pre-start-context` skill — discovers any project at runtime
|
|
24
|
+
- `post-start-validation` skill — validates any project using governance gates
|
|
25
|
+
|
|
26
|
+
**You generate (project-specific, from interview):**
|
|
27
|
+
- `governance.md` — the user's rules, quality bar, policies
|
|
28
|
+
- Hook scripts — configured for their tools
|
|
29
|
+
- Agent definitions — configured for their test/build commands
|
|
30
|
+
- Settings — permissions + hook wiring
|
|
31
|
+
- CI playbook template — based on their CI/CD
|
|
32
|
+
- MemStack rules — if enabled
|
|
33
|
+
- Session name — if remote access enabled
|
|
34
|
+
|
|
35
|
+
## Phase 1: Interview
|
|
36
|
+
|
|
37
|
+
Ask ONE question at a time. Wait for the answer. Adapt follow-ups. Skip obvious ones.
|
|
38
|
+
|
|
39
|
+
### 1.1 Project Identity
|
|
40
|
+
- Project name? (used for MemStack, session-name, commit references)
|
|
41
|
+
- One-line description?
|
|
42
|
+
|
|
43
|
+
### 1.2 Tech Stack
|
|
44
|
+
- Backend: language + framework? (Java/Spring, Node/Express, Python/FastAPI, Go, Rust, etc.)
|
|
45
|
+
- Frontend: framework? (Next.js, React+Vite, Vue, Svelte, none)
|
|
46
|
+
- Database? (PostgreSQL, MySQL, MongoDB, SQLite, Redis, etc.)
|
|
47
|
+
- Build system? (Gradle, Maven, npm, pnpm, cargo, pip, etc.)
|
|
48
|
+
|
|
49
|
+
### 1.3 Architecture
|
|
50
|
+
- Monolith or microservices? If micro: how many, names?
|
|
51
|
+
- Monorepo or multi-repo?
|
|
52
|
+
|
|
53
|
+
### 1.4 Deployment
|
|
54
|
+
- How deployed? (Docker Compose, Kubernetes, Vercel, Fly.io, bare metal)
|
|
55
|
+
- CI/CD? (GitHub Actions, GitLab CI, none)
|
|
56
|
+
- Deploy strategy? (blue-green, rolling, recreate)
|
|
57
|
+
|
|
58
|
+
### 1.5 Quality Bar
|
|
59
|
+
- Testing? (framework + philosophy: TDD, integration, e2e, minimal)
|
|
60
|
+
- Linting? (Biome, ESLint, Checkstyle, Clippy, ruff)
|
|
61
|
+
- Type checking? (TypeScript strict, mypy, Java types)
|
|
62
|
+
- Formatter? (Biome, Prettier, google-java-format, rustfmt, ruff)
|
|
63
|
+
|
|
64
|
+
### 1.6 Security
|
|
65
|
+
- Auth? (JWT, OAuth, session, API key, none)
|
|
66
|
+
- Rate limiting? (yes/no, what tool)
|
|
67
|
+
- File uploads? (yes/no, scanning?)
|
|
68
|
+
- Security headers? (CSP, HSTS)
|
|
69
|
+
|
|
70
|
+
### 1.7 Workflow
|
|
71
|
+
- Branch strategy? (feature branches, trunk-based)
|
|
72
|
+
- Commit convention? (conventional, free-form)
|
|
73
|
+
- Git auth? (GITHUB_TOKEN, gh CLI, SSH key)
|
|
74
|
+
- Autonomy: auto-commit after gates, or ask first?
|
|
75
|
+
|
|
76
|
+
### 1.8 Session Management
|
|
77
|
+
- Remote phone access? (yes/no)
|
|
78
|
+
- If yes: tmux session name?
|
|
79
|
+
- MemStack for cross-session memory? (yes/no)
|
|
80
|
+
|
|
81
|
+
## Phase 2: Generate
|
|
82
|
+
|
|
83
|
+
### 2.1 Universal Skills — DO NOT REGENERATE
|
|
84
|
+
|
|
85
|
+
The CLI (`crag init`) already installed the universal skills before launching you. They exist at:
|
|
86
|
+
- `.claude/skills/pre-start-context/SKILL.md`
|
|
87
|
+
- `.claude/skills/post-start-validation/SKILL.md`
|
|
88
|
+
- `.agents/workflows/pre-start-context.md`
|
|
89
|
+
- `.agents/workflows/post-start-validation.md`
|
|
90
|
+
|
|
91
|
+
**Do NOT overwrite them.** Check that they exist with `ls`. If missing (user ran you directly, not via CLI), create the directories and tell the user to run `crag init` to install them properly.
|
|
92
|
+
|
|
93
|
+
### 2.2 Generate governance.md
|
|
94
|
+
|
|
95
|
+
Write `.claude/governance.md` from the interview answers:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
# Governance — [project name]
|
|
99
|
+
# This file defines YOUR rules. The universal skills read it and adapt.
|
|
100
|
+
# Change this when your standards change. The skills never go stale.
|
|
101
|
+
|
|
102
|
+
## Identity
|
|
103
|
+
- Project: [name]
|
|
104
|
+
- Description: [one-liner]
|
|
105
|
+
|
|
106
|
+
## Gates (run in order, stop on failure)
|
|
107
|
+
### Frontend
|
|
108
|
+
- [lint command]
|
|
109
|
+
- [type check command]
|
|
110
|
+
- [build command]
|
|
111
|
+
- [test command]
|
|
112
|
+
|
|
113
|
+
### Backend
|
|
114
|
+
- [test command with flags]
|
|
115
|
+
- [compile/check command]
|
|
116
|
+
|
|
117
|
+
## Branch Strategy
|
|
118
|
+
- [feature branches / trunk-based]
|
|
119
|
+
- [conventional commits / free-form]
|
|
120
|
+
- Commit trailer: Co-Authored-By: Claude <noreply@anthropic.com>
|
|
121
|
+
|
|
122
|
+
## Security Requirements
|
|
123
|
+
- Auth: [JWT / OAuth / API key / none]
|
|
124
|
+
- [rate limiting rules if any]
|
|
125
|
+
- [file upload rules if any]
|
|
126
|
+
- [security header rules if any]
|
|
127
|
+
- No hardcoded secrets — grep for sk_live, AKIA, password= before commit
|
|
128
|
+
|
|
129
|
+
## Autonomy
|
|
130
|
+
- [auto-commit after gates pass / ask before commit / ask before everything]
|
|
131
|
+
|
|
132
|
+
## Deployment
|
|
133
|
+
- Target: [Docker Compose / K8s / Vercel / etc.]
|
|
134
|
+
- CI: [GitHub Actions / GitLab / none]
|
|
135
|
+
- Strategy: [blue-green / rolling / recreate]
|
|
136
|
+
- [Verification command if applicable: health check URL, kubectl status, etc.]
|
|
137
|
+
|
|
138
|
+
## Conventions
|
|
139
|
+
- [Any project-specific rules from the interview]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2.3 Generate Hooks
|
|
143
|
+
|
|
144
|
+
Create `.claude/hooks/`:
|
|
145
|
+
|
|
146
|
+
**drift-detector.sh** — always generate. Check for existence of key files based on detected stack:
|
|
147
|
+
- If Gradle: check build.gradle.kts, settings.gradle.kts
|
|
148
|
+
- If npm: check package.json, tsconfig.json
|
|
149
|
+
- If Python: check pyproject.toml, requirements.txt
|
|
150
|
+
- If Go: check go.mod
|
|
151
|
+
- Check CI workflow files
|
|
152
|
+
- Check Docker/K8s configs
|
|
153
|
+
|
|
154
|
+
**circuit-breaker.sh** — always generate. Same for all projects.
|
|
155
|
+
|
|
156
|
+
**auto-post-start.sh** — always generate. Gate enforcement safety net. Reads tool input from stdin, checks if the command is a `git commit`, warns if `.claude/.gates-passed` sentinel doesn't exist. Non-blocking (warns, doesn't prevent). Same for all projects.
|
|
157
|
+
|
|
158
|
+
**sandbox-guard.sh** — always generate. Security hardening. Reads tool input from stdin (PreToolUse on Bash), hard-blocks destructive system commands (rm -rf /, dd, mkfs, DROP TABLE, docker system prune, kubectl delete namespace, curl|bash, force-push to main). Warns on file operations targeting paths outside the project root. Same for all projects.
|
|
159
|
+
|
|
160
|
+
**pre-compact-snapshot.sh** — only if MemStack enabled. Use correct project name.
|
|
161
|
+
|
|
162
|
+
**post-compact-recovery.sh** — only if MemStack enabled. Use correct project name.
|
|
163
|
+
|
|
164
|
+
### 2.4 Generate Settings
|
|
165
|
+
|
|
166
|
+
Write `.claude/settings.local.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"permissions": {
|
|
171
|
+
"allow": [
|
|
172
|
+
// RTK wildcards for detected tools
|
|
173
|
+
"Bash(rtk ./gradlew:*)", // if Gradle
|
|
174
|
+
"Bash(rtk npm:*)", // if npm
|
|
175
|
+
"Bash(rtk cargo:*)", // if Rust
|
|
176
|
+
"Bash(rtk pytest:*)", // if Python
|
|
177
|
+
"Bash(rtk git:*)",
|
|
178
|
+
"Bash(rtk gh:*)",
|
|
179
|
+
"Bash(rtk docker:*)",
|
|
180
|
+
"Bash(rtk curl:*)"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"hooks": {
|
|
184
|
+
// Wire the generated hook scripts
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Include PostToolUse auto-format hook using the project's formatter (Biome, Prettier, rustfmt, etc.).
|
|
190
|
+
|
|
191
|
+
Wire the security and gate hooks as PreToolUse for Bash (sandbox-guard runs FIRST):
|
|
192
|
+
```json
|
|
193
|
+
"PreToolUse": [
|
|
194
|
+
{
|
|
195
|
+
"matcher": "Bash",
|
|
196
|
+
"hooks": [
|
|
197
|
+
{
|
|
198
|
+
"type": "command",
|
|
199
|
+
"command": "bash .claude/hooks/sandbox-guard.sh"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"type": "command",
|
|
203
|
+
"command": "bash .claude/hooks/auto-post-start.sh"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 2.5 Generate Agents
|
|
211
|
+
|
|
212
|
+
Create `.claude/agents/`:
|
|
213
|
+
|
|
214
|
+
**test-runner.md** — always. Use the project's test commands from governance.md.
|
|
215
|
+
**security-reviewer.md** — always. Reference the project's security stack.
|
|
216
|
+
**dependency-scanner.md** — if package manager exists.
|
|
217
|
+
**skill-auditor.md** — always.
|
|
218
|
+
|
|
219
|
+
All agents get `isolation: worktree`.
|
|
220
|
+
|
|
221
|
+
### 2.6 Generate CI Playbook
|
|
222
|
+
|
|
223
|
+
Write `.claude/ci-playbook.md` with empty sections for the project's CI system. Include the correct format template.
|
|
224
|
+
|
|
225
|
+
### 2.7 MemStack Rules (if enabled)
|
|
226
|
+
|
|
227
|
+
Generate `.claude/rules/knowledge.md`, `diary.md`, `echo.md` with the correct project name and Python path.
|
|
228
|
+
|
|
229
|
+
### 2.8 Session Name (if remote access enabled)
|
|
230
|
+
|
|
231
|
+
Write `.claude/.session-name` with the tmux session name.
|
|
232
|
+
|
|
233
|
+
## Phase 3: Summary
|
|
234
|
+
|
|
235
|
+
Present:
|
|
236
|
+
- Files created (list)
|
|
237
|
+
- Governance rules (count)
|
|
238
|
+
- Hooks configured (list)
|
|
239
|
+
- Agents defined (list)
|
|
240
|
+
- Next step: "Run /pre-start-context to verify everything works"
|
|
241
|
+
- Mention: "Run `crag compile --target all` to generate CI workflows and git hooks from your governance"
|
|
242
|
+
|
|
243
|
+
## Rules
|
|
244
|
+
|
|
245
|
+
1. ASK before generating. Never assume.
|
|
246
|
+
2. The governance.md is the ONLY project-specific configuration. Everything else either ships universal or is derived from governance.
|
|
247
|
+
3. Use the project's actual tool names.
|
|
248
|
+
4. Generate for Windows (Git Bash syntax) unless told otherwise.
|
|
249
|
+
5. If user references another project they've scaffolded — read that governance file for reference.
|
|
250
|
+
6. **Check pwd before navigating.** Don't cd into a directory you're already in.
|
|
251
|
+
7. **Don't regenerate universal skills.** The CLI installs them. Check if they exist, don't overwrite.
|
|
252
|
+
8. **MemStack:** Ask the user if they have a shared MemStack DB installed and where. If yes, wire the generated rules to that path. Otherwise generate local SQLite rules or skip MemStack entirely.
|
|
253
|
+
9. **Sandbox boundaries.** Never run destructive commands (rm -rf /, dd, mkfs, DROP TABLE, docker system prune -a, kubectl delete namespace). Only write files within .claude/ and the project directory. Never modify system files or global config. Generated hooks and agents must inherit these boundaries.
|
|
254
|
+
10. **Subagent isolation.** All generated agent definitions must include a `## Boundaries` section stating: operate only within this repository, no destructive system commands, no network access beyond task requirements, no permission escalation.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape a string for safe inclusion inside double quotes in a shell command.
|
|
5
|
+
* Escapes: backslash, backtick, dollar sign, double quote.
|
|
6
|
+
*/
|
|
7
|
+
function shellEscapeDoubleQuoted(s) {
|
|
8
|
+
return String(s).replace(/[\\`"$]/g, '\\$&');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert human-readable gate descriptions to shell commands.
|
|
13
|
+
* e.g. Verify src/skills/pre-start-context.md contains "discovers any project"
|
|
14
|
+
* → grep -qi "discovers any project" "src/skills/pre-start-context.md"
|
|
15
|
+
*
|
|
16
|
+
* All interpolated values are shell-escaped to prevent command injection.
|
|
17
|
+
*/
|
|
18
|
+
function gateToShell(cmd) {
|
|
19
|
+
const verify = cmd.match(/^Verify\s+(\S+)\s+contains\s+["']([^"']+)["']$/i);
|
|
20
|
+
if (verify) {
|
|
21
|
+
const needle = shellEscapeDoubleQuoted(verify[2]);
|
|
22
|
+
const file = shellEscapeDoubleQuoted(verify[1]);
|
|
23
|
+
return `grep -qi "${needle}" "${file}"`;
|
|
24
|
+
}
|
|
25
|
+
return cmd;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { gateToShell, shellEscapeDoubleQuoted };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Governance parser v2 — backward-compatible with v1.
|
|
5
|
+
*
|
|
6
|
+
* New optional annotations:
|
|
7
|
+
* ### Section (path: dir/) — path-scoped gates
|
|
8
|
+
* ### Section (if: file) — conditional section
|
|
9
|
+
* - command # [OPTIONAL]
|
|
10
|
+
* ## Gates (inherit: root) — inheritance marker
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Defensive cap: governance files should be well under this size.
|
|
14
|
+
// Protects against ReDoS on catastrophic-backtracking-prone regex.
|
|
15
|
+
const MAX_CONTENT_SIZE = 256 * 1024; // 256 KB
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract a markdown section body by heading name.
|
|
19
|
+
* Starts after the first line matching `## <name>` (with optional trailing text),
|
|
20
|
+
* ends at the next `## ` heading or EOF. Returns the body string, or null if not found.
|
|
21
|
+
*
|
|
22
|
+
* Implemented via line-by-line scan to avoid regex backtracking on large inputs.
|
|
23
|
+
*/
|
|
24
|
+
function extractSection(content, name) {
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
const headingPrefix = `## ${name}`;
|
|
27
|
+
let start = -1;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i];
|
|
31
|
+
if (line === headingPrefix || line.startsWith(headingPrefix + ' ') || line.startsWith(headingPrefix + '\t')) {
|
|
32
|
+
start = i + 1;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (start === -1) return null;
|
|
38
|
+
|
|
39
|
+
let end = lines.length;
|
|
40
|
+
for (let i = start; i < lines.length; i++) {
|
|
41
|
+
// Next top-level section (## but not ###)
|
|
42
|
+
if (/^## [^#]/.test(lines[i])) {
|
|
43
|
+
end = i;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return lines.slice(start, end).join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseGovernance(content) {
|
|
52
|
+
const result = {
|
|
53
|
+
name: '',
|
|
54
|
+
description: '',
|
|
55
|
+
gates: {},
|
|
56
|
+
runtimes: [],
|
|
57
|
+
inherit: null,
|
|
58
|
+
warnings: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (typeof content !== 'string') {
|
|
62
|
+
result.warnings.push('Invalid content type (expected string)');
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
if (content.length > MAX_CONTENT_SIZE) {
|
|
66
|
+
result.warnings.push(`governance.md exceeds ${MAX_CONTENT_SIZE} bytes — truncating`);
|
|
67
|
+
content = content.slice(0, MAX_CONTENT_SIZE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nameMatch = content.match(/- Project:\s*(.+)/);
|
|
71
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
72
|
+
|
|
73
|
+
const descMatch = content.match(/- Description:\s*(.+)/);
|
|
74
|
+
if (descMatch) result.description = descMatch[1].trim();
|
|
75
|
+
|
|
76
|
+
// Check for inheritance marker: ## Gates (inherit: root)
|
|
77
|
+
const inheritMatch = content.match(/## Gates[^\n]*\(inherit:\s*(\w+)\)/);
|
|
78
|
+
if (inheritMatch) result.inherit = inheritMatch[1].trim();
|
|
79
|
+
|
|
80
|
+
// Extract the Gates section (ends at next ## heading or EOF).
|
|
81
|
+
// Splitting manually avoids potential catastrophic backtracking on large inputs.
|
|
82
|
+
const gatesBody = extractSection(content, 'Gates');
|
|
83
|
+
if (gatesBody) {
|
|
84
|
+
let section = 'default';
|
|
85
|
+
let sectionMeta = { path: null, condition: null };
|
|
86
|
+
|
|
87
|
+
for (const line of gatesBody.split('\n')) {
|
|
88
|
+
// Match ### Section or ### Section (path: dir/) or ### Section (if: file)
|
|
89
|
+
const sub = line.match(/^### (.+?)(?:\s*\((?:(path|if):\s*(.+?))\))?\s*$/);
|
|
90
|
+
if (sub) {
|
|
91
|
+
section = sub[1].trim().toLowerCase();
|
|
92
|
+
sectionMeta = { path: null, condition: null };
|
|
93
|
+
if (sub[2] === 'path') sectionMeta.path = sub[3].trim();
|
|
94
|
+
if (sub[2] === 'if') sectionMeta.condition = sub[3].trim();
|
|
95
|
+
result.gates[section] = {
|
|
96
|
+
commands: [],
|
|
97
|
+
path: sectionMeta.path,
|
|
98
|
+
condition: sectionMeta.condition,
|
|
99
|
+
};
|
|
100
|
+
} else if (line.match(/^\s*- [^[\s]/) && line.trim() !== '-') {
|
|
101
|
+
let cmd = line.replace(/^\s*- /, '').trim();
|
|
102
|
+
let classification = 'MANDATORY';
|
|
103
|
+
|
|
104
|
+
// Check for # [OPTIONAL] or # [MANDATORY] suffix
|
|
105
|
+
const classMatch = cmd.match(/\s*#\s*\[(MANDATORY|OPTIONAL|ADVISORY)\]\s*$/);
|
|
106
|
+
if (classMatch) {
|
|
107
|
+
classification = classMatch[1];
|
|
108
|
+
cmd = cmd.replace(/\s*#\s*\[(?:MANDATORY|OPTIONAL|ADVISORY)\]\s*$/, '').trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cmd) {
|
|
112
|
+
if (!result.gates[section]) {
|
|
113
|
+
result.gates[section] = { commands: [], path: null, condition: null };
|
|
114
|
+
}
|
|
115
|
+
result.gates[section].commands.push({ cmd, classification });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Warn if no gates were found (helps users catch structural mistakes)
|
|
122
|
+
if (Object.keys(result.gates).length === 0) {
|
|
123
|
+
result.warnings.push('No gates found in governance.md. Expected: "## Gates" section with "- command" entries.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Detect runtimes from gate commands
|
|
127
|
+
const allCmds = Object.values(result.gates)
|
|
128
|
+
.flatMap(g => (g.commands || []).map(c => c.cmd))
|
|
129
|
+
.join(' ');
|
|
130
|
+
if (/\b(node|npm|npx|eslint|prettier|biome|vitest|jest|next)\b/.test(allCmds)) result.runtimes.push('node');
|
|
131
|
+
if (/\b(cargo|rustc|clippy|rustfmt)\b/.test(allCmds)) result.runtimes.push('rust');
|
|
132
|
+
if (/\b(python|pip|pytest|ruff|mypy|django)\b/.test(allCmds)) result.runtimes.push('python');
|
|
133
|
+
if (/\b(java|gradle|gradlew|maven|mvn)\b/.test(allCmds)) result.runtimes.push('java');
|
|
134
|
+
if (/\bgo (build|test|vet|lint)\b/.test(allCmds)) result.runtimes.push('go');
|
|
135
|
+
if (/\bdocker\b/.test(allCmds)) result.runtimes.push('docker');
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Flatten v2 gates to v1 format for backward compat with compile targets.
|
|
142
|
+
* Returns { sectionName: ['cmd1', 'cmd2'] } — classification and metadata are lost.
|
|
143
|
+
* Use flattenGatesRich() when you need annotations.
|
|
144
|
+
*/
|
|
145
|
+
function flattenGates(gates) {
|
|
146
|
+
const flat = {};
|
|
147
|
+
if (!gates || typeof gates !== 'object') return flat;
|
|
148
|
+
for (const [section, data] of Object.entries(gates)) {
|
|
149
|
+
if (!data || typeof data !== 'object') continue;
|
|
150
|
+
const cmds = Array.isArray(data.commands) ? data.commands : [];
|
|
151
|
+
flat[section] = cmds
|
|
152
|
+
.filter(c => c && typeof c.cmd === 'string' && c.cmd.trim())
|
|
153
|
+
.map(c => c.cmd);
|
|
154
|
+
}
|
|
155
|
+
return flat;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Flatten v2 gates preserving metadata.
|
|
160
|
+
* Returns an array of { section, cmd, classification, path, condition } in order.
|
|
161
|
+
*/
|
|
162
|
+
function flattenGatesRich(gates) {
|
|
163
|
+
const out = [];
|
|
164
|
+
if (!gates || typeof gates !== 'object') return out;
|
|
165
|
+
for (const [section, data] of Object.entries(gates)) {
|
|
166
|
+
if (!data || typeof data !== 'object') continue;
|
|
167
|
+
const cmds = Array.isArray(data.commands) ? data.commands : [];
|
|
168
|
+
for (const c of cmds) {
|
|
169
|
+
if (!c || typeof c.cmd !== 'string' || !c.cmd.trim()) continue;
|
|
170
|
+
out.push({
|
|
171
|
+
section,
|
|
172
|
+
cmd: c.cmd,
|
|
173
|
+
classification: c.classification || 'MANDATORY',
|
|
174
|
+
path: data.path || null,
|
|
175
|
+
condition: data.condition || null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { parseGovernance, flattenGates, flattenGatesRich, extractSection };
|