@webpieces/ai-hook-rules 0.0.1 → 0.2.113
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/package.json +4 -3
- package/src/adapters/claude-code-hook.d.ts +1 -0
- package/src/adapters/claude-code-hook.js +112 -0
- package/src/adapters/claude-code-hook.js.map +1 -0
- package/src/adapters/openclaw-plugin.d.ts +14 -0
- package/src/adapters/openclaw-plugin.js +73 -0
- package/src/adapters/openclaw-plugin.js.map +1 -0
- package/src/core/build-context.d.ts +8 -0
- package/src/core/build-context.js +62 -0
- package/src/core/build-context.js.map +1 -0
- package/src/core/configs/default.d.ts +2 -0
- package/src/core/configs/{default.ts → default.js} +6 -3
- package/src/core/configs/default.js.map +1 -0
- package/src/core/disable-directives.d.ts +9 -0
- package/src/core/disable-directives.js +92 -0
- package/src/core/disable-directives.js.map +1 -0
- package/src/core/instruct-ai-writer.d.ts +1 -0
- package/src/core/instruct-ai-writer.js +18 -0
- package/src/core/instruct-ai-writer.js.map +1 -0
- package/src/core/load-config.d.ts +1 -0
- package/src/core/load-config.js +10 -0
- package/src/core/load-config.js.map +1 -0
- package/src/core/load-rules.d.ts +3 -0
- package/src/core/{load-rules.ts → load-rules.js} +33 -32
- package/src/core/load-rules.js.map +1 -0
- package/src/core/rejection-log.d.ts +2 -0
- package/src/core/{rejection-log.ts → rejection-log.js} +34 -51
- package/src/core/rejection-log.js.map +1 -0
- package/src/core/report.d.ts +2 -0
- package/src/core/{report.ts → report.js} +7 -8
- package/src/core/report.js.map +1 -0
- package/src/core/rules/catch-error-pattern.d.ts +3 -0
- package/src/core/rules/{catch-error-pattern.ts → catch-error-pattern.js} +25 -54
- package/src/core/rules/catch-error-pattern.js.map +1 -0
- package/src/core/rules/file-location.d.ts +3 -0
- package/src/core/rules/{file-location.ts → file-location.js} +29 -43
- package/src/core/rules/file-location.js.map +1 -0
- package/src/core/rules/index.d.ts +1 -0
- package/src/core/rules/{index.ts → index.js} +5 -1
- package/src/core/rules/index.js.map +1 -0
- package/src/core/rules/max-file-lines.d.ts +3 -0
- package/src/core/rules/{max-file-lines.ts → max-file-lines.js} +17 -23
- package/src/core/rules/max-file-lines.js.map +1 -0
- package/src/core/rules/no-any-unknown.d.ts +3 -0
- package/src/core/rules/no-any-unknown.js +30 -0
- package/src/core/rules/no-any-unknown.js.map +1 -0
- package/src/core/rules/no-destructure.d.ts +3 -0
- package/src/core/rules/{no-destructure.ts → no-destructure.js} +13 -17
- package/src/core/rules/no-destructure.js.map +1 -0
- package/src/core/rules/no-implicit-any.d.ts +3 -0
- package/src/core/rules/{no-implicit-any.ts → no-implicit-any.js} +32 -30
- package/src/core/rules/no-implicit-any.js.map +1 -0
- package/src/core/rules/no-shell-substitution.d.ts +3 -0
- package/src/core/rules/no-shell-substitution.js +54 -0
- package/src/core/rules/no-shell-substitution.js.map +1 -0
- package/src/core/rules/no-unmanaged-exceptions.d.ts +3 -0
- package/src/core/rules/{no-unmanaged-exceptions.ts → no-unmanaged-exceptions.js} +21 -24
- package/src/core/rules/no-unmanaged-exceptions.js.map +1 -0
- package/src/core/rules/require-return-type.d.ts +3 -0
- package/src/core/rules/{require-return-type.ts → require-return-type.js} +21 -28
- package/src/core/rules/require-return-type.js.map +1 -0
- package/src/core/runner.d.ts +3 -0
- package/src/core/runner.js +181 -0
- package/src/core/runner.js.map +1 -0
- package/src/core/strip-ts-noise.d.ts +1 -0
- package/src/core/strip-ts-noise.js +178 -0
- package/src/core/strip-ts-noise.js.map +1 -0
- package/src/core/to-error.d.ts +5 -0
- package/src/core/{to-error.ts → to-error.js} +7 -4
- package/src/core/to-error.js.map +1 -0
- package/src/core/types.d.ts +93 -0
- package/src/core/types.js +93 -0
- package/src/core/types.js.map +1 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +25 -0
- package/src/index.js.map +1 -0
- package/LICENSE +0 -373
- package/src/adapters/claude-code-hook.ts +0 -117
- package/src/adapters/openclaw-plugin.ts +0 -88
- package/src/core/__tests__/disable-directives.test.ts +0 -114
- package/src/core/__tests__/rules/file-location.test.ts +0 -90
- package/src/core/__tests__/rules/max-file-lines.test.ts +0 -53
- package/src/core/__tests__/rules/no-any.test.ts +0 -68
- package/src/core/__tests__/rules/no-destructure.test.ts +0 -50
- package/src/core/__tests__/rules/no-shell-substitution.test.ts +0 -118
- package/src/core/__tests__/rules/no-unmanaged-exceptions.test.ts +0 -54
- package/src/core/__tests__/rules/require-return-type.test.ts +0 -79
- package/src/core/__tests__/runner.test.ts +0 -288
- package/src/core/__tests__/strip-ts-noise.test.ts +0 -109
- package/src/core/build-context.ts +0 -96
- package/src/core/disable-directives.ts +0 -90
- package/src/core/instruct-ai-writer.ts +0 -15
- package/src/core/load-config.ts +0 -3
- package/src/core/rules/no-any-unknown.ts +0 -35
- package/src/core/rules/no-shell-substitution.ts +0 -71
- package/src/core/runner.ts +0 -205
- package/src/core/strip-ts-noise.ts +0 -103
- package/src/core/types.ts +0 -196
- package/src/index.ts +0 -14
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { EditRule, EditContext, Violation } from '../types';
|
|
2
|
-
import { Violation as V } from '../types';
|
|
3
|
-
|
|
4
|
-
const ANY_PATTERN =
|
|
5
|
-
/(?::\s*any\b|\bas\s+any\b|<any>|any\[\]|Array<any>|Promise<any>|Map<[^,<>]+,\s*any\s*>|Record<[^,<>]+,\s*any\s*>|Set<any>)/;
|
|
6
|
-
|
|
7
|
-
const noAnyRule: EditRule = {
|
|
8
|
-
name: 'no-any-unknown',
|
|
9
|
-
description: 'Disallow the `any` keyword. Use concrete types or interfaces.',
|
|
10
|
-
scope: 'edit',
|
|
11
|
-
files: ['**/*.ts', '**/*.tsx'],
|
|
12
|
-
defaultOptions: {},
|
|
13
|
-
fixHint: [
|
|
14
|
-
'Prefer: interface MyData { ... } or class MyData { ... }',
|
|
15
|
-
'// webpieces-disable no-any-unknown -- <one-line reason>',
|
|
16
|
-
],
|
|
17
|
-
|
|
18
|
-
check(ctx: EditContext): readonly Violation[] {
|
|
19
|
-
const violations: V[] = [];
|
|
20
|
-
for (let i = 0; i < ctx.strippedLines.length; i += 1) {
|
|
21
|
-
const stripped = ctx.strippedLines[i];
|
|
22
|
-
if (!ANY_PATTERN.test(stripped)) continue;
|
|
23
|
-
const lineNum = i + 1;
|
|
24
|
-
if (ctx.isLineDisabled(lineNum, 'no-any-unknown')) continue;
|
|
25
|
-
violations.push(new V(
|
|
26
|
-
lineNum,
|
|
27
|
-
ctx.lines[i].trim(),
|
|
28
|
-
'`any` erases type information. Use a concrete type, an interface, or `unknown` with type guards.',
|
|
29
|
-
));
|
|
30
|
-
}
|
|
31
|
-
return violations;
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export default noAnyRule;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { BashRule, BashContext, Violation } from '../types';
|
|
2
|
-
import { Violation as V } from '../types';
|
|
3
|
-
|
|
4
|
-
const FIX_HINT: readonly string[] = [
|
|
5
|
-
'Shell substitutions trigger Claude Code "simple_expansion" permission prompts that interrupt the user.',
|
|
6
|
-
'Instead:',
|
|
7
|
-
' • Build payload files with Write, then: node script.js < /path/to/payload',
|
|
8
|
-
' • Use Read, Grep, or Glob instead of piping shell output through $(...)',
|
|
9
|
-
' • Write a small script file with Write and execute it: bash /path/to/script.sh',
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
const noShellSubstitutionRule: BashRule = {
|
|
13
|
-
name: 'no-shell-substitution',
|
|
14
|
-
description: 'Reject Bash commands containing shell substitutions ($(...), backticks, $VAR).',
|
|
15
|
-
scope: 'bash',
|
|
16
|
-
files: [],
|
|
17
|
-
defaultOptions: {},
|
|
18
|
-
fixHint: FIX_HINT,
|
|
19
|
-
|
|
20
|
-
check(ctx: BashContext): readonly Violation[] {
|
|
21
|
-
const scanned = stripSingleQuoted(ctx.command);
|
|
22
|
-
const violations: Violation[] = [];
|
|
23
|
-
|
|
24
|
-
if (/\$\(/.test(scanned)) {
|
|
25
|
-
violations.push(new V(
|
|
26
|
-
1,
|
|
27
|
-
truncate(ctx.command),
|
|
28
|
-
'Command contains `$(...)` command substitution.',
|
|
29
|
-
));
|
|
30
|
-
}
|
|
31
|
-
if (hasUnescapedBacktick(scanned)) {
|
|
32
|
-
violations.push(new V(
|
|
33
|
-
1,
|
|
34
|
-
truncate(ctx.command),
|
|
35
|
-
'Command contains backtick command substitution.',
|
|
36
|
-
));
|
|
37
|
-
}
|
|
38
|
-
if (/\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(scanned) || hasBareVarExpansion(scanned)) {
|
|
39
|
-
violations.push(new V(
|
|
40
|
-
1,
|
|
41
|
-
truncate(ctx.command),
|
|
42
|
-
'Command contains `$VAR` or `${VAR}` variable expansion.',
|
|
43
|
-
));
|
|
44
|
-
}
|
|
45
|
-
return violations;
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function stripSingleQuoted(cmd: string): string {
|
|
50
|
-
return cmd.replace(/'[^']*'/g, "''");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function hasUnescapedBacktick(cmd: string): boolean {
|
|
54
|
-
for (let i = 0; i < cmd.length; i += 1) {
|
|
55
|
-
if (cmd[i] === '`' && (i === 0 || cmd[i - 1] !== '\\')) return true;
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function hasBareVarExpansion(cmd: string): boolean {
|
|
61
|
-
const re = /(^|[^\\])\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
62
|
-
return re.test(cmd);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function truncate(s: string): string {
|
|
66
|
-
const MAX = 120;
|
|
67
|
-
if (s.length <= MAX) return s;
|
|
68
|
-
return s.slice(0, MAX) + '…';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export default noShellSubstitutionRule;
|
package/src/core/runner.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
|
|
3
|
-
import { buildContexts, buildBashContext } from './build-context';
|
|
4
|
-
import { loadConfig } from './load-config';
|
|
5
|
-
import { loadRules, globMatches } from './load-rules';
|
|
6
|
-
import { toError } from './to-error';
|
|
7
|
-
import { formatReport } from './report';
|
|
8
|
-
import {
|
|
9
|
-
ToolKind, NormalizedToolInput, BlockedResult,
|
|
10
|
-
Rule, EditRule, FileRule, BashRule, Violation, RuleGroup,
|
|
11
|
-
EditContext, FileContext, BashContext,
|
|
12
|
-
ResolvedConfig, ResolvedRuleConfig, RuleOptions,
|
|
13
|
-
} from './types';
|
|
14
|
-
|
|
15
|
-
export function run(
|
|
16
|
-
toolKind: ToolKind,
|
|
17
|
-
input: NormalizedToolInput,
|
|
18
|
-
cwd: string,
|
|
19
|
-
): BlockedResult | null {
|
|
20
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
21
|
-
try {
|
|
22
|
-
return runInternal(toolKind, input, cwd);
|
|
23
|
-
} catch (err: unknown) {
|
|
24
|
-
const error = toError(err);
|
|
25
|
-
console.error(`[ai-hooks] runner crashed (failing open): ${error.message}`);
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function runInternal(
|
|
31
|
-
toolKind: ToolKind,
|
|
32
|
-
input: NormalizedToolInput,
|
|
33
|
-
cwd: string,
|
|
34
|
-
): BlockedResult | null {
|
|
35
|
-
const config = loadConfig(cwd);
|
|
36
|
-
if (!config.configPath) return null;
|
|
37
|
-
|
|
38
|
-
const workspaceRoot = path.dirname(config.configPath);
|
|
39
|
-
const rules = loadRules(config, workspaceRoot);
|
|
40
|
-
if (rules.length === 0) return null;
|
|
41
|
-
|
|
42
|
-
const contexts = buildContexts(toolKind, input, workspaceRoot);
|
|
43
|
-
const relativePath = path.relative(workspaceRoot, input.filePath);
|
|
44
|
-
|
|
45
|
-
const editGroups = runEditRules(rules, contexts.editContexts, config);
|
|
46
|
-
const fileGroups = runFileRules(rules, contexts.fileContext, config);
|
|
47
|
-
const allGroups = [...editGroups, ...fileGroups];
|
|
48
|
-
|
|
49
|
-
if (allGroups.length === 0) return null;
|
|
50
|
-
|
|
51
|
-
const report = formatReport(relativePath, allGroups);
|
|
52
|
-
return new BlockedResult(report);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function runBash(command: string, cwd: string): BlockedResult | null {
|
|
56
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
57
|
-
try {
|
|
58
|
-
return runBashInternal(command, cwd);
|
|
59
|
-
} catch (err: unknown) {
|
|
60
|
-
const error = toError(err);
|
|
61
|
-
console.error(`[ai-hooks] bash runner crashed (failing open): ${error.message}`);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function runBashInternal(command: string, cwd: string): BlockedResult | null {
|
|
67
|
-
const config = loadConfig(cwd);
|
|
68
|
-
if (!config.configPath) return null;
|
|
69
|
-
|
|
70
|
-
const workspaceRoot = path.dirname(config.configPath);
|
|
71
|
-
const rules = loadRules(config, workspaceRoot);
|
|
72
|
-
if (rules.length === 0) return null;
|
|
73
|
-
|
|
74
|
-
const ctx = buildBashContext(command, workspaceRoot);
|
|
75
|
-
const groups = runBashRules(rules, ctx, config);
|
|
76
|
-
if (groups.length === 0) return null;
|
|
77
|
-
|
|
78
|
-
const report = formatReport('<bash>', groups);
|
|
79
|
-
return new BlockedResult(report);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function safeCheckBash(rule: BashRule, ctx: BashContext): readonly Violation[] {
|
|
83
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
84
|
-
try {
|
|
85
|
-
return rule.check(ctx);
|
|
86
|
-
} catch (err: unknown) {
|
|
87
|
-
const error = toError(err);
|
|
88
|
-
process.stderr.write(`[ai-hooks] rule ${rule.name} crashed: ${error.message}\n`);
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function runBashRules(
|
|
94
|
-
rules: readonly Rule[],
|
|
95
|
-
bashContext: BashContext,
|
|
96
|
-
config: ResolvedConfig,
|
|
97
|
-
): readonly RuleGroup[] {
|
|
98
|
-
const groups: RuleGroup[] = [];
|
|
99
|
-
for (const rule of rules) {
|
|
100
|
-
if (rule.scope !== 'bash') continue;
|
|
101
|
-
const ruleConfig = config.rules.get(rule.name);
|
|
102
|
-
if (!ruleConfig || ruleConfig.enabled === false) continue;
|
|
103
|
-
bashContext.options = mergeOptions(rule.defaultOptions, ruleConfig);
|
|
104
|
-
const vs = safeCheckBash(rule as BashRule, bashContext);
|
|
105
|
-
if (vs.length > 0) {
|
|
106
|
-
groups.push(new RuleGroup(
|
|
107
|
-
rule.name, rule.description, [...rule.fixHint], [...vs],
|
|
108
|
-
));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return groups;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function ruleMatchesFile(rule: Rule, relativePath: string): boolean {
|
|
115
|
-
for (const pattern of rule.files) {
|
|
116
|
-
if (globMatches(pattern, relativePath)) return true;
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function mergeOptions(defaultOptions: RuleOptions, ruleConfig: ResolvedRuleConfig): RuleOptions {
|
|
122
|
-
// webpieces-disable no-any-unknown -- building an options bag from opaque RuleOptions
|
|
123
|
-
const out: Record<string, unknown> = {};
|
|
124
|
-
for (const key of Object.keys(defaultOptions)) out[key] = defaultOptions[key];
|
|
125
|
-
for (const key of Object.keys(ruleConfig.options)) {
|
|
126
|
-
if (key === 'enabled') continue;
|
|
127
|
-
out[key] = ruleConfig.options[key];
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function safeCheckEdit(rule: EditRule, ctx: EditContext): readonly Violation[] {
|
|
133
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
134
|
-
try {
|
|
135
|
-
return rule.check(ctx);
|
|
136
|
-
} catch (err: unknown) {
|
|
137
|
-
const error = toError(err);
|
|
138
|
-
process.stderr.write(`[ai-hooks] rule ${rule.name} crashed: ${error.message}\n`);
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function safeCheckFile(rule: FileRule, ctx: FileContext): readonly Violation[] {
|
|
144
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
145
|
-
try {
|
|
146
|
-
return rule.check(ctx);
|
|
147
|
-
} catch (err: unknown) {
|
|
148
|
-
const error = toError(err);
|
|
149
|
-
process.stderr.write(`[ai-hooks] rule ${rule.name} crashed: ${error.message}\n`);
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function runEditRules(
|
|
155
|
-
rules: readonly Rule[],
|
|
156
|
-
editContexts: readonly EditContext[],
|
|
157
|
-
config: ResolvedConfig,
|
|
158
|
-
): readonly RuleGroup[] {
|
|
159
|
-
const groups: RuleGroup[] = [];
|
|
160
|
-
for (const rule of rules) {
|
|
161
|
-
if (rule.scope !== 'edit') continue;
|
|
162
|
-
const ruleConfig = config.rules.get(rule.name);
|
|
163
|
-
if (!ruleConfig || ruleConfig.enabled === false) continue;
|
|
164
|
-
const allViolations: Violation[] = [];
|
|
165
|
-
for (const ctx of editContexts) {
|
|
166
|
-
if (!ruleMatchesFile(rule, ctx.relativePath)) continue;
|
|
167
|
-
ctx.options = mergeOptions(rule.defaultOptions, ruleConfig);
|
|
168
|
-
const vs = safeCheckEdit(rule as EditRule, ctx);
|
|
169
|
-
for (const v of vs) {
|
|
170
|
-
const copy = new Violation(v.line, v.snippet, v.message);
|
|
171
|
-
copy.editIndex = ctx.editIndex;
|
|
172
|
-
copy.editCount = ctx.editCount;
|
|
173
|
-
allViolations.push(copy);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (allViolations.length > 0) {
|
|
177
|
-
groups.push(new RuleGroup(
|
|
178
|
-
rule.name, rule.description, [...rule.fixHint], allViolations,
|
|
179
|
-
));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return groups;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function runFileRules(
|
|
186
|
-
rules: readonly Rule[],
|
|
187
|
-
fileContext: FileContext,
|
|
188
|
-
config: ResolvedConfig,
|
|
189
|
-
): readonly RuleGroup[] {
|
|
190
|
-
const groups: RuleGroup[] = [];
|
|
191
|
-
for (const rule of rules) {
|
|
192
|
-
if (rule.scope !== 'file') continue;
|
|
193
|
-
const ruleConfig = config.rules.get(rule.name);
|
|
194
|
-
if (!ruleConfig || ruleConfig.enabled === false) continue;
|
|
195
|
-
if (!ruleMatchesFile(rule, fileContext.relativePath)) continue;
|
|
196
|
-
fileContext.options = mergeOptions(rule.defaultOptions, ruleConfig);
|
|
197
|
-
const vs = safeCheckFile(rule as FileRule, fileContext);
|
|
198
|
-
if (vs.length > 0) {
|
|
199
|
-
groups.push(new RuleGroup(
|
|
200
|
-
rule.name, rule.description, [...rule.fixHint], [...vs],
|
|
201
|
-
));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return groups;
|
|
205
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
interface StackFrame {
|
|
2
|
-
readonly state: string;
|
|
3
|
-
readonly braceDepth: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
class StripState {
|
|
7
|
-
readonly source: string;
|
|
8
|
-
readonly len: number;
|
|
9
|
-
readonly out: string[];
|
|
10
|
-
readonly stack: StackFrame[];
|
|
11
|
-
state: string;
|
|
12
|
-
braceDepth: number;
|
|
13
|
-
i: number;
|
|
14
|
-
|
|
15
|
-
constructor(source: string) {
|
|
16
|
-
this.source = source;
|
|
17
|
-
this.len = source.length;
|
|
18
|
-
this.out = new Array<string>(source.length);
|
|
19
|
-
this.stack = [];
|
|
20
|
-
this.state = 'code';
|
|
21
|
-
this.braceDepth = 0;
|
|
22
|
-
this.i = 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
ch(): string { return this.source[this.i]; }
|
|
26
|
-
next(): string { return this.i + 1 < this.len ? this.source[this.i + 1] : ''; }
|
|
27
|
-
emit(idx: number, ch: string): void { this.out[idx] = ch; }
|
|
28
|
-
blank(idx: number, ch: string): void { this.out[idx] = ch === '\n' ? '\n' : ' '; }
|
|
29
|
-
|
|
30
|
-
pushState(newState: string): void {
|
|
31
|
-
this.stack.push({ state: this.state, braceDepth: this.braceDepth });
|
|
32
|
-
this.state = newState;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
popState(): void {
|
|
36
|
-
const frame = this.stack.pop()!;
|
|
37
|
-
this.state = frame.state;
|
|
38
|
-
this.braceDepth = frame.braceDepth;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function handleCodeOrInterp(s: StripState): void {
|
|
43
|
-
const ch = s.ch();
|
|
44
|
-
const next = s.next();
|
|
45
|
-
|
|
46
|
-
if (s.state === 'templateInterp') {
|
|
47
|
-
if (ch === '{') { s.braceDepth += 1; s.emit(s.i, ch); s.i += 1; return; }
|
|
48
|
-
if (ch === '}') {
|
|
49
|
-
if (s.braceDepth === 0) { s.emit(s.i, ch); s.i += 1; s.popState(); return; }
|
|
50
|
-
s.braceDepth -= 1; s.emit(s.i, ch); s.i += 1; return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (ch === '/' && next === '/') { s.emit(s.i, '/'); s.emit(s.i + 1, '/'); s.i += 2; s.pushState('lineComment'); return; }
|
|
54
|
-
if (ch === '/' && next === '*') { s.emit(s.i, '/'); s.emit(s.i + 1, '*'); s.i += 2; s.pushState('blockComment'); return; }
|
|
55
|
-
if (ch === '"') { s.emit(s.i, '"'); s.i += 1; s.pushState('dquote'); return; }
|
|
56
|
-
if (ch === "'") { s.emit(s.i, "'"); s.i += 1; s.pushState('squote'); return; }
|
|
57
|
-
if (ch === '`') { s.emit(s.i, '`'); s.i += 1; s.pushState('template'); return; }
|
|
58
|
-
s.emit(s.i, ch); s.i += 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function handleLineComment(s: StripState): void {
|
|
62
|
-
if (s.ch() === '\n') { s.emit(s.i, '\n'); s.i += 1; s.popState(); return; }
|
|
63
|
-
s.blank(s.i, s.ch()); s.i += 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function handleBlockComment(s: StripState): void {
|
|
67
|
-
if (s.ch() === '*' && s.next() === '/') { s.emit(s.i, '*'); s.emit(s.i + 1, '/'); s.i += 2; s.popState(); return; }
|
|
68
|
-
s.blank(s.i, s.ch()); s.i += 1;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function handleStringLiteral(s: StripState, quoteChar: string): void {
|
|
72
|
-
const ch = s.ch();
|
|
73
|
-
if (ch === '\\' && s.i + 1 < s.len) { s.blank(s.i, ch); s.blank(s.i + 1, s.source[s.i + 1]); s.i += 2; return; }
|
|
74
|
-
if (ch === quoteChar) { s.emit(s.i, quoteChar); s.i += 1; s.popState(); return; }
|
|
75
|
-
s.blank(s.i, ch); s.i += 1;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function handleTemplate(s: StripState): void {
|
|
79
|
-
const ch = s.ch();
|
|
80
|
-
if (ch === '\\' && s.i + 1 < s.len) { s.blank(s.i, ch); s.blank(s.i + 1, s.source[s.i + 1]); s.i += 2; return; }
|
|
81
|
-
if (ch === '`') { s.emit(s.i, '`'); s.i += 1; s.popState(); return; }
|
|
82
|
-
if (ch === '$' && s.next() === '{') {
|
|
83
|
-
s.emit(s.i, '$'); s.emit(s.i + 1, '{'); s.i += 2;
|
|
84
|
-
s.stack.push({ state: 'template', braceDepth: 0 });
|
|
85
|
-
s.state = 'templateInterp'; s.braceDepth = 0;
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
s.blank(s.i, ch); s.i += 1;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function stripTsNoise(source: string): string {
|
|
92
|
-
const s = new StripState(source);
|
|
93
|
-
while (s.i < s.len) {
|
|
94
|
-
if (s.state === 'code' || s.state === 'templateInterp') { handleCodeOrInterp(s); }
|
|
95
|
-
else if (s.state === 'lineComment') { handleLineComment(s); }
|
|
96
|
-
else if (s.state === 'blockComment') { handleBlockComment(s); }
|
|
97
|
-
else if (s.state === 'dquote') { handleStringLiteral(s, '"'); }
|
|
98
|
-
else if (s.state === 'squote') { handleStringLiteral(s, "'"); }
|
|
99
|
-
else if (s.state === 'template') { handleTemplate(s); }
|
|
100
|
-
else { s.emit(s.i, s.ch()); s.i += 1; }
|
|
101
|
-
}
|
|
102
|
-
return s.out.join('');
|
|
103
|
-
}
|
package/src/core/types.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config
|
|
2
|
-
// so ai-hooks and the Nx validate-code executor share one loader and one config file.
|
|
3
|
-
import { RuleOptions } from '@webpieces/rules-config';
|
|
4
|
-
export { ResolvedConfig, ResolvedRuleConfig, RuleOptions } from '@webpieces/rules-config';
|
|
5
|
-
|
|
6
|
-
export type ToolKind = 'Write' | 'Edit' | 'MultiEdit';
|
|
7
|
-
export type RuleScope = 'edit' | 'file' | 'bash';
|
|
8
|
-
export type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;
|
|
9
|
-
|
|
10
|
-
export class Violation {
|
|
11
|
-
readonly line: number;
|
|
12
|
-
readonly snippet: string;
|
|
13
|
-
readonly message: string;
|
|
14
|
-
editIndex: number | undefined;
|
|
15
|
-
editCount: number | undefined;
|
|
16
|
-
|
|
17
|
-
constructor(line: number, snippet: string, message: string) {
|
|
18
|
-
this.line = line;
|
|
19
|
-
this.snippet = snippet;
|
|
20
|
-
this.message = message;
|
|
21
|
-
this.editIndex = undefined;
|
|
22
|
-
this.editCount = undefined;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class NormalizedEdit {
|
|
27
|
-
readonly oldString: string;
|
|
28
|
-
readonly newString: string;
|
|
29
|
-
|
|
30
|
-
constructor(oldString: string, newString: string) {
|
|
31
|
-
this.oldString = oldString;
|
|
32
|
-
this.newString = newString;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class NormalizedToolInput {
|
|
37
|
-
readonly filePath: string;
|
|
38
|
-
readonly edits: readonly NormalizedEdit[];
|
|
39
|
-
|
|
40
|
-
constructor(filePath: string, edits: readonly NormalizedEdit[]) {
|
|
41
|
-
this.filePath = filePath;
|
|
42
|
-
this.edits = edits;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class NormalizedBashInput {
|
|
47
|
-
readonly command: string;
|
|
48
|
-
|
|
49
|
-
constructor(command: string) {
|
|
50
|
-
this.command = command;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class EditContext {
|
|
55
|
-
readonly tool: ToolKind;
|
|
56
|
-
readonly editIndex: number;
|
|
57
|
-
readonly editCount: number;
|
|
58
|
-
readonly filePath: string;
|
|
59
|
-
readonly relativePath: string;
|
|
60
|
-
readonly workspaceRoot: string;
|
|
61
|
-
readonly addedContent: string;
|
|
62
|
-
readonly strippedContent: string;
|
|
63
|
-
readonly lines: readonly string[];
|
|
64
|
-
readonly strippedLines: readonly string[];
|
|
65
|
-
readonly removedContent: string;
|
|
66
|
-
readonly isLineDisabled: IsLineDisabled;
|
|
67
|
-
options: RuleOptions;
|
|
68
|
-
|
|
69
|
-
constructor(
|
|
70
|
-
tool: ToolKind,
|
|
71
|
-
editIndex: number,
|
|
72
|
-
editCount: number,
|
|
73
|
-
filePath: string,
|
|
74
|
-
relativePath: string,
|
|
75
|
-
workspaceRoot: string,
|
|
76
|
-
addedContent: string,
|
|
77
|
-
strippedContent: string,
|
|
78
|
-
lines: readonly string[],
|
|
79
|
-
strippedLines: readonly string[],
|
|
80
|
-
removedContent: string,
|
|
81
|
-
isLineDisabled: IsLineDisabled,
|
|
82
|
-
) {
|
|
83
|
-
this.tool = tool;
|
|
84
|
-
this.editIndex = editIndex;
|
|
85
|
-
this.editCount = editCount;
|
|
86
|
-
this.filePath = filePath;
|
|
87
|
-
this.relativePath = relativePath;
|
|
88
|
-
this.workspaceRoot = workspaceRoot;
|
|
89
|
-
this.addedContent = addedContent;
|
|
90
|
-
this.strippedContent = strippedContent;
|
|
91
|
-
this.lines = lines;
|
|
92
|
-
this.strippedLines = strippedLines;
|
|
93
|
-
this.removedContent = removedContent;
|
|
94
|
-
this.isLineDisabled = isLineDisabled;
|
|
95
|
-
this.options = {};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export class BashContext {
|
|
100
|
-
readonly tool: 'Bash';
|
|
101
|
-
readonly command: string;
|
|
102
|
-
readonly workspaceRoot: string;
|
|
103
|
-
options: RuleOptions;
|
|
104
|
-
|
|
105
|
-
constructor(command: string, workspaceRoot: string) {
|
|
106
|
-
this.tool = 'Bash';
|
|
107
|
-
this.command = command;
|
|
108
|
-
this.workspaceRoot = workspaceRoot;
|
|
109
|
-
this.options = {};
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export class FileContext {
|
|
114
|
-
readonly tool: ToolKind;
|
|
115
|
-
readonly filePath: string;
|
|
116
|
-
readonly relativePath: string;
|
|
117
|
-
readonly workspaceRoot: string;
|
|
118
|
-
readonly currentFileLines: number;
|
|
119
|
-
readonly linesAdded: number;
|
|
120
|
-
readonly linesRemoved: number;
|
|
121
|
-
readonly projectedFileLines: number;
|
|
122
|
-
options: RuleOptions;
|
|
123
|
-
|
|
124
|
-
constructor(
|
|
125
|
-
tool: ToolKind,
|
|
126
|
-
filePath: string,
|
|
127
|
-
relativePath: string,
|
|
128
|
-
workspaceRoot: string,
|
|
129
|
-
currentFileLines: number,
|
|
130
|
-
linesAdded: number,
|
|
131
|
-
linesRemoved: number,
|
|
132
|
-
projectedFileLines: number,
|
|
133
|
-
) {
|
|
134
|
-
this.tool = tool;
|
|
135
|
-
this.filePath = filePath;
|
|
136
|
-
this.relativePath = relativePath;
|
|
137
|
-
this.workspaceRoot = workspaceRoot;
|
|
138
|
-
this.currentFileLines = currentFileLines;
|
|
139
|
-
this.linesAdded = linesAdded;
|
|
140
|
-
this.linesRemoved = linesRemoved;
|
|
141
|
-
this.projectedFileLines = projectedFileLines;
|
|
142
|
-
this.options = {};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
interface RuleBase {
|
|
147
|
-
readonly name: string;
|
|
148
|
-
readonly description: string;
|
|
149
|
-
readonly files: readonly string[];
|
|
150
|
-
readonly defaultOptions: RuleOptions;
|
|
151
|
-
readonly fixHint: readonly string[];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface EditRule extends RuleBase {
|
|
155
|
-
readonly scope: 'edit';
|
|
156
|
-
check(ctx: EditContext): readonly Violation[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface FileRule extends RuleBase {
|
|
160
|
-
readonly scope: 'file';
|
|
161
|
-
check(ctx: FileContext): readonly Violation[];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface BashRule extends RuleBase {
|
|
165
|
-
readonly scope: 'bash';
|
|
166
|
-
check(ctx: BashContext): readonly Violation[];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export type Rule = EditRule | FileRule | BashRule;
|
|
170
|
-
|
|
171
|
-
export class RuleGroup {
|
|
172
|
-
readonly ruleName: string;
|
|
173
|
-
readonly ruleDescription: string;
|
|
174
|
-
readonly fixHint: readonly string[];
|
|
175
|
-
readonly violations: readonly Violation[];
|
|
176
|
-
|
|
177
|
-
constructor(
|
|
178
|
-
ruleName: string,
|
|
179
|
-
ruleDescription: string,
|
|
180
|
-
fixHint: readonly string[],
|
|
181
|
-
violations: readonly Violation[],
|
|
182
|
-
) {
|
|
183
|
-
this.ruleName = ruleName;
|
|
184
|
-
this.ruleDescription = ruleDescription;
|
|
185
|
-
this.fixHint = fixHint;
|
|
186
|
-
this.violations = violations;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export class BlockedResult {
|
|
191
|
-
readonly report: string;
|
|
192
|
-
|
|
193
|
-
constructor(report: string) {
|
|
194
|
-
this.report = report;
|
|
195
|
-
}
|
|
196
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Pluggable write-time validation framework for AI coding agents
|
|
2
|
-
export {
|
|
3
|
-
ToolKind, RuleScope, RuleOptions, IsLineDisabled,
|
|
4
|
-
Violation, NormalizedEdit, NormalizedToolInput,
|
|
5
|
-
EditContext, FileContext,
|
|
6
|
-
Rule, EditRule, FileRule,
|
|
7
|
-
RuleGroup, BlockedResult,
|
|
8
|
-
ResolvedConfig, ResolvedRuleConfig,
|
|
9
|
-
} from './core/types';
|
|
10
|
-
|
|
11
|
-
export { run } from './core/runner';
|
|
12
|
-
export { stripTsNoise } from './core/strip-ts-noise';
|
|
13
|
-
export { parseDirectives, DirectiveIndex, createIsLineDisabled } from './core/disable-directives';
|
|
14
|
-
export { formatReport } from './core/report';
|