@mmnto/totem 0.21.0 → 0.22.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/dist/adversarial.test.d.ts +2 -0
- package/dist/adversarial.test.d.ts.map +1 -0
- package/dist/adversarial.test.js +255 -0
- package/dist/adversarial.test.js.map +1 -0
- package/dist/ast-classifier.d.ts +17 -0
- package/dist/ast-classifier.d.ts.map +1 -0
- package/dist/ast-classifier.js +155 -0
- package/dist/ast-classifier.js.map +1 -0
- package/dist/ast-classifier.test.d.ts +2 -0
- package/dist/ast-classifier.test.d.ts.map +1 -0
- package/dist/ast-classifier.test.js +109 -0
- package/dist/ast-classifier.test.js.map +1 -0
- package/dist/ast-gate.d.ts +18 -0
- package/dist/ast-gate.d.ts.map +1 -0
- package/dist/ast-gate.js +71 -0
- package/dist/ast-gate.js.map +1 -0
- package/dist/ast-gate.test.d.ts +2 -0
- package/dist/ast-gate.test.d.ts.map +1 -0
- package/dist/ast-gate.test.js +94 -0
- package/dist/ast-gate.test.js.map +1 -0
- package/dist/compiler.d.ts +10 -2
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +34 -13
- package/dist/compiler.js.map +1 -1
- package/dist/compiler.test.js +24 -0
- package/dist/compiler.test.js.map +1 -1
- package/dist/config-schema.d.ts +93 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +7 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config-schema.test.js +13 -1
- package/dist/config-schema.test.js.map +1 -1
- package/dist/drift-detector.d.ts.map +1 -1
- package/dist/drift-detector.js +5 -1
- package/dist/drift-detector.js.map +1 -1
- package/dist/drift-detector.test.js +15 -0
- package/dist/drift-detector.test.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/lesson-format.d.ts +6 -0
- package/dist/lesson-format.d.ts.map +1 -1
- package/dist/lesson-format.js +18 -1
- package/dist/lesson-format.js.map +1 -1
- package/dist/lesson-format.test.js +29 -1
- package/dist/lesson-format.test.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adversarial.test.d.ts","sourceRoot":"","sources":["../src/adversarial.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { applyRules, applyRulesToAdditions, } from './compiler.js';
|
|
3
|
+
// ─── Adversarial Evaluation Harness ─────────────────
|
|
4
|
+
//
|
|
5
|
+
// Tests that compiled Totem rules catch planted bugs in realistic diffs.
|
|
6
|
+
// This is the deterministic subset of #196 — no LLM calls, pure regex.
|
|
7
|
+
// Each test simulates a developer making a known-bad change and verifies
|
|
8
|
+
// the compiled rules catch it before it reaches production.
|
|
9
|
+
const makeRule = (pattern, message, heading) => ({
|
|
10
|
+
lessonHash: 'adversarial',
|
|
11
|
+
lessonHeading: heading,
|
|
12
|
+
pattern,
|
|
13
|
+
message,
|
|
14
|
+
engine: 'regex',
|
|
15
|
+
compiledAt: new Date().toISOString(),
|
|
16
|
+
});
|
|
17
|
+
// ─── Rule set: common project traps ──────────────────
|
|
18
|
+
const ADVERSARIAL_RULES = [
|
|
19
|
+
makeRule('catch\\s*\\(\\s*error\\s*[\\):]', 'Use err, not error, in catch blocks (project convention)', 'Use err not error'),
|
|
20
|
+
makeRule('\\bnpm\\s+(install|run|exec|ci)\\b', 'Use pnpm instead of npm (monorepo convention)', 'Never use npm'),
|
|
21
|
+
makeRule('\\bdebugger\\b', 'Remove debugger statements before commit', 'No debugger in production'),
|
|
22
|
+
makeRule('process\\.exit\\(0\\)', 'Do not call process.exit(0) — let the process end naturally', 'Avoid process.exit(0)'),
|
|
23
|
+
makeRule('\\.catch\\(\\(\\)\\s*=>\\s*\\{\\s*\\}\\)', 'Empty catch blocks swallow errors silently', 'No empty catch blocks'),
|
|
24
|
+
makeRule('TODO|FIXME|HACK|XXX', 'Resolve TODO/FIXME comments before merging', 'No TODO in production'),
|
|
25
|
+
makeRule('password\\s*[:=]\\s*["\'][^"\']+["\']', 'Hardcoded passwords detected — use environment variables', 'No hardcoded secrets'),
|
|
26
|
+
makeRule('\\bany\\b', 'Avoid TypeScript any — use unknown or proper types', 'No any types'),
|
|
27
|
+
];
|
|
28
|
+
// ─── Adversarial diffs: planted bugs ─────────────────
|
|
29
|
+
describe('adversarial evaluation harness', () => {
|
|
30
|
+
it('catches npm usage in a CI script change', () => {
|
|
31
|
+
const diff = `diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
|
|
32
|
+
--- a/.github/workflows/ci.yml
|
|
33
|
+
+++ b/.github/workflows/ci.yml
|
|
34
|
+
@@ -10,3 +10,4 @@ jobs:
|
|
35
|
+
- uses: actions/setup-node@v6
|
|
36
|
+
- run: pnpm install
|
|
37
|
+
+ - run: npm run test
|
|
38
|
+
- run: pnpm build
|
|
39
|
+
`;
|
|
40
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
41
|
+
expect(violations.length).toBeGreaterThan(0); // totem-ignore
|
|
42
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'Never use npm')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('catches error instead of err in catch block', () => {
|
|
45
|
+
const diff = `diff --git a/src/handler.ts b/src/handler.ts
|
|
46
|
+
--- a/src/handler.ts
|
|
47
|
+
+++ b/src/handler.ts
|
|
48
|
+
@@ -5,3 +5,5 @@ export function handler() {
|
|
49
|
+
try {
|
|
50
|
+
doWork();
|
|
51
|
+
+ } catch (error) {
|
|
52
|
+
+ console.error(error);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
56
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'Use err not error')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('catches debugger statements left in production code', () => {
|
|
59
|
+
const diff = `diff --git a/src/api.ts b/src/api.ts
|
|
60
|
+
--- a/src/api.ts
|
|
61
|
+
+++ b/src/api.ts
|
|
62
|
+
@@ -1,3 +1,5 @@
|
|
63
|
+
export async function fetchData() {
|
|
64
|
+
+ debugger;
|
|
65
|
+
const res = await fetch('/api/data');
|
|
66
|
+
+ debugger;
|
|
67
|
+
return res.json();
|
|
68
|
+
`;
|
|
69
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
70
|
+
const debugViolations = violations.filter((v) => v.rule.lessonHeading === 'No debugger in production');
|
|
71
|
+
expect(debugViolations).toHaveLength(2);
|
|
72
|
+
});
|
|
73
|
+
it('catches empty catch block', () => {
|
|
74
|
+
const diff = `diff --git a/src/utils.ts b/src/utils.ts
|
|
75
|
+
--- a/src/utils.ts
|
|
76
|
+
+++ b/src/utils.ts
|
|
77
|
+
@@ -1,3 +1,4 @@
|
|
78
|
+
export function tryParse(json: string) {
|
|
79
|
+
- return JSON.parse(json);
|
|
80
|
+
+ return fetchData().catch(() => {})
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
84
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'No empty catch blocks')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it('catches TODO comments in new code', () => {
|
|
87
|
+
const diff = `diff --git a/src/auth.ts b/src/auth.ts
|
|
88
|
+
--- a/src/auth.ts
|
|
89
|
+
+++ b/src/auth.ts
|
|
90
|
+
@@ -1,3 +1,5 @@
|
|
91
|
+
export function authenticate(token: string) {
|
|
92
|
+
+ // TODO: add rate limiting
|
|
93
|
+
+ // FIXME: this is a temporary hack
|
|
94
|
+
return validateToken(token);
|
|
95
|
+
`;
|
|
96
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
97
|
+
const todoViolations = violations.filter((v) => v.rule.lessonHeading === 'No TODO in production');
|
|
98
|
+
expect(todoViolations).toHaveLength(2);
|
|
99
|
+
});
|
|
100
|
+
it('catches hardcoded password', () => {
|
|
101
|
+
const diff = `diff --git a/src/config.ts b/src/config.ts
|
|
102
|
+
--- a/src/config.ts
|
|
103
|
+
+++ b/src/config.ts
|
|
104
|
+
@@ -1,3 +1,4 @@
|
|
105
|
+
export const config = {
|
|
106
|
+
host: 'localhost',
|
|
107
|
+
+ password: 'hunter2',
|
|
108
|
+
};
|
|
109
|
+
`;
|
|
110
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
111
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'No hardcoded secrets')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it('catches TypeScript any type', () => {
|
|
114
|
+
const diff = `diff --git a/src/types.ts b/src/types.ts
|
|
115
|
+
--- a/src/types.ts
|
|
116
|
+
+++ b/src/types.ts
|
|
117
|
+
@@ -1,3 +1,4 @@
|
|
118
|
+
export interface Config {
|
|
119
|
+
host: string;
|
|
120
|
+
+ metadata: any;
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
124
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'No any types')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
it('passes a clean diff with no violations', () => {
|
|
127
|
+
const diff = `diff --git a/src/clean.ts b/src/clean.ts
|
|
128
|
+
--- a/src/clean.ts
|
|
129
|
+
+++ b/src/clean.ts
|
|
130
|
+
@@ -1,3 +1,5 @@
|
|
131
|
+
export function greet(name: string): string {
|
|
132
|
+
- return 'hello';
|
|
133
|
+
+ return \`Hello, \${name}!\`;
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
137
|
+
expect(violations).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
it('respects inline suppression (totem-ignore)', () => {
|
|
140
|
+
const diff = `diff --git a/src/cli.ts b/src/cli.ts
|
|
141
|
+
--- a/src/cli.ts
|
|
142
|
+
+++ b/src/cli.ts
|
|
143
|
+
@@ -1,3 +1,4 @@
|
|
144
|
+
export function main() {
|
|
145
|
+
+ debugger; // totem-ignore
|
|
146
|
+
run();
|
|
147
|
+
`;
|
|
148
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
149
|
+
const debugViolations = violations.filter((v) => v.rule.lessonHeading === 'No debugger in production');
|
|
150
|
+
expect(debugViolations).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
it('catches multiple violations in a single diff', () => {
|
|
153
|
+
const diff = `diff --git a/src/bad.ts b/src/bad.ts
|
|
154
|
+
--- a/src/bad.ts
|
|
155
|
+
+++ b/src/bad.ts
|
|
156
|
+
@@ -1,5 +1,10 @@
|
|
157
|
+
export function bad() {
|
|
158
|
+
+ debugger;
|
|
159
|
+
+ // TODO: remove this
|
|
160
|
+
try {
|
|
161
|
+
doWork();
|
|
162
|
+
+ } catch (error) {
|
|
163
|
+
+ // swallow
|
|
164
|
+
}
|
|
165
|
+
+ const data: any = fetch('/api');
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
const violations = applyRules(ADVERSARIAL_RULES, diff);
|
|
169
|
+
// Should catch: debugger, TODO, error (not err), any
|
|
170
|
+
expect(violations.length).toBeGreaterThanOrEqual(4); // totem-ignore
|
|
171
|
+
const headings = new Set(violations.map((v) => v.rule.lessonHeading));
|
|
172
|
+
expect(headings.has('No debugger in production')).toBe(true);
|
|
173
|
+
expect(headings.has('No TODO in production')).toBe(true);
|
|
174
|
+
expect(headings.has('Use err not error')).toBe(true);
|
|
175
|
+
expect(headings.has('No any types')).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ─── AST gating: false-positive suppression ─────────
|
|
179
|
+
describe('AST gating suppresses false positives', () => {
|
|
180
|
+
it('skips violations in string-context additions', () => {
|
|
181
|
+
const rules = ADVERSARIAL_RULES;
|
|
182
|
+
// Simulate additions where AST gate has classified lines as string
|
|
183
|
+
// (e.g., inside a template literal test fixture)
|
|
184
|
+
const additions = [
|
|
185
|
+
{
|
|
186
|
+
file: 'src/test.ts',
|
|
187
|
+
line: ' debugger;',
|
|
188
|
+
lineNumber: 3,
|
|
189
|
+
precedingLine: null,
|
|
190
|
+
astContext: 'string', // inside template literal
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
file: 'src/test.ts',
|
|
194
|
+
line: ' // TODO: remove this',
|
|
195
|
+
lineNumber: 4,
|
|
196
|
+
precedingLine: ' debugger;',
|
|
197
|
+
astContext: 'string',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
file: 'src/test.ts',
|
|
201
|
+
line: ' password: "hunter2",',
|
|
202
|
+
lineNumber: 5,
|
|
203
|
+
precedingLine: ' // TODO: remove this',
|
|
204
|
+
astContext: 'string',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
const violations = applyRulesToAdditions(rules, additions);
|
|
208
|
+
expect(violations).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
it('skips violations in comment-context additions', () => {
|
|
211
|
+
const rules = ADVERSARIAL_RULES;
|
|
212
|
+
const additions = [
|
|
213
|
+
{
|
|
214
|
+
file: 'src/app.ts',
|
|
215
|
+
line: '// npm install is required for setup',
|
|
216
|
+
lineNumber: 1,
|
|
217
|
+
precedingLine: null,
|
|
218
|
+
astContext: 'comment',
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
const violations = applyRulesToAdditions(rules, additions);
|
|
222
|
+
expect(violations).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
it('still catches violations in code-context additions', () => {
|
|
225
|
+
const rules = ADVERSARIAL_RULES;
|
|
226
|
+
const additions = [
|
|
227
|
+
{
|
|
228
|
+
file: 'src/app.ts',
|
|
229
|
+
line: ' debugger;',
|
|
230
|
+
lineNumber: 5,
|
|
231
|
+
precedingLine: null,
|
|
232
|
+
astContext: 'code',
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
const violations = applyRulesToAdditions(rules, additions);
|
|
236
|
+
expect(violations.length).toBeGreaterThan(0); // totem-ignore
|
|
237
|
+
expect(violations.some((v) => v.rule.lessonHeading === 'No debugger in production')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it('still catches violations when astContext is undefined (fail-open)', () => {
|
|
240
|
+
const rules = ADVERSARIAL_RULES;
|
|
241
|
+
// No astContext = not classified = treated as code (fail-open)
|
|
242
|
+
const additions = [
|
|
243
|
+
{
|
|
244
|
+
file: 'src/app.ts',
|
|
245
|
+
line: ' debugger;',
|
|
246
|
+
lineNumber: 5,
|
|
247
|
+
precedingLine: null,
|
|
248
|
+
// astContext is undefined
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
const violations = applyRulesToAdditions(rules, additions);
|
|
252
|
+
expect(violations.length).toBeGreaterThan(0); // totem-ignore
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
//# sourceMappingURL=adversarial.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adversarial.test.js","sourceRoot":"","sources":["../src/adversarial.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AstContext } from './compiler.js';
|
|
2
|
+
export type SupportedLanguage = 'typescript' | 'tsx' | 'javascript';
|
|
3
|
+
/**
|
|
4
|
+
* Classify specific lines of source code by their AST context.
|
|
5
|
+
*
|
|
6
|
+
* @param content - Full file content (used for parsing)
|
|
7
|
+
* @param lineNumbers - 1-based line numbers to classify
|
|
8
|
+
* @param language - Language to parse as
|
|
9
|
+
* @returns Map from line number to AstContext
|
|
10
|
+
*/
|
|
11
|
+
export declare function classifyLines(content: string, lineNumbers: number[], language: SupportedLanguage): Promise<Map<number, AstContext>>;
|
|
12
|
+
/**
|
|
13
|
+
* Map file extension to a supported Tree-sitter language.
|
|
14
|
+
* Returns undefined for unsupported extensions.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extensionToLanguage(ext: string): SupportedLanguage | undefined;
|
|
17
|
+
//# sourceMappingURL=ast-classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-classifier.d.ts","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAIhD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,KAAK,GAAG,YAAY,CAAC;AAgGpE;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CA+ClC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAe9E"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// ─── Lazy-loaded Tree-sitter state ──────────────────
|
|
3
|
+
let Parser = null;
|
|
4
|
+
let initPromise = null;
|
|
5
|
+
const grammarCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Initialize web-tree-sitter WASM engine. Idempotent — safe to call multiple times.
|
|
8
|
+
*/
|
|
9
|
+
async function ensureInit() {
|
|
10
|
+
if (Parser)
|
|
11
|
+
return;
|
|
12
|
+
if (initPromise)
|
|
13
|
+
return initPromise;
|
|
14
|
+
initPromise = (async () => {
|
|
15
|
+
const TreeSitter = await import('web-tree-sitter');
|
|
16
|
+
const ParserClass = TreeSitter.default?.Parser ?? TreeSitter.Parser;
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
// Locate the WASM engine file
|
|
19
|
+
const wasmPath = require.resolve('web-tree-sitter/web-tree-sitter.wasm');
|
|
20
|
+
await ParserClass.init({ locateFile: () => wasmPath });
|
|
21
|
+
Parser = ParserClass;
|
|
22
|
+
})();
|
|
23
|
+
return initPromise;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load a Tree-sitter grammar WASM file for the given language.
|
|
27
|
+
*/
|
|
28
|
+
async function loadGrammar(lang) {
|
|
29
|
+
const cached = grammarCache.get(lang);
|
|
30
|
+
if (cached)
|
|
31
|
+
return cached;
|
|
32
|
+
await ensureInit();
|
|
33
|
+
const TreeSitter = await import('web-tree-sitter');
|
|
34
|
+
const LanguageClass = TreeSitter.default?.Language ?? TreeSitter.Language;
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
let wasmFile;
|
|
37
|
+
switch (lang) {
|
|
38
|
+
case 'typescript':
|
|
39
|
+
wasmFile = require.resolve('tree-sitter-typescript/tree-sitter-typescript.wasm');
|
|
40
|
+
break;
|
|
41
|
+
case 'tsx':
|
|
42
|
+
wasmFile = require.resolve('tree-sitter-typescript/tree-sitter-tsx.wasm');
|
|
43
|
+
break;
|
|
44
|
+
case 'javascript':
|
|
45
|
+
wasmFile = require.resolve('tree-sitter-javascript/tree-sitter-javascript.wasm');
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const grammar = await LanguageClass.load(wasmFile);
|
|
49
|
+
grammarCache.set(lang, grammar);
|
|
50
|
+
return grammar;
|
|
51
|
+
}
|
|
52
|
+
// ─── Node type classification ───────────────────────
|
|
53
|
+
/** Tree-sitter node types that represent string literals */
|
|
54
|
+
const STRING_NODE_TYPES = new Set([
|
|
55
|
+
'string',
|
|
56
|
+
'template_string',
|
|
57
|
+
'string_fragment',
|
|
58
|
+
'template_literal_type',
|
|
59
|
+
]);
|
|
60
|
+
/** Tree-sitter node types that represent comments */
|
|
61
|
+
const COMMENT_NODE_TYPES = new Set(['comment']);
|
|
62
|
+
/** Tree-sitter node types that represent regex literals */
|
|
63
|
+
const REGEX_NODE_TYPES = new Set(['regex', 'regex_pattern']);
|
|
64
|
+
/**
|
|
65
|
+
* Walk the AST ancestry from a node to determine its syntactic context.
|
|
66
|
+
* Returns the deepest enclosing string/comment/regex context, or 'code'.
|
|
67
|
+
*/
|
|
68
|
+
function classifyNode(node) {
|
|
69
|
+
let current = node;
|
|
70
|
+
while (current) {
|
|
71
|
+
if (STRING_NODE_TYPES.has(current.type))
|
|
72
|
+
return 'string';
|
|
73
|
+
if (COMMENT_NODE_TYPES.has(current.type))
|
|
74
|
+
return 'comment';
|
|
75
|
+
if (REGEX_NODE_TYPES.has(current.type))
|
|
76
|
+
return 'regex';
|
|
77
|
+
current = current.parent;
|
|
78
|
+
}
|
|
79
|
+
return 'code';
|
|
80
|
+
}
|
|
81
|
+
// ─── Public API ─────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Classify specific lines of source code by their AST context.
|
|
84
|
+
*
|
|
85
|
+
* @param content - Full file content (used for parsing)
|
|
86
|
+
* @param lineNumbers - 1-based line numbers to classify
|
|
87
|
+
* @param language - Language to parse as
|
|
88
|
+
* @returns Map from line number to AstContext
|
|
89
|
+
*/
|
|
90
|
+
export async function classifyLines(content, lineNumbers, language) {
|
|
91
|
+
const result = new Map();
|
|
92
|
+
if (lineNumbers.length === 0)
|
|
93
|
+
return result;
|
|
94
|
+
await ensureInit();
|
|
95
|
+
const grammar = await loadGrammar(language);
|
|
96
|
+
const parser = new Parser();
|
|
97
|
+
try {
|
|
98
|
+
parser.setLanguage(grammar);
|
|
99
|
+
const tree = parser.parse(content);
|
|
100
|
+
if (!tree) {
|
|
101
|
+
// Parse failed — fail-open, leave all lines unclassified
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const rootNode = tree.rootNode;
|
|
106
|
+
const lines = content.split('\n');
|
|
107
|
+
for (const lineNum of lineNumbers) {
|
|
108
|
+
// Tree-sitter uses 0-based rows
|
|
109
|
+
const row = lineNum - 1;
|
|
110
|
+
// Get the named node at the start of this line (skip leading whitespace)
|
|
111
|
+
const lineText = lines[row];
|
|
112
|
+
if (lineText === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
const col = lineText.length - lineText.trimStart().length;
|
|
115
|
+
const node = rootNode.descendantForPosition({ row, column: col });
|
|
116
|
+
if (!node)
|
|
117
|
+
continue;
|
|
118
|
+
// If the node is an ERROR node, classify as 'code' (fail-open)
|
|
119
|
+
if (node.type === 'ERROR' || node.hasError) {
|
|
120
|
+
result.set(lineNum, 'code');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
result.set(lineNum, classifyNode(node));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
tree.delete();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
parser.delete();
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Map file extension to a supported Tree-sitter language.
|
|
137
|
+
* Returns undefined for unsupported extensions.
|
|
138
|
+
*/
|
|
139
|
+
export function extensionToLanguage(ext) {
|
|
140
|
+
switch (ext.toLowerCase()) {
|
|
141
|
+
case '.ts':
|
|
142
|
+
return 'typescript';
|
|
143
|
+
case '.tsx':
|
|
144
|
+
return 'tsx';
|
|
145
|
+
case '.js':
|
|
146
|
+
case '.mjs':
|
|
147
|
+
case '.cjs':
|
|
148
|
+
return 'javascript';
|
|
149
|
+
case '.jsx':
|
|
150
|
+
return 'tsx'; // TSX grammar handles JSX
|
|
151
|
+
default:
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=ast-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-classifier.js","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-classifier.test.d.ts","sourceRoot":"","sources":["../src/ast-classifier.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classifyLines, extensionToLanguage } from './ast-classifier.js';
|
|
3
|
+
// ─── extensionToLanguage ────────────────────────────
|
|
4
|
+
describe('extensionToLanguage', () => {
|
|
5
|
+
it('maps .ts to typescript', () => {
|
|
6
|
+
expect(extensionToLanguage('.ts')).toBe('typescript');
|
|
7
|
+
});
|
|
8
|
+
it('maps .tsx to tsx', () => {
|
|
9
|
+
expect(extensionToLanguage('.tsx')).toBe('tsx');
|
|
10
|
+
});
|
|
11
|
+
it('maps .js/.mjs/.cjs to javascript', () => {
|
|
12
|
+
expect(extensionToLanguage('.js')).toBe('javascript');
|
|
13
|
+
expect(extensionToLanguage('.mjs')).toBe('javascript');
|
|
14
|
+
expect(extensionToLanguage('.cjs')).toBe('javascript');
|
|
15
|
+
});
|
|
16
|
+
it('maps .jsx to tsx', () => {
|
|
17
|
+
expect(extensionToLanguage('.jsx')).toBe('tsx');
|
|
18
|
+
});
|
|
19
|
+
it('returns undefined for unsupported extensions', () => {
|
|
20
|
+
expect(extensionToLanguage('.py')).toBeUndefined();
|
|
21
|
+
expect(extensionToLanguage('.rs')).toBeUndefined();
|
|
22
|
+
expect(extensionToLanguage('.md')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
// ─── classifyLines ──────────────────────────────────
|
|
26
|
+
describe('classifyLines', () => {
|
|
27
|
+
it('classifies a regular code line as code', async () => {
|
|
28
|
+
const content = `const x = 1;\nconsole.log(x);\n`; // totem-ignore
|
|
29
|
+
const result = await classifyLines(content, [1, 2], 'typescript');
|
|
30
|
+
expect(result.get(1)).toBe('code');
|
|
31
|
+
expect(result.get(2)).toBe('code');
|
|
32
|
+
});
|
|
33
|
+
it('classifies a single-line comment as comment', async () => {
|
|
34
|
+
const content = `// this is a comment\nconst x = 1;\n`;
|
|
35
|
+
const result = await classifyLines(content, [1], 'typescript');
|
|
36
|
+
expect(result.get(1)).toBe('comment');
|
|
37
|
+
});
|
|
38
|
+
it('classifies a multi-line comment as comment', async () => {
|
|
39
|
+
const content = `/*\n * block comment\n */\nconst x = 1;\n`;
|
|
40
|
+
const result = await classifyLines(content, [2], 'typescript');
|
|
41
|
+
expect(result.get(2)).toBe('comment');
|
|
42
|
+
});
|
|
43
|
+
it('classifies a string literal as string', async () => {
|
|
44
|
+
const content = `const msg = "hello world";\n`;
|
|
45
|
+
// The string token is on line 1 — but the whole line is an assignment (code)
|
|
46
|
+
// The string node is a child of the variable declaration
|
|
47
|
+
// classifyLines checks the node at the start of the trimmed line
|
|
48
|
+
const result = await classifyLines(content, [1], 'typescript');
|
|
49
|
+
// The leftmost token is `const`, which is code
|
|
50
|
+
expect(result.get(1)).toBe('code');
|
|
51
|
+
});
|
|
52
|
+
it('classifies content inside a template literal as string', async () => {
|
|
53
|
+
const content = [
|
|
54
|
+
'const fixture = `',
|
|
55
|
+
' this is inside a template literal',
|
|
56
|
+
' console.log("fake code")',
|
|
57
|
+
'`;',
|
|
58
|
+
].join('\n');
|
|
59
|
+
const result = await classifyLines(content, [2, 3], 'typescript');
|
|
60
|
+
expect(result.get(2)).toBe('string');
|
|
61
|
+
expect(result.get(3)).toBe('string');
|
|
62
|
+
});
|
|
63
|
+
it('classifies a multi-line template literal body as string', async () => {
|
|
64
|
+
const content = [
|
|
65
|
+
'const diff = `diff --git a/foo.ts b/foo.ts',
|
|
66
|
+
'--- a/foo.ts',
|
|
67
|
+
'+++ b/foo.ts',
|
|
68
|
+
'@@ -1,3 +1,4 @@',
|
|
69
|
+
' const a = 1;',
|
|
70
|
+
'+const b = 2;',
|
|
71
|
+
'`;',
|
|
72
|
+
].join('\n');
|
|
73
|
+
// Lines 2-6 are inside the template literal
|
|
74
|
+
const result = await classifyLines(content, [2, 3, 4, 5, 6], 'typescript');
|
|
75
|
+
for (const line of [2, 3, 4, 5, 6]) {
|
|
76
|
+
expect(result.get(line)).toBe('string');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it('classifies a regex literal as regex', async () => {
|
|
80
|
+
const content = `const re = /foo\\.bar/;\n`;
|
|
81
|
+
const result = await classifyLines(content, [1], 'typescript');
|
|
82
|
+
// The leftmost token is `const` → code
|
|
83
|
+
expect(result.get(1)).toBe('code');
|
|
84
|
+
});
|
|
85
|
+
it('returns empty map for empty line numbers', async () => {
|
|
86
|
+
const result = await classifyLines('const x = 1;', [], 'typescript');
|
|
87
|
+
expect(result.size).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
it('handles JavaScript files', async () => {
|
|
90
|
+
const content = `// JS comment\nconst x = 1;\n`;
|
|
91
|
+
const result = await classifyLines(content, [1, 2], 'javascript');
|
|
92
|
+
expect(result.get(1)).toBe('comment');
|
|
93
|
+
expect(result.get(2)).toBe('code');
|
|
94
|
+
});
|
|
95
|
+
it('handles TSX files', async () => {
|
|
96
|
+
const content = `// TSX comment\nconst x = <div>hello</div>;\n`;
|
|
97
|
+
const result = await classifyLines(content, [1, 2], 'tsx');
|
|
98
|
+
expect(result.get(1)).toBe('comment');
|
|
99
|
+
expect(result.get(2)).toBe('code');
|
|
100
|
+
});
|
|
101
|
+
it('classifies ERROR nodes as code (fail-open)', async () => {
|
|
102
|
+
// Intentionally broken syntax
|
|
103
|
+
const content = `const x = {{\n`;
|
|
104
|
+
const result = await classifyLines(content, [1], 'typescript');
|
|
105
|
+
// Should not throw, and should classify as code (fail-open)
|
|
106
|
+
expect(result.get(1)).toBe('code');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=ast-classifier.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-classifier.test.js","sourceRoot":"","sources":["../src/ast-classifier.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DiffAddition } from './compiler.js';
|
|
2
|
+
export interface AstGateOptions {
|
|
3
|
+
/** Working directory to resolve file paths against */
|
|
4
|
+
cwd?: string;
|
|
5
|
+
/** Optional callback for non-fatal warnings (e.g., file not readable) */
|
|
6
|
+
onWarn?: (msg: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Enrich diff additions with AST context by parsing the actual files.
|
|
10
|
+
*
|
|
11
|
+
* Groups additions by file, reads each file from disk, runs Tree-sitter
|
|
12
|
+
* classification, and sets `astContext` on each addition.
|
|
13
|
+
*
|
|
14
|
+
* Additions for unsupported file types or unreadable files are left
|
|
15
|
+
* with `astContext: undefined` (fail-open — treated as code by the shield).
|
|
16
|
+
*/
|
|
17
|
+
export declare function enrichWithAstContext(additions: DiffAddition[], options?: AstGateOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=ast-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-gate.d.ts","sourceRoot":"","sources":["../src/ast-gate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,eAAe,CAAC;AAI9D,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAID;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,YAAY,EAAE,EACzB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
|