@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.
Files changed (33) hide show
  1. package/hooks/pre-commit +120 -11
  2. package/lib/mattpocock-license-verdict.json +1 -1
  3. package/mock-policy/AGENTS.md +2 -2
  4. package/mutation/AGENTS.md +2 -2
  5. package/package.json +1 -1
  6. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  7. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  8. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  9. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  10. package/plugins/dsh/package.json +1 -1
  11. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  12. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  13. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  14. package/plugins/opencode/package.json +1 -1
  15. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  16. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  17. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  18. package/plugins/qoder/plugin.json +1 -1
  19. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  20. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  21. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  22. package/principles/AGENTS.md +2 -2
  23. package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
  24. package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
  25. package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
  26. package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
  27. package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
  28. package/principles/adapters/cpp.ts +102 -50
  29. package/principles/adapters/typescript.ts +503 -174
  30. package/principles/baseline.ts +58 -70
  31. package/skills/delphi-review/AGENTS.md +2 -2
  32. package/skills/sprint-flow/AGENTS.md +2 -2
  33. 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
- if (!inString && !inComment) {
101
- const result = this.advanceBraceTracker(char, i, state);
102
- if (result.closed) {
103
- endPos = result.endPos;
104
- break;
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) {