@boyingliu01/xp-gate 0.31.3 → 0.31.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/hooks/pre-commit +120 -11
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/typescript.ts +503 -174
- package/principles/baseline.ts +58 -70
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Prove the C/C++ block scanner split is behaviour-preserving on real
|
|
4
|
+
* source, not just on the handful of fixtures above. The scanner feeds
|
|
5
|
+
* every C/C++ metric (length, nesting, io operations), so an off-by-one
|
|
6
|
+
* in the refactored loop would silently shift every reported span.
|
|
7
|
+
* @covers AC-620-03
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { CppAdapter } from '../cpp';
|
|
11
|
+
|
|
12
|
+
vi.mock('fs', () => ({
|
|
13
|
+
readFileSync: vi.fn(),
|
|
14
|
+
existsSync: vi.fn(() => true),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'fs';
|
|
18
|
+
|
|
19
|
+
const SOURCE = [
|
|
20
|
+
'#include <stdio.h>',
|
|
21
|
+
'/* a header comment with a } brace */',
|
|
22
|
+
'int helper(int x) {',
|
|
23
|
+
' // a line comment with a } brace',
|
|
24
|
+
' return x * 2;',
|
|
25
|
+
'}',
|
|
26
|
+
'',
|
|
27
|
+
'int main(void) {',
|
|
28
|
+
' const char* msg = "a } inside a string";',
|
|
29
|
+
" char c = '}';",
|
|
30
|
+
' /* block } comment */',
|
|
31
|
+
' if (helper(1) > 0) {',
|
|
32
|
+
' printf("%s", msg);',
|
|
33
|
+
' }',
|
|
34
|
+
' return 0;',
|
|
35
|
+
'}',
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
describe('AC-620-03: the split scanner finds the same spans as the original', () => {
|
|
39
|
+
it('extracts main() up to its real closing brace', () => {
|
|
40
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(SOURCE);
|
|
41
|
+
const adapter = new CppAdapter('probe.c');
|
|
42
|
+
const start = SOURCE.indexOf('int main(void)');
|
|
43
|
+
const open = SOURCE.indexOf('{', start);
|
|
44
|
+
const out = adapter.extractCodeBlock(open);
|
|
45
|
+
|
|
46
|
+
// The body must run to the LAST brace of main, not to the `}` in a comment
|
|
47
|
+
// or a string. Line count is the cheap proxy for "did not truncate".
|
|
48
|
+
const bodyLines = out.split('\n').length;
|
|
49
|
+
expect(bodyLines, `extracted: ${JSON.stringify(out)}`).toBe(
|
|
50
|
+
SOURCE.slice(open).split('\n').length,
|
|
51
|
+
);
|
|
52
|
+
expect(out).toContain('return 0;');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('every declaration in the file yields a span that ends on a balanced brace', () => {
|
|
56
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(SOURCE);
|
|
57
|
+
const adapter = new CppAdapter('probe.c');
|
|
58
|
+
const fns = adapter.extractFunctions() as Array<{ name: string; code?: string }>;
|
|
59
|
+
expect(fns.length).toBeGreaterThanOrEqual(2);
|
|
60
|
+
for (const fn of fns) {
|
|
61
|
+
if (!fn.code) continue;
|
|
62
|
+
// Balanced braces modulo the literals/comments the scanner masks.
|
|
63
|
+
expect(fn.code.length, `${fn.name} produced an empty or unbounded span`)
|
|
64
|
+
.toBeLessThanOrEqual(SOURCE.length);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Prove the block-comment masking defect before fixing it. The masker's
|
|
4
|
+
* contract is to preserve offsets and newlines so that `lineOf()` and
|
|
5
|
+
* every line-numbered violation stays correct. A block comment
|
|
6
|
+
* containing a newline is currently collapsed, shifting every later
|
|
7
|
+
* line number up -- which silently mislabels every violation that
|
|
8
|
+
* follows a multi-line comment in the file.
|
|
9
|
+
* @covers AC-620-02
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { TypeScriptAdapter } from '../typescript';
|
|
13
|
+
|
|
14
|
+
vi.mock('fs', () => ({
|
|
15
|
+
readFileSync: vi.fn(),
|
|
16
|
+
existsSync: vi.fn(() => true),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
|
|
21
|
+
function mask(src: string): string {
|
|
22
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
23
|
+
const adapter = new TypeScriptAdapter('probe.ts');
|
|
24
|
+
return (adapter as unknown as { maskedSource: string }).maskedSource;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AC-620-02: a multi-line block comment must not shift later line numbers', () => {
|
|
28
|
+
it('keeps the newline inside a block comment', () => {
|
|
29
|
+
const src = '/* a\nb */\nconst keep = 1;';
|
|
30
|
+
const out = mask(src);
|
|
31
|
+
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps the line count when the block comment spans three lines', () => {
|
|
35
|
+
const src = '/* one\ntwo\nthree */\nconst keep = 1;\nconst also = 2;';
|
|
36
|
+
const out = mask(src);
|
|
37
|
+
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
38
|
+
expect(out).toContain('const keep = 1;');
|
|
39
|
+
expect(out).toContain('const also = 2;');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports a function line number that matches the physical source', () => {
|
|
43
|
+
// The function starts on physical line 4; a collapsed block comment would
|
|
44
|
+
// make the adapter report line 2.
|
|
45
|
+
const src = ['/* a', 'b', 'c */', 'function target() {', ' return 1;', '}'].join('\n');
|
|
46
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
47
|
+
const adapter = new TypeScriptAdapter('probe.ts');
|
|
48
|
+
const fns = adapter.extractFunctions() as Array<{ name: string; line: number }>;
|
|
49
|
+
const target = fns.find((f) => f.name === 'target');
|
|
50
|
+
expect(target).toBeDefined();
|
|
51
|
+
expect(target!.line).toBe(4);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Pin the behaviour of the literal/comment masker before it is
|
|
4
|
+
* refactored. The masker is a four-state character scanner (line
|
|
5
|
+
* comment, block comment, regex literal, string/template) whose output
|
|
6
|
+
* feeds EVERY metric the rule set reads: a masking mistake changes
|
|
7
|
+
* reported nesting depth, function length and body text for the whole
|
|
8
|
+
* adapter. These cases are the safety net for the CCN-36 split, so they
|
|
9
|
+
* assert observable masking outcomes (offsets preserved, literals
|
|
10
|
+
* blanked, structure kept) rather than any internal helper shape.
|
|
11
|
+
* @covers AC-620-01
|
|
12
|
+
*
|
|
13
|
+
* Masking contract (from the function's own docblock):
|
|
14
|
+
* - every comment / string / template / regex literal becomes spaces
|
|
15
|
+
* - offsets and newlines are preserved exactly (length and line count equal)
|
|
16
|
+
* - everything else is left byte-identical
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
19
|
+
import { TypeScriptAdapter } from '../typescript';
|
|
20
|
+
|
|
21
|
+
vi.mock('fs', () => ({
|
|
22
|
+
readFileSync: vi.fn(),
|
|
23
|
+
existsSync: vi.fn(() => true),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from 'fs';
|
|
27
|
+
|
|
28
|
+
/** Mask a source string by round-tripping it through the adapter's parser. */
|
|
29
|
+
function mask(src: string): string {
|
|
30
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
31
|
+
const adapter = new TypeScriptAdapter('probe.ts');
|
|
32
|
+
const ast = adapter.parseAST() as { masked?: string };
|
|
33
|
+
// The adapter exposes the masked source through a getter on the instance.
|
|
34
|
+
const masked = (adapter as unknown as { maskedSource?: string }).maskedSource;
|
|
35
|
+
return masked ?? ast.masked ?? '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('AC-620-01: literal masking keeps offsets and blanks exactly the non-code', () => {
|
|
39
|
+
const cases: Array<[string, string]> = [
|
|
40
|
+
['line comment', 'const a = 1; // hidden\nconst b = 2;'],
|
|
41
|
+
['block comment', 'const a = 1; /* hidden\nstill hidden */ const b = 2;'],
|
|
42
|
+
['single-quoted string', "const s = 'hidden text';"],
|
|
43
|
+
['double-quoted string', 'const s = "hidden text";'],
|
|
44
|
+
['template literal', 'const s = `hidden ${x} text`;'],
|
|
45
|
+
['regex literal', 'const re = /hid+den/g;'],
|
|
46
|
+
['escaped quote in string', "const s = 'it\\'s hidden';"],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const [label, src] of cases) {
|
|
50
|
+
it(`preserves length and line count for a ${label}`, () => {
|
|
51
|
+
const out = mask(src);
|
|
52
|
+
expect(out.length).toBe(src.length);
|
|
53
|
+
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('blanks a line comment but keeps the code before it', () => {
|
|
58
|
+
const out = mask('const a = 1; // hidden');
|
|
59
|
+
expect(out).toContain('const a = 1;');
|
|
60
|
+
expect(out).not.toContain('hidden');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('blanks a block comment across lines without eating the newline', () => {
|
|
64
|
+
const src = '/* a\nb */\nconst keep = 1;';
|
|
65
|
+
const out = mask(src);
|
|
66
|
+
expect(out).not.toContain('a\nb');
|
|
67
|
+
expect(out.split('\n').length).toBe(3);
|
|
68
|
+
expect(out).toContain('const keep = 1;');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('blanks string contents and keeps the code around them', () => {
|
|
72
|
+
const out = mask('const s = "hidden"; const n = 42;');
|
|
73
|
+
expect(out).not.toContain('hidden');
|
|
74
|
+
expect(out).toContain('const s =');
|
|
75
|
+
expect(out).toContain('const n = 42;');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('blanks a template literal including its interpolations', () => {
|
|
79
|
+
const out = mask('const s = `before ${x} after`;');
|
|
80
|
+
expect(out).not.toContain('before');
|
|
81
|
+
expect(out).not.toContain('after');
|
|
82
|
+
expect(out).toContain('const s =');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not treat a division slash as the start of a regex literal', () => {
|
|
86
|
+
// `a / b` is division: the slash must not swallow the rest of the line.
|
|
87
|
+
const out = mask('const r = a / b; const keep = 1;');
|
|
88
|
+
expect(out).toContain('const keep = 1;');
|
|
89
|
+
expect(out).toContain('a / b');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('leaves code with no literals byte-identical', () => {
|
|
93
|
+
const src = 'function f(a, b) {\n return a + b;\n}';
|
|
94
|
+
expect(mask(src)).toBe(src);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { BaseAdapter } from './base';
|
|
2
2
|
import { Adapter } from '../types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Lexical state for the C/C++ block scanner: which comment/quote the scan is
|
|
6
|
+
* currently inside. Braces seen in any of these states are data, not structure.
|
|
7
|
+
*/
|
|
8
|
+
interface CppLexState {
|
|
9
|
+
inString: boolean;
|
|
10
|
+
/** Which quote character opened the string (`'` or `"`). */
|
|
11
|
+
stringChar: string;
|
|
12
|
+
inBlockComment: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
export class CppAdapter extends BaseAdapter implements Adapter {
|
|
5
16
|
detectLanguage(): string {
|
|
6
17
|
const ext = this.filePath.toLowerCase();
|
|
@@ -49,61 +60,102 @@ export class CppAdapter extends BaseAdapter implements Adapter {
|
|
|
49
60
|
return classMatches;
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Report how far the lexer can skip from `i`, updating `lex` as it goes.
|
|
65
|
+
*
|
|
66
|
+
* C and C++ share three constructs the brace tracker must not see into: line
|
|
67
|
+
* comments, block comments, and quoted literals. A `}` inside any of them is
|
|
68
|
+
* data, so counting it would truncate the function body at the wrong place.
|
|
69
|
+
*
|
|
70
|
+
* Extracted from `extractCodeBlock`, which carried all three state machines
|
|
71
|
+
* plus the brace tracking in one CCN-28 body (#620).
|
|
72
|
+
*
|
|
73
|
+
* IMPORTANT: this reports a skip distance and never decides that a character
|
|
74
|
+
* is "consumed". Returning 0 means "the caller must look at this character
|
|
75
|
+
* itself" -- which is what lets a `{` both end a literal-free run and open the
|
|
76
|
+
* block. A `//` or `/*` opener IS skippable (the tracker never wants it), and
|
|
77
|
+
* so are quotes; braces are not.
|
|
78
|
+
*
|
|
79
|
+
* @returns the number of characters to skip, or 0 when the caller must inspect.
|
|
80
|
+
*/
|
|
81
|
+
private lexSkip(i: number, lex: CppLexState): number {
|
|
82
|
+
const char = this.fileContent[i];
|
|
83
|
+
const nextChar = this.fileContent[i + 1] || '';
|
|
84
|
+
|
|
85
|
+
if (lex.inBlockComment) return this.skipBlockCommentBody(i, char, nextChar, lex);
|
|
86
|
+
if (lex.inString) return this.skipStringBody(i, char, lex);
|
|
87
|
+
|
|
88
|
+
if (char === '/' && nextChar === '/') return this.skipLineComment(i);
|
|
89
|
+
if (char === '/' && nextChar === '*') {
|
|
90
|
+
lex.inBlockComment = true;
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"' || char === "'") {
|
|
94
|
+
lex.inString = true;
|
|
95
|
+
lex.stringChar = char;
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Skip a `//` comment through, but not including, the newline. */
|
|
102
|
+
private skipLineComment(start: number): number {
|
|
103
|
+
let i = start;
|
|
104
|
+
while (i < this.fileContent.length && this.fileContent[i] !== '\n') i++;
|
|
105
|
+
return i - start;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private skipBlockCommentBody(
|
|
109
|
+
i: number,
|
|
110
|
+
char: string,
|
|
111
|
+
nextChar: string,
|
|
112
|
+
lex: CppLexState,
|
|
113
|
+
): number {
|
|
114
|
+
if (char === '*' && nextChar === '/') {
|
|
115
|
+
lex.inBlockComment = false;
|
|
116
|
+
return 2;
|
|
117
|
+
}
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private skipStringBody(i: number, char: string, lex: CppLexState): number {
|
|
122
|
+
if (char !== lex.stringChar) return 1;
|
|
123
|
+
// A quote closes the literal only after an EVEN number of backslashes:
|
|
124
|
+
// `"\\"` is a backslash then a terminator, `"\""` is a quote as data.
|
|
125
|
+
let backslashCount = 0;
|
|
126
|
+
let j = i - 1;
|
|
127
|
+
while (j >= 0 && this.fileContent[j] === '\\') {
|
|
128
|
+
backslashCount++;
|
|
129
|
+
j--;
|
|
130
|
+
}
|
|
131
|
+
if (backslashCount % 2 === 0) lex.inString = false;
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
52
135
|
override extractCodeBlock(startPos: number, maxFallback: number = 200): string {
|
|
53
136
|
const state = { inBlock: false, braceCount: 0 };
|
|
137
|
+
const lex: CppLexState = { inString: false, stringChar: '', inBlockComment: false };
|
|
54
138
|
let endPos = startPos;
|
|
55
|
-
let inString = false;
|
|
56
|
-
let stringChar = '';
|
|
57
|
-
let inComment = false;
|
|
58
|
-
|
|
59
|
-
for (let i = startPos; i < this.fileContent.length; i++) {
|
|
60
|
-
const char = this.fileContent[i];
|
|
61
|
-
const nextChar = this.fileContent[i + 1] || '';
|
|
62
|
-
|
|
63
|
-
if (!inString && !inComment) {
|
|
64
|
-
if (char === '/' && nextChar === '/') {
|
|
65
|
-
while (i < this.fileContent.length && this.fileContent[i] !== '\n') {
|
|
66
|
-
i++;
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (char === '/' && nextChar === '*') {
|
|
71
|
-
inComment = true;
|
|
72
|
-
i++;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
} else if (inComment) {
|
|
76
|
-
if (char === '*' && nextChar === '/') {
|
|
77
|
-
inComment = false;
|
|
78
|
-
i++;
|
|
79
|
-
}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!inComment) {
|
|
84
|
-
if (!inString && (char === '"' || char === "'")) {
|
|
85
|
-
inString = true;
|
|
86
|
-
stringChar = char;
|
|
87
|
-
} else if (inString && char === stringChar) {
|
|
88
|
-
let backslashCount = 0;
|
|
89
|
-
let j = i - 1;
|
|
90
|
-
while (j >= 0 && this.fileContent[j] === '\\') {
|
|
91
|
-
backslashCount++;
|
|
92
|
-
j--;
|
|
93
|
-
}
|
|
94
|
-
if (backslashCount % 2 === 0) {
|
|
95
|
-
inString = false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
139
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
for (let i = startPos; i < this.fileContent.length;) {
|
|
141
|
+
// The lexer reports how far to skip, but must NEVER consume a character
|
|
142
|
+
// the brace tracker still has to see. A `{` both ends a literal-free run
|
|
143
|
+
// and opens the block; consuming it here would hide the function's own
|
|
144
|
+
// opening brace and leave every later brace off by one.
|
|
145
|
+
//
|
|
146
|
+
// `lexSkip` returns 0 ONLY for characters outside any literal or comment,
|
|
147
|
+
// so a positive skip already guarantees "not structural". There is
|
|
148
|
+
// deliberately no separate inString/inBlockComment test: it would be dead
|
|
149
|
+
// (verified by deleting it -- no test changes colour).
|
|
150
|
+
const skip = this.lexSkip(i, lex);
|
|
151
|
+
if (skip > 0) { i += skip; continue; }
|
|
152
|
+
|
|
153
|
+
const result = this.advanceBraceTracker(this.fileContent[i], i, state);
|
|
154
|
+
if (result.closed) {
|
|
155
|
+
endPos = result.endPos;
|
|
156
|
+
break;
|
|
106
157
|
}
|
|
158
|
+
i++;
|
|
107
159
|
}
|
|
108
160
|
|
|
109
161
|
if (endPos > startPos) {
|