@boyingliu01/xp-gate 0.31.2 → 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 (36) hide show
  1. package/gate-3.sh +244 -9
  2. package/hooks/gate-3.sh +244 -9
  3. package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
  4. package/hooks/pre-commit +152 -13
  5. package/lib/mattpocock-license-verdict.json +1 -1
  6. package/mock-policy/AGENTS.md +3 -3
  7. package/mutation/AGENTS.md +3 -3
  8. package/package.json +1 -1
  9. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  11. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  12. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  13. package/plugins/dsh/package.json +1 -1
  14. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  15. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  16. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  17. package/plugins/opencode/package.json +1 -1
  18. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  19. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  20. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  21. package/plugins/qoder/plugin.json +1 -1
  22. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  23. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  24. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  25. package/principles/AGENTS.md +3 -3
  26. package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
  27. package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
  28. package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
  29. package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
  30. package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
  31. package/principles/adapters/cpp.ts +102 -50
  32. package/principles/adapters/typescript.ts +503 -174
  33. package/principles/baseline.ts +58 -70
  34. package/skills/delphi-review/AGENTS.md +3 -3
  35. package/skills/sprint-flow/AGENTS.md +3 -3
  36. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-25
4
- **Commit:** 1ab53eff
3
+ **Generated:** 2026-10-26
4
+ **Commit:** dfe3bf53
5
5
  **Branch:** main
6
- **Version:** 0.31.2.0
6
+ **Version:** 0.31.4.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-25
4
- **Commit:** 1ab53eff
3
+ **Generated:** 2026-10-26
4
+ **Commit:** dfe3bf53
5
5
  **Branch:** main
6
- **Version:** 0.31.2.0
6
+ **Version:** 0.31.4.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
- **Generated:** 2026-10-25
4
- **Commit:** 1ab53eff
3
+ **Generated:** 2026-10-26
4
+ **Commit:** dfe3bf53
5
5
  **Branch:** main
