@whitehatd/crag 0.2.2 → 0.2.4
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 +2 -1
- package/package.json +1 -1
- package/src/analyze/ci-extractors.js +317 -0
- package/src/analyze/doc-mining.js +142 -0
- package/src/analyze/gates.js +417 -0
- package/src/analyze/normalize.js +146 -0
- package/src/analyze/stacks.js +453 -0
- package/src/analyze/task-runners.js +146 -0
- package/src/cli-errors.js +55 -0
- package/src/cli.js +10 -2
- package/src/commands/analyze.js +185 -271
- package/src/commands/check.js +67 -34
- package/src/commands/compile.js +69 -22
- package/src/commands/diff.js +10 -43
- package/src/commands/init.js +55 -31
- package/src/compile/atomic-write.js +12 -4
- package/src/compile/github-actions.js +17 -11
- package/src/compile/husky.js +6 -5
- package/src/compile/pre-commit.js +13 -5
- package/src/governance/gate-to-shell.js +11 -1
- package/src/governance/parse.js +41 -3
- package/src/governance/yaml-run.js +145 -0
- package/src/skills/post-start-validation.md +1 -1
- package/src/skills/pre-start-context.md +1 -1
- package/src/update/integrity.js +20 -7
- package/src/update/skill-sync.js +1 -1
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Escape a string for safe inclusion inside double quotes in a shell command.
|
|
5
5
|
* Escapes: backslash, backtick, dollar sign, double quote.
|
|
6
|
+
* Backslash MUST be replaced first so its replacement isn't re-escaped.
|
|
6
7
|
*/
|
|
7
8
|
function shellEscapeDoubleQuoted(s) {
|
|
8
9
|
return String(s).replace(/[\\`"$]/g, '\\$&');
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Escape a string for safe inclusion inside single quotes in a shell command.
|
|
14
|
+
* Single quotes cannot be escaped inside single quotes — the standard pattern
|
|
15
|
+
* is to close the quote, emit an escaped quote, and reopen: 'foo'\''bar'.
|
|
16
|
+
*/
|
|
17
|
+
function shellEscapeSingleQuoted(s) {
|
|
18
|
+
return String(s).replace(/'/g, "'\\''");
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Convert human-readable gate descriptions to shell commands.
|
|
13
23
|
* e.g. Verify src/skills/pre-start-context.md contains "discovers any project"
|
|
@@ -25,4 +35,4 @@ function gateToShell(cmd) {
|
|
|
25
35
|
return cmd;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
module.exports = { gateToShell, shellEscapeDoubleQuoted };
|
|
38
|
+
module.exports = { gateToShell, shellEscapeDoubleQuoted, shellEscapeSingleQuoted };
|
package/src/governance/parse.js
CHANGED
|
@@ -14,6 +14,30 @@
|
|
|
14
14
|
// Protects against ReDoS on catastrophic-backtracking-prone regex.
|
|
15
15
|
const MAX_CONTENT_SIZE = 256 * 1024; // 256 KB
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Validate an annotation path (used for `path:` and `if:` on gate sections).
|
|
19
|
+
*
|
|
20
|
+
* Rejects:
|
|
21
|
+
* - Absolute paths (/, C:\, \\server\share)
|
|
22
|
+
* - Parent traversal (..)
|
|
23
|
+
* - Newlines or null bytes (defense against injection into generated YAML/shell)
|
|
24
|
+
*
|
|
25
|
+
* These paths are interpolated into shell commands and YAML scalars downstream
|
|
26
|
+
* (husky, pre-commit, github-actions), so the parser is the single chokepoint
|
|
27
|
+
* where untrusted path strings from governance.md become structured data.
|
|
28
|
+
*/
|
|
29
|
+
function isValidAnnotationPath(p) {
|
|
30
|
+
if (typeof p !== 'string' || p.length === 0) return false;
|
|
31
|
+
if (p.length > 512) return false;
|
|
32
|
+
if (/[\n\r\x00]/.test(p)) return false;
|
|
33
|
+
// POSIX absolute or Windows drive-letter / UNC
|
|
34
|
+
if (p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('\\\\')) return false;
|
|
35
|
+
// Parent traversal (match as a path segment, not as substring of a name)
|
|
36
|
+
const segments = p.split(/[\\/]/);
|
|
37
|
+
if (segments.includes('..')) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
/**
|
|
18
42
|
* Extract a markdown section body by heading name.
|
|
19
43
|
* Starts after the first line matching `## <name>` (with optional trailing text),
|
|
@@ -90,8 +114,22 @@ function parseGovernance(content) {
|
|
|
90
114
|
if (sub) {
|
|
91
115
|
section = sub[1].trim().toLowerCase();
|
|
92
116
|
sectionMeta = { path: null, condition: null };
|
|
93
|
-
if (sub[2] === 'path')
|
|
94
|
-
|
|
117
|
+
if (sub[2] === 'path') {
|
|
118
|
+
const raw = sub[3].trim();
|
|
119
|
+
if (isValidAnnotationPath(raw)) {
|
|
120
|
+
sectionMeta.path = raw;
|
|
121
|
+
} else {
|
|
122
|
+
result.warnings.push(`Invalid path annotation in section "${sub[1].trim()}": ${JSON.stringify(raw)} (must be a relative in-repo path without "..")`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (sub[2] === 'if') {
|
|
126
|
+
const raw = sub[3].trim();
|
|
127
|
+
if (isValidAnnotationPath(raw)) {
|
|
128
|
+
sectionMeta.condition = raw;
|
|
129
|
+
} else {
|
|
130
|
+
result.warnings.push(`Invalid if annotation in section "${sub[1].trim()}": ${JSON.stringify(raw)} (must be a relative in-repo path without "..")`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
95
133
|
result.gates[section] = {
|
|
96
134
|
commands: [],
|
|
97
135
|
path: sectionMeta.path,
|
|
@@ -179,4 +217,4 @@ function flattenGatesRich(gates) {
|
|
|
179
217
|
return out;
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
module.exports = { parseGovernance, flattenGates, flattenGatesRich, extractSection };
|
|
220
|
+
module.exports = { parseGovernance, flattenGates, flattenGatesRich, extractSection, isValidAnnotationPath };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared YAML `run:` command extraction for GitHub Actions workflows.
|
|
5
|
+
*
|
|
6
|
+
* Both `crag analyze` and `crag diff` need to enumerate the shell commands
|
|
7
|
+
* inside CI workflows to either generate gates from them or compare them
|
|
8
|
+
* against governance. This module is the single source of truth so a fix to
|
|
9
|
+
* the parser benefits both commands.
|
|
10
|
+
*
|
|
11
|
+
* Handles:
|
|
12
|
+
* run: npm test (inline)
|
|
13
|
+
* run: "npm test" (inline, quoted)
|
|
14
|
+
* run: | (literal block scalar)
|
|
15
|
+
* npm test
|
|
16
|
+
* npm run build
|
|
17
|
+
* run: >- (folded block scalar)
|
|
18
|
+
* npm test
|
|
19
|
+
*
|
|
20
|
+
* Comment-only lines and blank lines inside blocks are skipped.
|
|
21
|
+
*/
|
|
22
|
+
function extractRunCommands(content) {
|
|
23
|
+
const commands = [];
|
|
24
|
+
const lines = String(content).split(/\r?\n/);
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
const m = line.match(/^(\s*)-?\s*run:\s*(.*)$/);
|
|
29
|
+
if (!m) continue;
|
|
30
|
+
|
|
31
|
+
const baseIndent = m[1].length;
|
|
32
|
+
const rest = m[2].trim();
|
|
33
|
+
|
|
34
|
+
if (/^[|>][+-]?\s*$/.test(rest)) {
|
|
35
|
+
// Block scalar: collect following lines with greater indent than the key
|
|
36
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
37
|
+
const ln = lines[j];
|
|
38
|
+
if (ln.trim() === '') continue;
|
|
39
|
+
const indentMatch = ln.match(/^(\s*)/);
|
|
40
|
+
if (indentMatch[1].length <= baseIndent) break;
|
|
41
|
+
const trimmed = ln.trim();
|
|
42
|
+
if (trimmed && !trimmed.startsWith('#')) commands.push(trimmed);
|
|
43
|
+
}
|
|
44
|
+
} else if (rest && !rest.startsWith('#')) {
|
|
45
|
+
// Inline: strip surrounding single/double quotes if present
|
|
46
|
+
commands.push(rest.replace(/^["']|["']$/g, ''));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return commands;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Classify a shell command as a "gate" — i.e., a quality check that belongs
|
|
55
|
+
* in governance.md (test, lint, typecheck, build, etc.) as opposed to
|
|
56
|
+
* deployment, git operations, or environment setup.
|
|
57
|
+
*
|
|
58
|
+
* This is a heuristic and intentionally conservative: false positives
|
|
59
|
+
* (extra gates) are easier to spot than false negatives (missing gates).
|
|
60
|
+
*/
|
|
61
|
+
function isGateCommand(cmd) {
|
|
62
|
+
const patterns = [
|
|
63
|
+
// Node ecosystem
|
|
64
|
+
/\bnpm (run |ci|test|install)/,
|
|
65
|
+
/\bnpx /,
|
|
66
|
+
/\bnode /,
|
|
67
|
+
/\byarn (test|lint|build|check)/,
|
|
68
|
+
/\bpnpm (run |test|lint|build|check|install|i\b)/,
|
|
69
|
+
/\bbun (test|run)/,
|
|
70
|
+
/\bdeno (test|lint|fmt|check)/,
|
|
71
|
+
// Rust
|
|
72
|
+
/\bcargo (test|build|check|clippy|fmt)/,
|
|
73
|
+
/\brustfmt/,
|
|
74
|
+
// Go
|
|
75
|
+
/\bgo (test|build|vet)/,
|
|
76
|
+
/\bgolangci-lint/,
|
|
77
|
+
// Python — direct + modern runner wrappers
|
|
78
|
+
/\bpytest/,
|
|
79
|
+
/\bpython -m/,
|
|
80
|
+
/\bruff/,
|
|
81
|
+
/\bmypy/,
|
|
82
|
+
/\bflake8/,
|
|
83
|
+
/\bblack\b/,
|
|
84
|
+
/\bisort\b/,
|
|
85
|
+
/\bpylint\b/,
|
|
86
|
+
/\btox\s+(run|r)/,
|
|
87
|
+
/\buv run /,
|
|
88
|
+
/\bpoetry run /,
|
|
89
|
+
/\bpdm run /,
|
|
90
|
+
/\bhatch run /,
|
|
91
|
+
/\brye run /,
|
|
92
|
+
/\bnox\b/,
|
|
93
|
+
// JVM
|
|
94
|
+
/\bgradle/,
|
|
95
|
+
/\bmvn /,
|
|
96
|
+
/\bmaven/,
|
|
97
|
+
/\.\/gradlew/,
|
|
98
|
+
/\.\/mvnw/,
|
|
99
|
+
// Ruby
|
|
100
|
+
/\bbundle exec /,
|
|
101
|
+
/\brake\b/,
|
|
102
|
+
/\brspec\b/,
|
|
103
|
+
/\brubocop/,
|
|
104
|
+
// PHP
|
|
105
|
+
/\bcomposer (test|lint|run|validate)/,
|
|
106
|
+
/\bvendor\/bin\/(phpunit|phpcs|phpstan|psalm|pest|php-cs-fixer|rector)/,
|
|
107
|
+
// .NET
|
|
108
|
+
/\bdotnet (test|build|format)/,
|
|
109
|
+
// Swift
|
|
110
|
+
/\bswift (test|build)/,
|
|
111
|
+
/\bswiftlint/,
|
|
112
|
+
// Elixir
|
|
113
|
+
/\bmix (test|format|credo|dialyzer)/,
|
|
114
|
+
// Node linters
|
|
115
|
+
/\beslint/,
|
|
116
|
+
/\bbiome/,
|
|
117
|
+
/\bprettier/,
|
|
118
|
+
/\btsc/,
|
|
119
|
+
/\bxo\b/,
|
|
120
|
+
// Task runners
|
|
121
|
+
/\bmake /,
|
|
122
|
+
/\bjust /,
|
|
123
|
+
/\btask /,
|
|
124
|
+
// Containers / infra
|
|
125
|
+
/\bdocker (build|compose)/,
|
|
126
|
+
/\bterraform (fmt|validate|plan)/,
|
|
127
|
+
/\btflint/,
|
|
128
|
+
/\bhelm (lint|template)/,
|
|
129
|
+
/\bkubeconform/,
|
|
130
|
+
/\bkubeval/,
|
|
131
|
+
/\bhadolint/,
|
|
132
|
+
/\bactionlint/,
|
|
133
|
+
/\bmarkdownlint/,
|
|
134
|
+
/\byamllint/,
|
|
135
|
+
/\bbuf (lint|build)/,
|
|
136
|
+
/\bspectral lint/,
|
|
137
|
+
/\bshellcheck/,
|
|
138
|
+
/\bsemgrep/,
|
|
139
|
+
/\btrivy/,
|
|
140
|
+
/\bgitleaks/,
|
|
141
|
+
];
|
|
142
|
+
return patterns.some((p) => p.test(cmd));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { extractRunCommands, isGateCommand };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: post-start-validation
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.2
|
|
4
4
|
source_hash: 5a64dfe68b13577dff818fa63ddb6185be360c80b100f205bc586aac39e19e80
|
|
5
5
|
description: Universal validation and knowledge capture. Detects what changed, runs governance gates, captures knowledge, verifies deployment. Works for any project.
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pre-start-context
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.2
|
|
4
4
|
source_hash: b7be8434b99d5b189c904263e783d573c82109218725cc31fbd4fa1bf81538b6
|
|
5
5
|
description: Universal context loader. Discovers any project's stack, architecture, and state at runtime. Reads governance.md for project-specific rules. Works for any language, framework, or deployment target.
|
|
6
6
|
---
|
package/src/update/integrity.js
CHANGED
|
@@ -47,8 +47,18 @@ function readFrontmatter(filePath) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Format a value for YAML frontmatter.
|
|
50
|
+
* Format a value for YAML frontmatter or YAML block scalars.
|
|
51
51
|
* Quotes strings that contain special characters or could be ambiguous.
|
|
52
|
+
*
|
|
53
|
+
* Rules roughly follow YAML 1.2 plain-scalar constraints:
|
|
54
|
+
* - Leading/trailing whitespace → must quote
|
|
55
|
+
* - Special markers anywhere: : # & * ! | > ' " % @ `
|
|
56
|
+
* - Leading flow indicators: [ ] { } , (would start a flow sequence/map)
|
|
57
|
+
* - Leading dash + space looks like a block sequence entry
|
|
58
|
+
* - Leading ? or ! looks like a YAML tag or complex-key marker
|
|
59
|
+
* - Reserved words that coerce to other types: true/false/null/yes/no/~
|
|
60
|
+
* - Number-like strings
|
|
61
|
+
* - Empty string
|
|
52
62
|
*/
|
|
53
63
|
function yamlScalar(value) {
|
|
54
64
|
if (value == null) return '';
|
|
@@ -60,15 +70,18 @@ function yamlScalar(value) {
|
|
|
60
70
|
return `|\n${indented}`;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
// Control characters (tabs, etc.) must be quoted so they survive round-trip.
|
|
74
|
+
// eslint-disable-next-line no-control-regex
|
|
75
|
+
if (/[\x00-\x1f]/.test(str)) {
|
|
76
|
+
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\t/g, '\\t').replace(/\r/g, '\\r')}"`;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
const needsQuoting =
|
|
70
80
|
/^[\s]|[\s]$/.test(str) ||
|
|
71
81
|
/[:#&*!|>'"%@`]/.test(str) ||
|
|
82
|
+
/^[\[\]{},]/.test(str) ||
|
|
83
|
+
/^- /.test(str) ||
|
|
84
|
+
/^[?!]/.test(str) ||
|
|
72
85
|
/^(true|false|null|yes|no|~)$/i.test(str) ||
|
|
73
86
|
/^-?\d+(\.\d+)?$/.test(str) ||
|
|
74
87
|
str === '';
|
package/src/update/skill-sync.js
CHANGED