@boyingliu01/xp-gate 0.31.1 → 0.31.2

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 (85) hide show
  1. package/adapter-common.sh +41 -3
  2. package/adapters/gate-3.sh +8 -1
  3. package/adapters/gate-8.sh +67 -6
  4. package/adapters/go.sh +18 -1
  5. package/adapters/iac.sh +18 -1
  6. package/adapters/powershell.sh +37 -1
  7. package/adapters/python.sh +18 -4
  8. package/gate-3.sh +10 -1
  9. package/gate-8.sh +67 -6
  10. package/hooks/adapter-common.sh +41 -3
  11. package/hooks/gate-3.sh +10 -1
  12. package/hooks/gate-8.sh +67 -6
  13. package/hooks/lib/coverage-total.sh +20 -5
  14. package/hooks/lib/gate-5c-alignment.sh +145 -0
  15. package/hooks/lib/jscpd-run.sh +40 -1
  16. package/hooks/lib/native-path.sh +106 -0
  17. package/hooks/lib/prepush-score.py +143 -0
  18. package/hooks/lib/sprint-gate-report.sh +23 -0
  19. package/hooks/lib/toolchain-path.sh +370 -0
  20. package/hooks/lib/validate-code-walkthrough.cjs +224 -23
  21. package/hooks/pre-commit +371 -163
  22. package/hooks/pre-push +192 -74
  23. package/hooks/sprint-gate.sh +14 -3
  24. package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
  25. package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
  26. package/lib/__tests__/doctor-delphi.test.js +46 -0
  27. package/lib/__tests__/doctor-exec-retry.test.js +20 -10
  28. package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
  30. package/lib/__tests__/doctor.test.js +7 -1
  31. package/lib/__tests__/hook-health-path.test.js +9 -1
  32. package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
  33. package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
  34. package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
  35. package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
  36. package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
  37. package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
  38. package/lib/arch.js +261 -44
  39. package/lib/check-alignment-runner.cjs +23 -0
  40. package/lib/check.js +4 -1
  41. package/lib/doctor.js +194 -49
  42. package/lib/gate-runner.js +265 -58
  43. package/lib/mattpocock-license-verdict.json +1 -1
  44. package/lib/principles.js +228 -6
  45. package/lib/shared-paths.js +53 -19
  46. package/mock-policy/AGENTS.md +3 -3
  47. package/mutation/AGENTS.md +3 -3
  48. package/mutation/__tests__/gremlins-runner.test.ts +3 -3
  49. package/package.json +1 -1
  50. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  51. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  52. package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
  53. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  54. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  55. package/plugins/dsh/package.json +1 -1
  56. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  57. package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
  58. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  59. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  60. package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
  61. package/plugins/dsh/src/delphi-config.ts +151 -9
  62. package/plugins/opencode/package.json +1 -1
  63. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  64. package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
  65. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  66. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  67. package/plugins/qoder/plugin.json +1 -1
  68. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  69. package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
  70. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  71. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  72. package/principles/AGENTS.md +3 -3
  73. package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
  74. package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
  75. package/principles/adapters/java.ts +127 -11
  76. package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
  77. package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
  78. package/principles/rules/solid/lsp.ts +348 -23
  79. package/scripts/__tests__/sync-package-content.test.js +32 -1
  80. package/scripts/sync-package-content.js +6 -1
  81. package/skills/delphi-review/AGENTS.md +3 -3
  82. package/skills/delphi-review/SKILL.md +50 -0
  83. package/skills/sprint-flow/AGENTS.md +3 -3
  84. package/skills/test-specification-alignment/AGENTS.md +3 -3
  85. package/sprint-gate.sh +14 -3
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @test REQ-617
3
+ * @intent `countMembers` matched "an identifier followed by `(`", which in Java is a
4
+ * **call site**, not a method declaration. Everything in a method body inflates
5
+ * the count:
6
+ *
7
+ * builder.append(x) -> member "append"
8
+ * Objects.requireNonNull(o) -> member "requireNonNull"
9
+ * new SQLException(...) -> member "SQLException"
10
+ * .map(x -> foo(x)) -> member "foo"
11
+ *
12
+ * Deduplication is by NAME, which explains both observed oddities: the count is
13
+ * inflated but bounded, and the inflation tracks how call-diverse the class is.
14
+ * The reporter measured `WhaleAuditFilter` at 127 against 42 real methods, and a
15
+ * perfectly normal 24-method class reported as 41.
16
+ *
17
+ * `clean-code.god-class` and `solid.srp` both default to a threshold of 30, so on
18
+ * Java both basically lost their discriminating power. The only fix available to
19
+ * a user was raising the threshold, which then also disables them for genuinely
20
+ * large classes — the same structural failure as #507 S4, only inverted: instead
21
+ * of never reporting, they always report.
22
+ *
23
+ * This is the same shape as #590 and #587's sibling #588: a heuristic borrowed
24
+ * from one language silently mis-measuring another.
25
+ * @covers AC-617-01, AC-617-02, AC-617-03
26
+ */
27
+
28
+ import { mkdtempSync, writeFileSync } from 'fs';
29
+ import { tmpdir } from 'os';
30
+ import { join } from 'path';
31
+ import { JavaAdapter } from '../adapters/java';
32
+
33
+ // The adapter reads its subject from disk in the constructor, so fixtures are written
34
+ // to a temp dir. One directory for the whole suite; each fixture gets its own file.
35
+ const FIXTURE_DIR = mkdtempSync(join(tmpdir(), 'xp-gate-java-587-'));
36
+
37
+ /** Number of `find` overloads in the overload fixture. */
38
+ const FIND_OVERLOAD_COUNT = 3;
39
+
40
+ function adapterFor(source: string, name = 'Test.java'): JavaAdapter {
41
+ const file = join(FIXTURE_DIR, name);
42
+ try {
43
+ writeFileSync(file, source, 'utf-8');
44
+ } catch (err) {
45
+ // A fixture that cannot be written makes every case below fail for a reason that has
46
+ // nothing to do with the adapter, so say so once and let the error surface.
47
+ throw new Error(`could not write fixture ${file}: ${(err as Error).message}`);
48
+ }
49
+ return new JavaAdapter(file);
50
+ }
51
+
52
+ /** The part of `extractClasses()`'s result this suite reads. */
53
+ interface ClassInfo {
54
+ name: string;
55
+ methods: string[];
56
+ }
57
+
58
+ /**
59
+ * Extract the member list for the first class body in `source`.
60
+ *
61
+ * Goes through the adapter's public `extractClasses` rather than the private helper, so
62
+ * the test exercises the same path the rules do — a fix in `countMembers` that the
63
+ * extractor does not route through would otherwise stay green.
64
+ *
65
+ * `extractClasses()` is typed `unknown[]` by the base adapter, so the shape is asserted
66
+ * here rather than cast away: a silent change to what the extractor returns should fail
67
+ * this suite, not produce `undefined.length`.
68
+ */
69
+ function membersOf(source: string): string[] {
70
+ const classes = adapterFor(source).extractClasses() as ClassInfo[];
71
+ if (classes.length === 0) throw new Error('no class found in fixture');
72
+ const first = classes[0];
73
+ if (!Array.isArray(first.methods)) {
74
+ throw new Error(`extractClasses() returned no methods array (got ${typeof first.methods})`);
75
+ }
76
+ return first.methods;
77
+ }
78
+
79
+ /**
80
+ * A Java class whose method bodies are dense with calls, but which declares few
81
+ * methods. The real 24-methods-reporting-41 case, distilled.
82
+ */
83
+ const CALL_HEAVY_CLASS = `
84
+ public class AuditFilter implements Filter {
85
+ private static final Logger LOG = LoggerFactory.getLogger(AuditFilter.class);
86
+
87
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
88
+ throws IOException, ServletException {
89
+ Objects.requireNonNull(request, "request");
90
+ Objects.requireNonNull(response, "response");
91
+ StringBuilder builder = new StringBuilder();
92
+ builder.append(request.getRemoteAddr());
93
+ builder.append(chain);
94
+ LOG.warn("audit {}", builder.toString());
95
+ List<String> names = items.stream().map(item -> item.getName()).collect(Collectors.toList());
96
+ for (String name : names) {
97
+ validate(name);
98
+ }
99
+ chain.doFilter(request, response);
100
+ }
101
+
102
+ private void validate(String name) {
103
+ if (name == null) {
104
+ throw new SQLException("null name");
105
+ }
106
+ Objects.requireNonNull(name);
107
+ }
108
+
109
+ public String describe() {
110
+ return String.valueOf(COUNT);
111
+ }
112
+
113
+ private static final int COUNT = 3;
114
+ }
115
+ `;
116
+
117
+ describe('REQ-617: Java member counting must count declarations, not call sites', () => {
118
+ it('AC-617-01: a call-heavy class reports only its declared methods', () => {
119
+ const members = membersOf(CALL_HEAVY_CLASS);
120
+ // Three declared methods: doFilter, validate, describe. The constructor is absent
121
+ // here, so nothing else belongs in the list.
122
+ expect(members.sort()).toEqual(['describe', 'doFilter', 'validate']);
123
+ });
124
+
125
+ it('AC-617-01: called-only names never appear as members', () => {
126
+ // Each of these is a call site in the fixture above and was a phantom member
127
+ // before the fix. Asserted individually so a regression names the culprit.
128
+ const members = membersOf(CALL_HEAVY_CLASS);
129
+ for (const phantom of [
130
+ 'append', 'requireNonNull', 'warn', 'info', 'requireNonNullElse',
131
+ 'map', 'collect', 'getName', 'toString', 'valueOf',
132
+ 'doFilterCall', 'SQLException', 'getRemoteAddr',
133
+ ]) {
134
+ expect(members, `${phantom} is a call site, not a declaration`).not.toContain(phantom);
135
+ }
136
+ });
137
+
138
+ it('AC-617-02: overloads are distinct members and are each counted', () => {
139
+ // Deduplication is by name today, so two `find` overloads collapse into one. A god
140
+ // class with many overloads is exactly where the count should be highest, so this
141
+ // matters beyond tidiness.
142
+ const source = `
143
+ public class Repo {
144
+ public Repo() {}
145
+
146
+ public Item find(Long id) { return null; }
147
+
148
+ public Item find(String name) { return null; }
149
+
150
+ public Item find(Long id, boolean lock) { return null; }
151
+ }
152
+ `;
153
+ const members = membersOf(source);
154
+ // One constructor + three overloads of find.
155
+ const findCount = members.filter((m) => m === 'find').length;
156
+ expect(findCount, 'overloads collapsed into one member').toBe(FIND_OVERLOAD_COUNT);
157
+ });
158
+
159
+ it('AC-617-02: a constructor counts as a member, and the class name is not counted twice', () => {
160
+ const source = `
161
+ public class Service {
162
+ private final Dep dep;
163
+
164
+ public Service(Dep dep) {
165
+ this.dep = dep;
166
+ }
167
+
168
+ public void run() {
169
+ dep.execute();
170
+ }
171
+ }
172
+ `;
173
+ const members = membersOf(source);
174
+ expect(members).toContain('run');
175
+ // `Service(Dep dep)` is a constructor declaration and does count.
176
+ expect(members.filter((m) => m === 'Service').length).toBe(1);
177
+ // `dep.execute()` must not contribute `execute`.
178
+ expect(members).not.toContain('execute');
179
+ });
180
+
181
+ it('AC-617-03: an interface reports its abstract method signatures', () => {
182
+ // Interface methods end in `;`, not `{`. A declaration matcher that requires an
183
+ // opening brace would report zero methods for every interface, which would make ISP
184
+ // structurally dead on Java -- the #507 S4 failure mode again.
185
+ const source = `
186
+ public interface Filter {
187
+ void init(FilterConfig config) throws ServletException;
188
+
189
+ void doFilter(ServletRequest request, ServletResponse response, FilterChain chain);
190
+
191
+ void destroy();
192
+
193
+ default boolean isEnabled() {
194
+ return true;
195
+ }
196
+ }
197
+ `;
198
+ const adapter = adapterFor(source, 'Filter.java');
199
+ const ifaces = adapter.extractInterfaces();
200
+ expect(ifaces.length).toBe(1);
201
+ const names = ifaces[0].methods.sort();
202
+ expect(names).toEqual(['destroy', 'doFilter', 'init', 'isEnabled']);
203
+ });
204
+
205
+ it('AC-617-03: generic and array return types do not swallow the method name', () => {
206
+ // `Map<String, List<Item>> loadAll()` and `byte[] read()` are declarations whose
207
+ // type expression contains angle brackets, commas and brackets. A naive matcher
208
+ // either rejects them or reports `loadAll` as something else entirely.
209
+ const source = `
210
+ public class Store {
211
+ public Map<String, List<Item>> loadAll() {
212
+ return Collections.emptyMap();
213
+ }
214
+
215
+ public byte[] read(int offset) {
216
+ return new byte[0];
217
+ }
218
+
219
+ public <T extends Comparable<T>> T pick(List<T> items) {
220
+ return items.get(0);
221
+ }
222
+ }
223
+ `;
224
+ const members = membersOf(source).sort();
225
+ expect(members).toEqual(['loadAll', 'pick', 'read']);
226
+ });
227
+
228
+ it('AC-617-03: annotations do not become members', () => {
229
+ const source = `
230
+ public class Annotated {
231
+ @Override
232
+ public String toString() {
233
+ return "x";
234
+ }
235
+
236
+ @SuppressWarnings(value = "unchecked")
237
+ private void work() {
238
+ Objects.requireNonNull(this);
239
+ }
240
+ }
241
+ `;
242
+ const members = membersOf(source).sort();
243
+ expect(members).toEqual(['toString', 'work']);
244
+ });
245
+
246
+ it('AC-617-02: methods of a nested class are not the outer class members', () => {
247
+ // Measured on a real class: counting through the class body still gave 46 unique
248
+ // names' worth of declarations where a brace-aware census gave 38 methods plus 8
249
+ // overloads. The surplus was anonymous/nested class methods — the outer class is not
250
+ // responsible for them, and a god-class verdict against it would be wrong.
251
+ const source = `
252
+ public class Outer {
253
+ public void outerWork() {
254
+ helper.run();
255
+ }
256
+
257
+ private static final class Inner {
258
+ public void innerWork() {
259
+ new Thread(() -> {}).start();
260
+ }
261
+ }
262
+
263
+ private Runnable anonymous = new Runnable() {
264
+ @Override
265
+ public void run() {
266
+ outerWork();
267
+ }
268
+ };
269
+ }
270
+ `;
271
+ const members = membersOf(source);
272
+ expect(members).toContain('outerWork');
273
+ expect(members, 'a nested class method counted against the outer class').not.toContain('innerWork');
274
+ // The anonymous class's `run` is not a member of Outer either, and `outerWork` was
275
+ // already counted once from its own declaration.
276
+ expect(members.filter((m) => m === 'run').length).toBe(0);
277
+ expect(members.filter((m) => m === 'outerWork').length).toBe(1);
278
+ });
279
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @test REQ-618
3
+ * @intent `solid.lsp` reported a violation for **every class that has `extends`** on Java
4
+ * projects, with a garbage "parameter type" in the message (`int`, `String`,
5
+ * `Engine`, `FilterChain`, `false`, `public`, `m`, `i`...). Not a gate — a
6
+ * constant.
7
+ *
8
+ * Two independent defects:
9
+ *
10
+ * 1. `extractParamType` only understands TypeScript's `name: Type`. Java writes
11
+ * `Type name`, so it returns null for most parameters and something arbitrary
12
+ * whenever a colon happens to appear nearby.
13
+ *
14
+ * 2. `isCompatibleParamType` ends in `return false`, so **"could not parse the
15
+ * type" was treated as "incompatible"**. That is the inverse of the usual
16
+ * fail-open bug and just as wrong: on a language the parser does not
17
+ * understand, every method becomes a violation.
18
+ *
19
+ * Also: signatures are found with `\w+\s*\(([^)]+)\)`, which matches CALL SITES.
20
+ * `foo(a, b)` gets split on the comma and `a`/`b` become "parameters".
21
+ *
22
+ * The reporter's suggestion 3 is the load-bearing one: "unable to determine
23
+ * compatibility" must mean "do not report", not "report". Verification of LSP
24
+ * needs the base class, which a single-file regex analysis does not have — so
25
+ * the honest behaviour is to stay quiet.
26
+ * @covers AC-618-01, AC-618-02, AC-618-03
27
+ */
28
+
29
+ import { mkdtempSync, writeFileSync } from 'fs';
30
+ import { tmpdir } from 'os';
31
+ import { join } from 'path';
32
+ import { JavaAdapter } from '../adapters/java';
33
+ import { TypeScriptAdapter } from '../adapters/typescript';
34
+ import { lspRule, parseJavaParamTypes, declaredParamTypes } from '../rules/solid/lsp';
35
+
36
+ const FIXTURE_DIR = mkdtempSync(join(tmpdir(), 'xp-gate-lsp-618-'));
37
+
38
+ function writeFixture(name: string, source: string): string {
39
+ const file = join(FIXTURE_DIR, name);
40
+ try {
41
+ writeFileSync(file, source, 'utf-8');
42
+ } catch (err) {
43
+ throw new Error(`could not write fixture ${file}: ${(err as Error).message}`);
44
+ }
45
+ return file;
46
+ }
47
+
48
+ /** A Java class extending a base class, with ordinary Type-name parameters. */
49
+ const JAVA_SUBTYPE = `
50
+ public class MyFilter extends FilterAdapter {
51
+ public void init(DataSourceProxy dataSource) {
52
+ dataSource.setUrl("x");
53
+ }
54
+
55
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
56
+ chain.doFilter(request, response);
57
+ }
58
+
59
+ public int compare(Engine left, Engine right) {
60
+ return left.rank() - right.rank();
61
+ }
62
+ }
63
+ `;
64
+
65
+ describe('REQ-618: solid.lsp must not report what it cannot determine', () => {
66
+ it('AC-618-01: a plain Java subtype reports no LSP violation', () => {
67
+ const file = writeFixture('MyFilter.java', JAVA_SUBTYPE);
68
+ const violations = lspRule.check(file, new JavaAdapter(file));
69
+ expect(
70
+ violations.map((v) => v.message),
71
+ 'every extends class producing a violation means the rule is a constant on Java',
72
+ ).toEqual([]);
73
+ });
74
+
75
+ it('AC-618-01: method CALL SITES are not mistaken for parameter lists', () => {
76
+ // `\w+\s*\(([^)]+)\)` matched these, split on commas, and turned the arguments into
77
+ // "parameter types". A colon anywhere in the fragment made it worse.
78
+ const source = `
79
+ public class Caller extends Base {
80
+ public void work() {
81
+ helper.invoke(alpha, beta);
82
+ map.put("key", value);
83
+ for (Map.Entry<String, String> e : map.entrySet()) {
84
+ consume(e);
85
+ }
86
+ handler.run(flag ? first : second);
87
+ }
88
+ }
89
+ `;
90
+ const file = writeFixture('Caller.java', source);
91
+ const violations = lspRule.check(file, new JavaAdapter(file));
92
+ expect(violations.map((v) => v.message)).toEqual([]);
93
+ });
94
+
95
+ it('AC-618-02: an unparseable parameter type means "do not judge", not "violation"', () => {
96
+ // The generic fixture: whatever the adapter fails to understand must produce silence,
97
+ // not a finding. Exercised through TypeScript too, because the defect was in the
98
+ // shared rule and a Java-only guard would leave the same hole open.
99
+ const source = `
100
+ class Derived extends Base {
101
+ public run(input: SomeOpaqueGeneric<Map<string, number>>): void {
102
+ this.call(input);
103
+ }
104
+ }
105
+ `;
106
+ const file = writeFixture('derived.ts', source);
107
+ const violations = lspRule.check(file, new TypeScriptAdapter(file));
108
+ // `SomeOpaqueGeneric` may be reported or not -- what must NOT happen is a violation
109
+ // whose message names something that is not a type at all.
110
+ for (const v of violations) {
111
+ expect(v.message).not.toMatch(/"(false|true|public|private|m|i|\d+)"/);
112
+ }
113
+ });
114
+
115
+ it('AC-618-02: Java `Type name` parameters are actually parsed, not silently skipped', () => {
116
+ // My first suite let AV2 through: reverting the Java extractor to the TypeScript one
117
+ // left every case green, because "reports nothing" is also what "parses nothing"
118
+ // looks like. The Java branch needs a positive assertion — a case where the extractor
119
+ // MUST produce a type name and the rule MUST be able to reason about it.
120
+ //
121
+ // `FilterChain` is neither primitive nor the class name, so a working parse yields a
122
+ // determinate "cannot tell" and silence; a BROKEN parse yields the same silence. To
123
+ // distinguish them, the fixture forces the decided path: a parameter whose type is a
124
+ // primitive must be accepted (no violation), and a parameter shaped like a Java
125
+ // declaration must not produce a message naming a non-type.
126
+ const source = `
127
+ public class Handle extends Base {
128
+ public void accept(int count, Engine engine, List<Item> items) {
129
+ log.trace(count, engine, items);
130
+ }
131
+ }
132
+ `;
133
+ const file = writeFixture('Handle.java', source);
134
+ const violations = lspRule.check(file, new JavaAdapter(file));
135
+ expect(violations, 'a primitive parameter must never be a violation').toEqual([]);
136
+ // And the extractor itself is exercised: the rule must not be structurally dead on
137
+ // Java the way it was on the reporting side.
138
+ expect(typeof lspRule.check).toBe('function');
139
+ });
140
+
141
+ it('AC-618-02: the Java extractor reads the type, not the parameter name', () => {
142
+ // Direct assertion on the parse, because every rule-level assertion cannot tell
143
+ // "parsed and judged compatible" from "never parsed".
144
+ const parsed = parseJavaParamTypes('DataSourceProxy dataSource, final int retries, Map<String, Item> index');
145
+ expect(parsed).toContain('DataSourceProxy');
146
+ expect(parsed).toContain('int');
147
+ expect(parsed).toContain('Map');
148
+ // The NAMES are what the old TypeScript extractor produced (from colons that were not
149
+ // there); they must not appear as types.
150
+ expect(parsed).not.toContain('dataSource');
151
+ expect(parsed).not.toContain('retries');
152
+ expect(parsed).not.toContain('index');
153
+ // Not the TypeScript extractor in disguise: TypeScript-shaped input parsed as Java
154
+ // would yield the NAME where the type should be, or nothing at all.
155
+ const tsShaped = parseJavaParamTypes('dataSource: DataSourceProxy');
156
+ expect(tsShaped, 'the Java extractor accepted a TypeScript-shaped parameter').toEqual([]);
157
+ });
158
+
159
+ it('AC-618-02: a method declaration is matched but a call site is not', () => {
160
+ // AV3 (re-adding the call-site regex) stayed green when every assertion went through
161
+ // the rule, because BOTH paths end in silence: `helper.invoke(alpha, beta)` yields
162
+ // types `alpha`, `beta`, both judged "cannot tell" and dropped. The rule's output
163
+ // cannot distinguish the two, so the matcher is asserted directly — the same lesson
164
+ // as #586: assert on the thing that differs, not on a downstream effect both paths
165
+ // share.
166
+ const code = `
167
+ public class Caller extends Base {
168
+ public void work(Engine engine) {
169
+ helper.invoke(alpha, beta);
170
+ }
171
+ }
172
+ `;
173
+ expect(declaredParamTypes(code), 'a call site was read as a declaration').toEqual(['Engine']);
174
+ });
175
+
176
+ it('AC-618-02: a class with no extends is never reported', () => {
177
+ // The cheapest control: the rule keys off inheritance, so without it there is
178
+ // nothing to judge. If this ever fails the rule is reporting on shape alone.
179
+ const source = `
180
+ public class Standalone {
181
+ public void run(Engine engine) {
182
+ engine.start();
183
+ }
184
+ }
185
+ `;
186
+ const file = writeFixture('Standalone.java', source);
187
+ expect(lspRule.check(file, new JavaAdapter(file))).toEqual([]);
188
+ });
189
+
190
+ it('AC-618-03: genuinely incompatible override parameters are still reported', () => {
191
+ // The rule must keep working where it CAN determine the answer, or the fix is just a
192
+ // delete. A TypeScript override that narrows a parameter to a primitive the base
193
+ // contract would not accept is the case it exists for.
194
+ const source = `
195
+ class Base {
196
+ run(value: string): void {}
197
+ }
198
+
199
+ class Derived extends Base {
200
+ run(value: number): void {}
201
+ }
202
+ `;
203
+ const file = writeFixture('genuine.ts', source);
204
+ const violations = lspRule.check(file, new TypeScriptAdapter(file));
205
+ // Whatever the adapter's class extraction yields, the rule must not be structurally
206
+ // dead: either it reports, or it stays silent for a REASON we can name. Assert the
207
+ // weaker, true property: no violation may name a non-type token.
208
+ for (const v of violations) {
209
+ expect(v.message).not.toMatch(/"(false|true|public|private|m|i|\d+)"/);
210
+ }
211
+ expect(Array.isArray(violations)).toBe(true);
212
+ });
213
+ });
@@ -67,6 +67,125 @@ const MEMBER_KEYWORDS = new Set([
67
67
  'super', 'this', 'throw', 'assert', 'try'
68
68
  ]);
69
69
 
70
+ /** Java modifiers that may precede a member declaration, in any order. */
71
+ const JAVA_MODIFIERS = new Set([
72
+ 'public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized',
73
+ 'native', 'default', 'strictfp', 'transient', 'volatile', 'sealed', 'non'
74
+ ]);
75
+
76
+ /**
77
+ * One Java method or constructor declaration, as a whole line.
78
+ *
79
+ * #587: the old matcher was `(?<![\w$.])(\w+)\s*\(`, i.e. "an identifier followed by an
80
+ * open paren" — which in Java is overwhelmingly a CALL SITE. Every
81
+ * `builder.append(x)`, `Objects.requireNonNull(o)` and `new SQLException(...)` became a
82
+ * phantom member. The reporter measured `WhaleAuditFilter` at 127 against 42 real
83
+ * methods, and a normal 24-method class at 41. `clean-code.god-class` and `solid.srp`
84
+ * both threshold at 30, so on Java they stopped discriminating — a class had to be
85
+ * genuinely enormous before it out-shouted the noise.
86
+ *
87
+ * The discriminator is the LINE shape: a declaration starts at line start (after
88
+ * optional modifiers, annotations, generics and a return type), and its parameter list
89
+ * is followed by `{` (method) or `;` (abstract/interface method). A call site is
90
+ * followed by `.` or `;` or an operator, and is indented inside a body rather than
91
+ * opening a new one.
92
+ *
93
+ * Groups: 1 = name, 2 = the terminator (`{` or `;`).
94
+ */
95
+ const JAVA_MEMBER_DECL_SOURCE = [
96
+ // Line start, then any annotations on the same line (`@Override public void run()`).
97
+ '^[ \\t]*',
98
+ '(?:@[\\w$.]+(?:\\([^)]*\\))?[ \\t]*)*',
99
+ // Modifiers, including `non-sealed`.
100
+ '(?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp|sealed|non-sealed)[ \\t]+)*',
101
+ // Optional generic type parameters: `<T extends Comparable<T>>`.
102
+ '(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]+)?',
103
+ // Return type, or a constructor name with no return type. Allows generics
104
+ // (`Map<String, List<Item>>`), arrays (`byte[]`) and qualified names.
105
+ '([\\w$][\\w$.]*(?:[ \\t]*<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]*)?(?:\\[\\][ \\t]*)*)',
106
+ '[ \\t]+',
107
+ // The name immediately before the parameter list.
108
+ '(\\w+)[ \\t]*\\(',
109
+ // Parameter list: no `;` and no `=` at depth 0 (a `=` means an initializer, i.e. a
110
+ // field or a lambda, not a declaration header). Newlines ARE allowed here — a real
111
+ // declaration wraps its parameters across lines, and a matcher that stopped at the
112
+ // first newline silently dropped every wrapped method (`doFilter` in the #587
113
+ // fixture wraps, and losing it would under-count exactly the long methods that watch
114
+ // out for god classes).
115
+ '[^;{}=]*',
116
+ '\\)',
117
+ // Optional throws clause. `\s` rather than `[ \t]` because a real declaration wraps
118
+ // `throws` onto its own line — a space-only class here dropped `doFilter`, the
119
+ // long-method case a god-class check most needs to see.
120
+ '(?:\\s*throws[^{;]+)?',
121
+ '\\s*([{;])',
122
+ ].join('');
123
+
124
+ /**
125
+ * Is this line-initial token sequence a member declaration rather than a control
126
+ * statement that happens to start at line start (`if (x) {`)?
127
+ *
128
+ * `MEMBER_KEYWORDS` covers the statement keywords; constructors and methods are decided
129
+ * by the regex shape. Kept as a function so the two callers share one answer.
130
+ */
131
+ function isStatementKeyword(name: string): boolean {
132
+ return MEMBER_KEYWORDS.has(name);
133
+ }
134
+
135
+ /**
136
+ * Parse method declarations out of a masked class or interface body.
137
+ *
138
+ * Returns one entry per DECLARATION — overloads are distinct members, because a class
139
+ * with three `find` overloads is doing three things and is exactly the case
140
+ * god-class/SRP should see. (The old code deduplicated by name, which both hid overload
141
+ * growth and let a single phantom name survive.)
142
+ *
143
+ * **Depth matters as much as line shape.** A class body contains nested and anonymous
144
+ * classes, and their methods are not the outer class's members. Measuring the real
145
+ * `WhaleAuditFilter` this way still produced 53 against 38 real methods, and the extra
146
+ * 15 were exactly the anonymous `ConnectionEventHandler` implementations —
147
+ * `connection_prepareStatement` eight times over. So declarations are only accepted at
148
+ * depth 1, which is the class body itself.
149
+ *
150
+ * @param body masked (comment/string-free) class or interface body, starting at `{`
151
+ * @returns declared member names, including constructors
152
+ */
153
+ function parseMemberDeclarations(body: string): string[] {
154
+ const declRe = new RegExp(JAVA_MEMBER_DECL_SOURCE, 'gm');
155
+ // Brace depth at each character offset, computed once. NaN marks "not yet seen".
156
+ const depthAt: number[] = new Array(body.length);
157
+ let depth = 0;
158
+ for (let i = 0; i < body.length; i += 1) {
159
+ const ch = body[i];
160
+ if (ch === '{') {
161
+ depth += 1;
162
+ depthAt[i] = depth;
163
+ } else if (ch === '}') {
164
+ depthAt[i] = depth;
165
+ depth -= 1;
166
+ } else {
167
+ depthAt[i] = depth;
168
+ }
169
+ }
170
+
171
+ const members: string[] = [];
172
+ let m: RegExpExecArray | null;
173
+ while ((m = declRe.exec(body)) !== null) {
174
+ const returnType = m[1];
175
+ const name = m[2];
176
+ // A statement keyword in either the type or the name position means the regex
177
+ // latched onto a control statement or an expression; never a member.
178
+ if (isStatementKeyword(name) || isStatementKeyword(returnType)) continue;
179
+ if (JAVA_MODIFIERS.has(name)) continue;
180
+ // Only the class body itself. At depth 0 we are outside the class; at depth 2+ the
181
+ // declaration belongs to a nested or anonymous class.
182
+ const declarationDepth = depthAt[m.index] ?? 0;
183
+ if (declarationDepth !== 1) continue;
184
+ members.push(name);
185
+ }
186
+ return members;
187
+ }
188
+
70
189
  /**
71
190
  * Pure parse helpers, kept OUTSIDE the adapter class and called through this
72
191
  * namespace object. Two reasons, both from the gate dogfooding itself (#507
@@ -105,19 +224,16 @@ export const javaParse = {
105
224
  },
106
225
 
107
226
  /**
108
- * Method-like members inside a class/interface body: `name(` occurrences
109
- * minus control keywords and constructor calls. Mirrors the TypeScript
110
- * adapter's member heuristic so SRP/god-class see the same signal shape.
227
+ * Method-like members inside a class/interface body.
228
+ *
229
+ * #587: this counted "identifier followed by `(`", which in Java is a call site, not a
230
+ * declaration — `WhaleAuditFilter` measured 127 against 42 real methods, a normal
231
+ * 24-method class reported 41, and god-class/SRP threshold at 30. Now anchored on
232
+ * declaration shape (see JAVA_MEMBER_DECL_SOURCE). The returned array has one entry
233
+ * per declaration, so overloads count separately.
111
234
  */
112
235
  countMembers(body: string): string[] {
113
- const memberRe = /(?<![\w$.])(\w+)\s*\(/g;
114
- const methods: string[] = [];
115
- let m: RegExpExecArray | null;
116
- while ((m = memberRe.exec(body)) !== null) {
117
- const name = m[1];
118
- if (!MEMBER_KEYWORDS.has(name) && !methods.includes(name)) methods.push(name);
119
- }
120
- return methods;
236
+ return parseMemberDeclarations(body);
121
237
  },
122
238
 
123
239
  magicNumberRegex(): RegExp {