6
- **Version:** 0.31.2.0
6
+ **Version:** 0.31.4.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @test REQ-620
3
+ * @intent Pin the single behavioural difference introduced when the two copies
4
+ * of the per-tool accumulator were merged into buildEntry:
5
+ * `createFromFiles` tested `totalWarnings` for TRUTHINESS, so a caller
6
+ * passing an explicit `0` had it replaced by the sum of the per-tool
7
+ * counts. That is exactly the case where the caller is overriding a
8
+ * stale per-tool number, so the override was silently discarded.
9
+ *
10
+ * `initializeWithAnalyzer` already used `!== undefined`, so the two
11
+ * entry points disagreed about the same input. The merged version uses
12
+ * the `!== undefined` reading for both.
13
+ * @covers AC-620-04
14
+ */
15
+ import { describe, it, expect } from 'vitest';
16
+ import { BaselineStorage } from '../baseline';
17
+
18
+ function storage(): InstanceType<typeof BaselineStorage> {
19
+ // Explicit config: a timeout far above any test's runtime.
20
+ return new BaselineStorage({ maxSize: 100, timeoutMs: 10000, batchSize: 5 });
21
+ }
22
+
23
+ describe('AC-620-04: an explicit totalWarnings wins, including zero', () => {
24
+ it('uses an explicit zero instead of the summed per-tool count', async () => {
25
+ const baseline = await storage().createFromFiles([
26
+ { file: 'a.ts', counts: { eslint: { warnings: 3, errors: 0 }, totalWarnings: 0 } },
27
+ ]);
28
+ expect(baseline['a.ts'].totalWarnings).toBe(0);
29
+ });
30
+
31
+ it('uses an explicit nonzero value instead of the summed count', async () => {
32
+ const baseline = await storage().createFromFiles([
33
+ { file: 'a.ts', counts: { eslint: { warnings: 3, errors: 0 }, totalWarnings: 42 } },
34
+ ]);
35
+ expect(baseline['a.ts'].totalWarnings).toBe(42);
36
+ });
37
+
38
+ it('sums the per-tool counts when no explicit total is given', async () => {
39
+ const baseline = await storage().createFromFiles([
40
+ {
41
+ file: 'a.ts',
42
+ counts: {
43
+ eslint: { warnings: 3, errors: 0 },
44
+ ruff: { warnings: 2, errors: 1 },
45
+ },
46
+ },
47
+ ]);
48
+ expect(baseline['a.ts'].totalWarnings).toBe(5);
49
+ });
50
+
51
+ it('keeps every per-tool count it was given', async () => {
52
+ const baseline = await storage().createFromFiles([
53
+ {
54
+ file: 'a.ts',
55
+ counts: {
56
+ eslint: { warnings: 1, errors: 2 },
57
+ ccn: { warnings: 3, max: 9 },
58
+ shellcheck: { warnings: 4, errors: 5 },
59
+ },
60
+ },
61
+ ]);
62
+ const entry = baseline['a.ts'];
63
+ expect(entry.eslint).toEqual({ warnings: 1, errors: 2 });
64
+ expect(entry.ccn).toEqual({ warnings: 3, max: 9 });
65
+ expect(entry.shellcheck).toEqual({ warnings: 4, errors: 5 });
66
+ // Tools that were not supplied must not appear at all.
67
+ expect(entry.ruff).toBeUndefined();
68
+ expect(entry.golangci).toBeUndefined();
69
+ });
70
+
71
+ it('reports zero warnings for a file with no tool counts', async () => {
72
+ const baseline = await storage().createFromFiles([{ file: 'a.ts', counts: {} }]);
73
+ expect(baseline['a.ts'].totalWarnings).toBe(0);
74
+ expect(baseline['a.ts'].lastAnalyzed).toBeTypeOf('string');
75
+ });
76
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @test REQ-620
3
+ * @intent Pin the C/C++ code-block scanner's treatment of comments and string
4
+ * literals before its CCN-28 body is split.
5
+ *
6
+ * Every case is built to DEFEAT the fallback path. `extractCodeBlock`
7
+ * falls back to `substring(startPos)` capped at 200 characters, so a
8
+ * short fixture returns "the whole thing" even when the scanner never
9
+ * found a closing brace -- the assertions then pass on a broken scanner.
10
+ * Each source below therefore pads past 200 characters and asserts the
11
+ * exact end position, so a truncated scan cannot look correct.
12
+ *
13
+ * Verified by injecting three real defects (brace tracker seeing into
14
+ * strings, block comments untracked, backslash parity ignored): each one
15
+ * reddens at least one case here.
16
+ * @covers AC-620-03
17
+ */
18
+ import { describe, it, expect, vi } from 'vitest';
19
+ import { CppAdapter } from '../cpp';
20
+
21
+ vi.mock('fs', () => ({
22
+ readFileSync: vi.fn(),
23
+ existsSync: vi.fn(() => true),
24
+ }));
25
+
26
+ import { readFileSync } from 'fs';
27
+
28
+ const MARKER = 'END_OF_FUNCTION';
29
+ /** Padding that keeps the source well past the 200-char fallback cap. */
30
+ const PAD = ' '.repeat(240);
31
+
32
+ function block(src: string, start = 0): string {
33
+ (readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
34
+ const adapter = new CppAdapter('probe.cpp');
35
+ return adapter.extractCodeBlock(start);
36
+ }
37
+
38
+ /**
39
+ * Build `void f() { <body> }` followed by `<MARKER>`, padded past the fallback
40
+ * cap. The padding sits INSIDE the function body so the fallback (which runs to
41
+ * the 200-char cap) would include the marker, while a correct scan stops at the
42
+ * real closing brace and does not.
43
+ */
44
+ function wrap(body: string): { src: string; open: number } {
45
+ const src = `void f() {\n${body}\n${PAD}\n}${MARKER}`;
46
+ return { src, open: src.indexOf('{') };
47
+ }
48
+
49
+ describe('AC-620-03: the C/C++ block scanner ignores braces in comments and strings', () => {
50
+ it('returns exactly the balanced body of a plain function', () => {
51
+ const { src, open } = wrap(' int a = 1;');
52
+ const out = block(src, open);
53
+ expect(out, `tail=${JSON.stringify(out.slice(-60))} len=${out.length}`).toMatch(/}$/);
54
+ });
55
+
56
+ it('does not close on a brace inside a line comment', () => {
57
+ const { src, open } = wrap(' // } not the end\n int a = 1;');
58
+ const out = block(src, open);
59
+ expect(out, `extracted: ${JSON.stringify(out.slice(0, 80))}`).toContain('int a = 1;');
60
+ expect(out).not.toContain(MARKER);
61
+ });
62
+
63
+ it('does not close on a brace inside a block comment', () => {
64
+ const { src, open } = wrap(' /* } not the end */\n int a = 1;');
65
+ const out = block(src, open);
66
+ expect(out).toContain('int a = 1;');
67
+ expect(out).not.toContain(MARKER);
68
+ });
69
+
70
+ it('does not close on a brace inside a string literal', () => {
71
+ const { src, open } = wrap(' const char* s = "}";\n int a = 1;');
72
+ const out = block(src, open);
73
+ expect(out).toContain('int a = 1;');
74
+ expect(out).not.toContain(MARKER);
75
+ });
76
+
77
+ it('keeps scanning past a string that CONTAINS the only early close brace', () => {
78
+ // The string's `}` is the first `}` after the opener. A scanner that counts
79
+ // it ends the block immediately and never reaches the statements below.
80
+ const { src, open } = wrap(' const char* s = "}";\n int markerStatement = 1;\n int tail = 2;');
81
+ const out = block(src, open);
82
+ expect(out, `extracted: ${JSON.stringify(out.slice(0, 120))}`)
83
+ .toContain('int tail = 2;');
84
+ });
85
+
86
+ it('keeps scanning past a char literal that contains a close brace', () => {
87
+ const { src, open } = wrap(" char c = '}';\n int tail = 2;");
88
+ const out = block(src, open);
89
+ expect(out).toContain('int tail = 2;');
90
+ });
91
+
92
+ it('does not close on a brace inside a char literal', () => {
93
+ const { src, open } = wrap(" char c = '}';\n int a = 1;");
94
+ const out = block(src, open);
95
+ expect(out).toContain('int a = 1;');
96
+ expect(out).not.toContain(MARKER);
97
+ });
98
+
99
+ it('treats an escaped quote inside a string as data, not a terminator', () => {
100
+ // `\"` does not close the string, so the `}` after it is still string data.
101
+ const { src, open } = wrap(' const char* s = "a\\"}b";\n int a = 1;');
102
+ const out = block(src, open);
103
+ expect(out).toContain('int a = 1;');
104
+ expect(out).not.toContain(MARKER);
105
+ });
106
+
107
+ it('treats an even run of backslashes as NOT escaping the quote', () => {
108
+ // `\\` is a literal backslash, so the following `"` DOES close the string,
109
+ // and the `}` that ends the line is real structure.
110
+ const { src, open } = wrap(' const char* s = "a\\\\";\n int a = 1;');
111
+ const out = block(src, open);
112
+ expect(out.trimEnd().endsWith('}')).toBe(true);
113
+ expect(out).toContain('int a = 1;');
114
+ expect(out).not.toContain(MARKER);
115
+ });
116
+
117
+ it('falls back when no balanced block is ever found', () => {
118
+ const src = 'void f() {\n int a = 1;' + PAD;
119
+ const out = block(src, 9);
120
+ expect(out.length).toBe(200);
121
+ expect(out.length).toBeLessThan(src.length);
122
+ });
123
+ });
@@ -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) {