@boyingliu01/xp-gate 0.31.0 → 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 +110 -7
  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
@@ -1,8 +1,16 @@
1
1
  import { Rule, Violation, Adapter, Severity } from '../../types';
2
2
  import { getActiveConfig } from '../../config';
3
3
 
4
+ /**
5
+ * Primitive type names whose compatibility with any base contract can be assumed.
6
+ *
7
+ * Covers TypeScript and Java/Kotlin spellings: a `string` or an `int` parameter is never
8
+ * the kind of narrowing this rule looks for.
9
+ */
4
10
  const PRIMITIVE_TYPES = new Set([
5
- 'string', 'number', 'boolean', 'any', 'void', 'null', 'undefined', 'Object', 'Array',
11
+ 'string', 'number', 'boolean', 'any', 'void', 'null', 'undefined', 'unknown', 'Object',
12
+ 'Array', 'int', 'long', 'short', 'byte', 'char', 'float', 'double', 'boolean', 'String',
13
+ 'Integer', 'Long', 'Short', 'Byte', 'Character', 'Float', 'Double', 'Boolean', 'Object',
6
14
  ]);
7
15
 
8
16
  interface ClassInfo {
@@ -11,30 +19,198 @@ interface ClassInfo {
11
19
  name?: string;
12
20
  }
13
21
 
14
- function extractParamType(param: string): string | null {
15
- if (!param.includes(':')) return null;
16
- const typeMatch = param.match(/:\s*(\w+)/);
17
- return typeMatch ? typeMatch[1] : null;
22
+ /**
23
+ * Tokens that are never a type: keywords, control-flow words and single-letter locals.
24
+ *
25
+ * #588: the message text used to be able to say `Parameter type "false"` or
26
+ * `Parameter type "public"`, which is the signature of the old matcher having read a
27
+ * call site or an expression fragment. Filtering them here means a parse that goes wrong
28
+ * produces no finding instead of a nonsense one.
29
+ */
30
+ const NOT_A_TYPE = new Set([
31
+ 'false', 'true', 'null', 'undefined', 'this', 'super', 'new', 'return', 'if', 'for',
32
+ 'while', 'switch', 'catch', 'public', 'private', 'protected', 'static', 'final',
33
+ 'abstract', 'synchronized', 'native', 'default', 'class', 'interface', 'enum', 'record',
34
+ ]);
35
+
36
+ /** Parameter type from a TypeScript-style `name: Type` fragment, or null. */
37
+ function extractTsParamType(param: string): string | null {
38
+ const typeMatch = param.match(/:\s*([\w$<>[\].]+)/);
39
+ if (!typeMatch) return null;
40
+ return normaliseTypeName(typeMatch[1]);
41
+ }
42
+
43
+ /** Reduce `Map<String, List<Item>>[]` / `Foo?` to a bare type name, or null. */
44
+ function normaliseTypeName(raw: string): string | null {
45
+ const stripped = raw.replace(/<.*$/, '').replace(/\[\]$/, '').replace(/\?$/, '').trim();
46
+ if (!stripped) return null;
47
+ // Only the last dotted segment matters for compatibility: `java.util.List` -> `List`.
48
+ const last = stripped.split('.').filter(Boolean).pop();
49
+ if (!last) return null;
50
+ if (!/^[\w$]+$/.test(last)) return null;
51
+ if (NOT_A_TYPE.has(last)) return null;
52
+ return last;
53
+ }
54
+
55
+ /**
56
+ * Parameter type from a Java/Kotlin-style `Type name` fragment, or null.
57
+ *
58
+ * Java puts the type first and has no colon, which is why the TypeScript-only extractor
59
+ * returned null for virtually every Java parameter — and the rule's `return false`
60
+ * default then read that null as "incompatible", flagging every `extends` class.
61
+ */
62
+ function extractJavaParamType(param: string): string | null {
63
+ // `final Foo bar`, `@NotNull Foo bar`, `Foo... bar`, `Map<String, X> bar`
64
+ const cleaned = param
65
+ .replace(/@\w+(\([^)]*\))?/g, ' ')
66
+ .replace(/\bfinal\b/g, ' ')
67
+ .replace(/\.\.\./g, ' ')
68
+ .trim();
69
+ if (!cleaned) return null;
70
+ // Everything except the trailing identifier is the type.
71
+ const m = cleaned.match(/^(.+?)\s+[\w$]+$/);
72
+ if (!m) return null;
73
+ return normaliseTypeName(m[1]);
74
+ }
75
+
76
+ function isPrimitive(paramType: string): boolean {
77
+ return PRIMITIVE_TYPES.has(paramType);
78
+ }
79
+
80
+ /**
81
+ * Parameter type names declared by the base class in the same file, keyed by method name.
82
+ *
83
+ * #588's core problem is that LSP cannot be judged without the base contract. A
84
+ * single-file analysis usually lacks it — but when the base class IS in the file (the
85
+ * common shape for these projects, and the only shape a per-file rule can honestly
86
+ * address), the contract is right there. Extracting it is what lets the rule keep a
87
+ * discriminating power instead of degenerating into unconditional silence.
88
+ *
89
+ * Returns an empty map when the base class is not analysable, which the caller reads as
90
+ * "cannot determine" rather than "no violations".
91
+ *
92
+ * @param code the derived class body, which names its base in the `extends` clause
93
+ * @param allClasses every class the adapter found in this file
94
+ * @returns method name -> parameter type names, for the base class only
95
+ */
96
+ function baseContract(
97
+ code: string,
98
+ allClasses: ClassInfo[],
99
+ isJava: boolean,
100
+ ): Map<string, string[]> {
101
+ const contract = new Map<string, string[]>();
102
+ const extendsMatch = code.match(/\bextends\s+([\w$.]+)/);
103
+ if (!extendsMatch) return contract;
104
+ const baseName = extendsMatch[1].split('.').pop() ?? extendsMatch[1];
105
+ const base = allClasses.find((c) => c.name === baseName && typeof c.code === 'string');
106
+ if (!base || !base.code) return contract;
107
+ const declRe = new RegExp(declarationMatcher(isJava).source, 'g');
108
+ let m: RegExpExecArray | null;
109
+ while ((m = declRe.exec(base.code)) !== null) {
110
+ contract.set(m[1], extractParamTypes(m[2], isJava));
111
+ }
112
+ return contract;
18
113
  }
19
114
 
20
- function isCompatibleParamType(paramType: string, className: string | undefined): boolean {
21
- if (PRIMITIVE_TYPES.has(paramType)) return true;
115
+ /**
116
+ * Can this parameter type be declared incompatible with the base class contract?
117
+ *
118
+ * Tri-state, and that is the whole point: `true` = compatible, `false` = incompatible,
119
+ * `null` = **cannot tell**. A single-file regex analysis does not always have the base
120
+ * class, so it often cannot tell — and the honest answer to "cannot tell" is silence.
121
+ *
122
+ * #588: the previous version was `return false` for anything it did not recognise, which
123
+ * turned a parse failure into a verdict. On Java the parse failed for nearly every
124
+ * parameter, so every class with `extends` was reported.
125
+ *
126
+ * When the base method IS known, a parameter type that the base also declares is
127
+ * compatible and one it does not is reported. That is a real comparison, not a guess:
128
+ * both sides come from declarations in the same file.
129
+ *
130
+ * @param paramType the derived method's parameter type
131
+ * @param className the derived class name
132
+ * @param baseParamTypes the base method's parameter types, or undefined when unavailable
133
+ * @returns true/false when decidable, null when not
134
+ */
135
+ function compatibilityVerdict(
136
+ paramType: string,
137
+ className: string | undefined,
138
+ baseParamTypes?: string[],
139
+ ): boolean | null {
140
+ if (isPrimitive(paramType)) return true;
22
141
  if (className && paramType.startsWith(className)) return true;
23
- return false;
142
+ if (baseParamTypes && baseParamTypes.length > 0) {
143
+ // A type the base method also accepts is compatible; one it does not is the LSP
144
+ // violation this rule exists to find. Compared by simple name with generics erased,
145
+ // which is the precision a regex analysis can honestly claim.
146
+ return baseParamTypes.includes(paramType);
147
+ }
148
+ // An unresolved generic, a lambda's inferred type, or a base we could not find: not
149
+ // enough information to judge.
150
+ return null;
151
+ }
152
+
153
+ /**
154
+ * Build the "I could not determine this" outcome, which is nothing at all.
155
+ *
156
+ * Kept as a named function so the rule's silence is a decision with a documented reason
157
+ * rather than an accident of a filter chain.
158
+ */
159
+ function undetermined(): Violation[] {
160
+ return [];
161
+ }
162
+
163
+ /**
164
+ * Split a parameter list on top-level commas only.
165
+ *
166
+ * #588: `split(',')` broke `Map<String, Item> index` into `Map<String` and `Item> index`,
167
+ * losing the real type and inventing a second one. Generic parameters are the normal
168
+ * case in both Java and TypeScript, so a comma-blanking split is wrong in both.
169
+ *
170
+ * @param paramsText parameter list without surrounding parens
171
+ * @returns parameter fragments
172
+ */
173
+ function splitParams(paramsText: string): string[] {
174
+ const parts: string[] = [];
175
+ let depth = 0;
176
+ let current = '';
177
+ for (const ch of paramsText) {
178
+ if (ch === '<' || ch === '(' || ch === '[' || ch === '{') depth += 1;
179
+ else if (ch === '>' || ch === ')' || ch === ']' || ch === '}') depth -= 1;
180
+ if (ch === ',' && depth <= 0) {
181
+ parts.push(current);
182
+ current = '';
183
+ continue;
184
+ }
185
+ current += ch;
186
+ }
187
+ parts.push(current);
188
+ return parts.map((p) => p.trim()).filter(Boolean);
24
189
  }
25
190
 
26
- function collectMethodParamTypes(methodSignature: string): string[] {
27
- const paramsMatch = methodSignature.match(/\(([^)]+)\)/);
28
- if (!paramsMatch) return [];
191
+ function extractParamTypes(paramsText: string, isJava: boolean): string[] {
29
192
  const types: string[] = [];
30
- for (const param of paramsMatch[1].split(',').map((p) => p.trim())) {
31
- const paramType = extractParamType(param);
193
+ for (const param of splitParams(paramsText)) {
194
+ const paramType = isJava ? extractJavaParamType(param) : extractTsParamType(param);
32
195
  if (paramType) types.push(paramType);
33
196
  }
34
197
  return types;
35
198
  }
36
199
 
37
- function buildViolation(file: string, cls: ClassInfo, paramType: string): Violation {
200
+ /**
201
+ * Build a violation record for a class whose parameter type was decided incompatible.
202
+ *
203
+ * Exported for the line-fallback case: after #588 the rule can only report types it can
204
+ * PROVE incompatible, so a fixture can no longer reach `buildViolation` through
205
+ * `lspRule.check`. Asserting it directly keeps the `?? 0` fallback covered instead of
206
+ * letting the case pass vacuously.
207
+ *
208
+ * @param file path reported in the violation
209
+ * @param cls the class, whose `line` may be absent
210
+ * @param paramType the offending parameter type
211
+ * @returns the violation record
212
+ */
213
+ export function buildViolation(file: string, cls: ClassInfo, paramType: string): Violation {
38
214
  // Read the ACTIVE severity here rather than at module load (#457).
39
215
  const settings = getActiveConfig().rules['solid']['lsp'];
40
216
  return {
@@ -46,23 +222,169 @@ function buildViolation(file: string, cls: ClassInfo, paramType: string): Violat
46
222
  };
47
223
  }
48
224
 
49
- function checkClass(file: string, cls: ClassInfo): Violation[] {
50
- if (!cls.code || !cls.code.includes('extends')) return [];
225
+ /**
226
+ * Method DECLARATIONS in a class body — not call sites.
227
+ *
228
+ * `\w+\s*\(([^)]+)\)` matched calls too, split `foo(a, b)` on the comma and made `a`/`b`
229
+ * into "parameter types". Requiring a declaration shape (modifiers/return type before the
230
+ * name, and `{`/`;` after the parameter list) is what tells the two apart.
231
+ */
232
+ const METHOD_DECL_RE = new RegExp(
233
+ '(?:^|\\n)[ \\t]*(?:@[\\w$.]+(?:\\([^)]*\\))?[ \\t]*)*' +
234
+ '(?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp|sealed|non-sealed|override|suspend|inline|fun)[ \\t]+)*' +
235
+ '(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]+)?' +
236
+ '[\\w$][\\w$.]*(?:[ \\t]*<[^<>]*(?:<[^<>]*>[^<>]*)*>[ \\t]*)?(?:\\[\\][ \\t]*)*[ \\t]+' +
237
+ '(\\w+)[ \\t]*\\(([^;{}=]*)\\)' +
238
+ '(?:\\s*(?:throws|:)[^{;]+)?\\s*[{;]',
239
+ 'g',
240
+ );
51
241
 
52
- const violations: Violation[] = [];
53
- const methodSignatures = cls.code.match(/\w+\s*\(([^)]+)\)/g) || [];
242
+ /**
243
+ * Declaration matcher for `name(params): ReturnType` (TypeScript, and where a language
244
+ * puts the return type AFTER the parameter list).
245
+ *
246
+ * The Java matcher above requires a return type BEFORE the name, so it never matches a
247
+ * TypeScript method. Without this second shape the base-contract lookup silently found no
248
+ * methods in TypeScript and every TypeScript override degraded to silence — the exact
249
+ * failure mode #588 is about, reached from the other side.
250
+ *
251
+ * The trailing `:` return-type annotation is what distinguishes a declaration from a call:
252
+ * a call is followed by `;`/`,`/`)`, not by `: Type`.
253
+ */
254
+ const TS_METHOD_DECL_RE = new RegExp(
255
+ '(?:^|[\\n{};])[ \\t]*(?:public|private|protected|static|readonly|abstract|async|override|declare)?[ \\t]*' +
256
+ '(\\w+)[ \\t]*\\(([^;{}=]*)\\)[ \\t]*:[ \\t]*[\\w$][\\w$<>[\\].| ]*' +
257
+ '\\s*[{;]',
258
+ 'g',
259
+ );
54
260
 
55
- for (const methodSignature of methodSignatures) {
56
- for (const paramType of collectMethodParamTypes(methodSignature)) {
57
- if (!isCompatibleParamType(paramType, cls.name)) {
261
+ /**
262
+ * The declaration matcher for a language.
263
+ *
264
+ * @param isJava true for Java/Kotlin (`Type name`), false for TypeScript (`name: Type`)
265
+ * @returns the regex to scan declarations with
266
+ */
267
+ function declarationMatcher(isJava: boolean): RegExp {
268
+ return isJava ? METHOD_DECL_RE : TS_METHOD_DECL_RE;
269
+ }
270
+
271
+ function methodParamTypes(code: string, isJava: boolean): string[] {
272
+ const types: string[] = [];
273
+ const matcher = new RegExp(declarationMatcher(isJava).source, 'g');
274
+ let m: RegExpExecArray | null;
275
+ while ((m = matcher.exec(code)) !== null) {
276
+ types.push(...extractParamTypes(m[2], isJava));
277
+ }
278
+ return types;
279
+ }
280
+
281
+ /**
282
+ * Check one derived class against its base class contract.
283
+ *
284
+ * The rule reports a parameter type only when the base method is visible in the SAME file
285
+ * and does not accept that type. Every other case is silence, because a single-file
286
+ * analysis cannot know the base contract otherwise (#588).
287
+ *
288
+ * `allClasses` is passed in so the base class can be located by name; the rule does not
289
+ * enumerate classes itself, keeping class discovery the adapter's job.
290
+ *
291
+ * @param file path reported in violations
292
+ * @param cls the derived class
293
+ * @param allClasses every class the adapter found in this file
294
+ * @param isJava true for Java/Kotlin parameter syntax
295
+ * @returns violations, possibly none
296
+ */
297
+ function checkClass(
298
+ file: string,
299
+ cls: ClassInfo,
300
+ allClasses: ClassInfo[],
301
+ isJava: boolean,
302
+ ): Violation[] {
303
+ if (!cls.code || !cls.code.includes('extends')) return undetermined();
304
+
305
+ const contract = baseContract(cls.code, allClasses, isJava);
306
+ if (contract.size === 0) return undetermined();
307
+
308
+ const violations: Violation[] = [];
309
+ for (const [methodName, paramTypes] of declaredDeclarations(cls.code, isJava)) {
310
+ const baseParamTypes = contract.get(methodName);
311
+ if (!baseParamTypes) continue;
312
+ for (const paramType of paramTypes) {
313
+ if (compatibilityVerdict(paramType, cls.name, baseParamTypes) === false) {
58
314
  violations.push(buildViolation(file, cls, paramType));
59
315
  }
60
316
  }
61
317
  }
62
-
63
318
  return violations;
64
319
  }
65
320
 
321
+ /**
322
+ * Method name -> parameter types, for the declarations in a class body.
323
+ *
324
+ * Unlike `methodParamTypes` this keeps the method name, which is what makes the base
325
+ * lookup possible.
326
+ *
327
+ * @param code a class body
328
+ * @param isJava true for Java/Kotlin parameter syntax
329
+ * @returns declarations, in source order
330
+ */
331
+ function declaredDeclarations(code: string, isJava: boolean): Array<[string, string[]]> {
332
+ const declarations: Array<[string, string[]]> = [];
333
+ const declRe = new RegExp(declarationMatcher(isJava).source, 'g');
334
+ let m: RegExpExecArray | null;
335
+ while ((m = declRe.exec(code)) !== null) {
336
+ declarations.push([m[1], extractParamTypes(m[2], isJava)]);
337
+ }
338
+ return declarations;
339
+ }
340
+
341
+ /**
342
+ * Parameter types of the METHOD DECLARATIONS in a body, in source order.
343
+ *
344
+ * Exported for the same reason as `parseJavaParamTypes`: the rule's verdict cannot tell a
345
+ * correct declaration matcher from one that also matches call sites, because both end in
346
+ * silence for unparseable types. #588's anti-vacuity check is what proved that.
347
+ *
348
+ * @param code a class body
349
+ * @param isJava true for Java/Kotlin (`Type name`), false for TypeScript (`name: Type`)
350
+ * @returns parsed parameter type names
351
+ */
352
+ export function declaredParamTypes(code: string, isJava = true): string[] {
353
+ return methodParamTypes(code, isJava);
354
+ }
355
+
356
+ /**
357
+ * Parameter types parsed out of a Java parameter list.
358
+ *
359
+ * Exported so the #588 suite can assert on the PARSE rather than only on the rule's
360
+ * verdict. Without this the rule's silence is ambiguous: "parsed and judged
361
+ * undeterminable" and "never parsed at all" look identical from the outside, which is how
362
+ * the first version of this fix survived its own anti-vacuity check.
363
+ *
364
+ * @param paramsText a Java parameter list without the surrounding parens
365
+ * @returns parsed type names
366
+ */
367
+ export function parseJavaParamTypes(paramsText: string): string[] {
368
+ return extractParamTypes(paramsText, true);
369
+ }
370
+
371
+ /**
372
+ * Does this adapter's language declare parameters as `Type name` (Java/Kotlin) rather
373
+ * than `name: Type` (TypeScript and friends)?
374
+ *
375
+ * Split out of `lspRule.check` to keep that function's cyclomatic complexity where it was:
376
+ * #588 pushed it from 2 to 4, and Gate 3's ratchet blocks a worsening. It also gives the
377
+ * one piece of language knowledge a name, which is where a future adapter should look.
378
+ *
379
+ * @param adapter the adapter under inspection
380
+ * @returns true for Java/Kotlin parameter syntax
381
+ */
382
+ function usesTypeFirstName(adapter: Adapter): boolean {
383
+ if (typeof adapter.detectLanguage !== 'function') return false;
384
+ const language = adapter.detectLanguage();
385
+ return language === 'java' || language === 'kotlin';
386
+ }
387
+
66
388
  export const lspRule: Rule = {
67
389
  id: 'solid.lsp',
68
390
  name: 'Liskov Substitution Principle Rule',
@@ -72,6 +394,9 @@ export const lspRule: Rule = {
72
394
  // #392-#4: no empty catch — let adapter/rule errors bubble up to
73
395
  // analyzer.runRuleOnFile() so they surface as gate errors, not silent [].
74
396
  const classes = (adapter.extractClasses() || []) as ClassInfo[];
75
- return classes.flatMap((cls) => checkClass(file, cls));
397
+ // The adapter decides which parameter syntax applies; the rule holds no language
398
+ // knowledge of its own (#588 suggestion 2, taken as far as a single file allows).
399
+ const isJava = usesTypeFirstName(adapter);
400
+ return classes.flatMap((cls) => checkClass(file, cls, classes, isJava));
76
401
  },
77
402
  };
@@ -314,7 +314,16 @@ describe('syncHooks', () => {
314
314
  it('syncHooks keeps every hook library as a regular byte-identical mirror', () => {
315
315
  expect(syncOk, 'sync script must exit 0').toBe(true);
316
316
  const canonicalDir = path.join(WORKTREE_PATH, 'githooks', 'lib');
317
- for (const name of fs.readdirSync(canonicalDir)) {
317
+ // #587/#588: `readdirSync` also returns directories, and `__pycache__` appears the
318
+ // moment anything imports prepush-score.py. The sync script deliberately does not
319
+ // mirror build artefacts into the package, so demanding a mirror for every entry made
320
+ // this case assert the OPPOSITE of the behaviour it exists to protect — and fail on a
321
+ // cache directory that is not a hook library at all.
322
+ const libraryNames = fs
323
+ .readdirSync(canonicalDir)
324
+ .filter((name) => fs.statSync(path.join(canonicalDir, name)).isFile());
325
+ expect(libraryNames.length, 'no hook libraries found to compare').toBeGreaterThan(0);
326
+ for (const name of libraryNames) {
318
327
  const canonical = path.join(canonicalDir, name);
319
328
  const mirror = path.join(HOOKS_DIR, 'lib', name);
320
329
  expect(fs.lstatSync(canonical).isSymbolicLink(), `${canonical} must not be a symlink`).toBe(false);
@@ -325,6 +334,28 @@ describe('syncHooks', () => {
325
334
  }
326
335
  });
327
336
 
337
+ it('syncHooks does not mirror Python bytecode caches into the package', () => {
338
+ // The positive half of the case above. Asserting only "the run did not fail" would
339
+ // pass on a clean checkout where no cache was ever created — which is exactly the
340
+ // state CI starts from, so it would prove nothing. Creating the cache first makes the
341
+ // assertion bite.
342
+ const canonicalCache = path.join(WORKTREE_PATH, 'githooks', 'lib', '__pycache__');
343
+ fs.mkdirSync(canonicalCache, { recursive: true });
344
+ fs.writeFileSync(path.join(canonicalCache, 'probe.cpython-312.pyc'), 'bytecode');
345
+
346
+ const result = spawnSync(nodeExe, [syncScript], {
347
+ cwd: WORKTREE_PATH,
348
+ stdio: 'pipe',
349
+ timeout: 15000,
350
+ });
351
+
352
+ expect(result.status, 'sync script must exit 0').toBe(0);
353
+ expect(
354
+ fs.existsSync(path.join(HOOKS_DIR, 'lib', '__pycache__')),
355
+ 'a Python bytecode cache was mirrored into the shipped package',
356
+ ).toBe(false);
357
+ });
358
+
328
359
  it('syncHooks removes stale generated hook libraries', () => {
329
360
  const staleFile = path.join(HOOKS_DIR, 'lib', 'stale-generated-file.sh');
330
361
  fs.mkdirSync(path.dirname(staleFile), { recursive: true });
@@ -36,7 +36,12 @@ function rmrf(target) {
36
36
  fs.rmSync(target, { recursive: true, force: true });
37
37
  }
38
38
 
39
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.opencode']);
39
+ // `.opencode` is a tool state directory; `__pycache__` is Python bytecode, created the
40
+ // moment anything imports a `.py` in the tree. It is gitignored, so it is not part of the
41
+ // package — but `find -type f` in scripts/check-hook-mirror.sh saw it in the mirror and
42
+ // reported "hook mirror file set drifted", which blocks the push with no fix the operator
43
+ // can apply from their own code (#587/#588).
44
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.opencode', '__pycache__']);
40
45
  const SKIP_FILES = new Set(['package-lock.json']);
41
46
  const SKIP_FILE_SUFFIXES = ['.lock', '.js.map'];
42
47
 
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-22
4
- **Commit:** fcd717af
3
+ **Generated:** 2026-10-25
4
+ **Commit:** 1ab53eff
5
5
  **Branch:** main
6
- **Version:** 0.31.0.0
6
+ **Version:** 0.31.2.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -392,6 +392,11 @@ delphi-review(
392
392
  )
393
393
  ```
394
394
 
395
+ > **先确认该工具真的存在(#574)**:不是所有 DSH profile 都装了它。若当前会话的工具列表里
396
+ > 没有 `delphi-review`,说明插件未安装 —— 此时**不要**推断"那就用 subagent / workflow 吧"。
397
+ > 按下方【通道白名单】处理:要么装插件(`dsh plugin add @boyingliu01/dsh-plugin-xp-gate`),
398
+ > 要么走 external 脚本路径,要么 BLOCK。
399
+
395
400
  该工具**直连外部 OpenAI 兼容网关**,每个席位绑定一个不同模型,因此不依赖宿主配置即可
396
401
  满足不变量。它复用**同一份** `.delphi-config.json`,无需额外配置。
397
402
 
@@ -431,6 +436,51 @@ delphi-review(
431
436
  - 降级证据落盘 `delphi-reviewed.json`:`channel`、`fallback_reason`(如 `dsh_subagent_model_selection_disabled`)、`probe_evidence`(①②③ 的实测摘录)。
432
437
  - 两通道同不变量:三专家齐全、`requested_model` 互异、consensus ≥90%、**无单专家路径**。
433
438
  - 可用性探测可用 `xp-gate doctor delphi` 复核(DSH 环境下含 `subagentModelSelection` 持久化设置检查)。
439
+
440
+ ##### 通道白名单(default-deny,#574)
441
+
442
+ 上面的双通道契约是**允许清单**,不是"除 subagent 外都能用"。**只有这两条通道合法**:
443
+ `subagent`(native,需通过 ①②③)与 external(插件工具 / `delphi-external-review.cjs`)。
444
+
445
+ **其余一切扇出机制一律禁止**,包括但不限于:
446
+
447
+ | 禁止的通道 | 为什么禁止 |
448
+ |---|---|
449
+ | `workflow` 工具的 `agent()` 扇出 | `agent()` 的 `provider`/`model` **不受** `allowedModels` 约束(`dsh-workflow-ptc` 里没有任何白名单校验,直达 `subagents.start`)。脚本里写什么模型就真的跑什么模型。 |
450
+ | 手写脚本 / 循环里逐个调 `subagent` 但省略 `model` | 缺 `model` 时子代理**静默继承宿主会话模型**,三席变一席。 |
451
+ | Agent Teams / `spawn_teammate` | 团队成员模型不可逐条指定,且 `TeamMemberView.model` 是输出字段、可能是继承值。 |
452
+ | 任何"从网关模型清单里挑三个"的做法 | 网关可能注册上百个模型,任意三个互异都满足旧不变量 —— 这正是 #574 的事故形态。 |
453
+
454
+ **推荐通道不可用时,必须 BLOCK 并如实报告,不得自行选一条"看起来能跑"的路径。**
455
+ 判定句:*如果我正在用的派发机制不在上面两条之内,立即停止,输出 BLOCK 并说明缺什么。*
456
+
457
+ ##### 席位解析(派发前强制步骤,#574)
458
+
459
+ 模型 ID **只能来自 `.delphi-config.json`**,不得来自会话默认模型、网关模型清单、
460
+ 记忆、或上一次评审的残留。派发前依次执行:
461
+
462
+ 1. 读取 `<项目根>/.delphi-config.json`,取 `active_profile` 指向的 profile。
463
+ (查找顺序:项目根 → `~/.config/xp-gate/.delphi-config.json`;多份存在时以项目根为准。)
464
+ 2. 解出 `architecture` / `technical` / `feasibility` 三个席位的 `model`,**去空白**。
465
+ 3. **原样打印**这三个 ID 与 `active_profile`,作为本轮派发的凭据。
466
+ 4. 只用这三个字面量派发;脚本里**不得出现配置中不存在的模型 ID**。
467
+ 5. 三席位必须互异 —— 若配置本身有重复,这是配置错误,BLOCK,不要去"挑一个换掉"。
468
+
469
+ > 反例(2026-10-22 实测事故):某次 code-walkthrough 的 workflow 脚本硬编码了
470
+ > `s-deepseek-v4.1-flash` / `g-deepseek-v4-pro` / `doubao-seed-2.0-pro`,
471
+ > 而配置写的是三个 `local-*` 免费席位。三者互异、评审"成功"、Gate MW 判 APPROVED,
472
+ > 但付费模型被真实调用,而 `local-*` 只落了 1/3。16 次 workflow 运行中读取配置的次数为 **0**。
473
+
474
+ ##### 执行后核验(#574)
475
+
476
+ 评审结束后必须逐席位核对**实际解析到的模型**,不能只看请求值:
477
+
478
+ - 证据来源:子会话的 `request/header`(平台日志)或 external 脚本的回执 `resolved_model`。
479
+ - 三席 `requested_model` 必须**逐一等于**配置中同角色的席位(按角色对应,不是"三者在集合里")。
480
+ - 任一不匹配 → 本次评审**作废**,不得写入 `.code-walkthrough-result.json`,按 BLOCK 处理。
481
+ - Gate MW 会独立复核这一点(#574):存在 `.delphi-config.json` 时,
482
+ 证据里每个 `requested_model` 必须等于该角色的配置席位,否则 pre-push 直接 BLOCK。
483
+
434
484
  #### WorkBuddy 平台(Expert 团队模式)
435
485
 
436
486
  WorkBuddy 侧以 Team 型专家团(示例:`delphi-review-team`,安装在 `~/.workbuddy/plugins/marketplaces/my-experts/plugins/`,创建方式见 [docs/platform-workbuddy.md](../../docs/platform-workbuddy.md))承载评审,触发本技能后按此路由执行:
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-22
4
- **Commit:** fcd717af
3
+ **Generated:** 2026-10-25
4
+ **Commit:** 1ab53eff
5
5
  **Branch:** main
6
- **Version:** 0.31.0.0
6
+ **Version:** 0.31.2.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-22
4
- **Commit:** fcd717af
3
+ **Generated:** 2026-10-25
4
+ **Commit:** 1ab53eff
5
5
  **Branch:** main
6
- **Version:** 0.31.0.0
6
+ **Version:** 0.31.2.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
package/sprint-gate.sh CHANGED
@@ -35,6 +35,17 @@
35
35
  # - JSON parser fallback chain: jq → Node.js → explicit degradation message
36
36
  #
37
37
  # OUTPUT: JSON to stdout — {"decision":"allow|deny|skip",...}
38
+ #
39
+ # #584: every `allow` also carries `"allow_reason"`, because `allow` alone was
40
+ # ambiguous. It meant three different things and pre-commit mapped all of them to
41
+ # Gate 11 PASS:
42
+ #
43
+ # checks-passed the sprint was actually validated -> PASS is honest
44
+ # phase-not-reached the sprint has not reached this gate's phase
45
+ # no-json-parser nothing could be read at all
46
+ #
47
+ # The last two examine nothing, so reporting them as PASS inflated the score.
48
+ # `checks-passed` is the only reason that licenses a PASS.
38
49
  # EXIT: 0 = pass/skip, 1 = block
39
50
 
40
51
  set -euo pipefail
@@ -264,7 +275,7 @@ if [ "$MODE" = "pre-commit" ]; then
264
275
 
265
276
  # If no JSON parser available, warn but allow (graceful degradation)
266
277
  if [ "$JSON_PARSER" = "none" ]; then
267
- echo '{"decision":"allow","warning":"neither jq nor node available, cannot verify delphi-review verdict. Install jq or node for full protection."}'
278
+ echo '{"decision":"allow","allow_reason":"no-json-parser","warning":"neither jq nor node available, cannot verify delphi-review verdict. Install jq or node for full protection."}'
268
279
  exit 0
269
280
  fi
270
281
 
@@ -299,7 +310,7 @@ if [ "$MODE" = "pre-commit" ]; then
299
310
  # claim: a 29-day-old sprint under a 30-day threshold is a very different
300
311
  # situation from one opened today, and the log should say which it is.
301
312
  if [ -n "$SPRINT_AGE_DAYS" ]; then
302
- echo "{\"decision\":\"allow\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated (sprint open ${SPRINT_AGE_DAYS} day(s), staleness threshold ${SPRINT_STALE_DAYS})\"}"
313
+ echo "{\"decision\":\"allow\",\"allow_reason\":\"checks-passed\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated (sprint open ${SPRINT_AGE_DAYS} day(s), staleness threshold ${SPRINT_STALE_DAYS})\"}"
303
314
  else
304
315
  echo "{\"decision\":\"allow\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated (sprint age unknown — started_at unreadable)\"}"
305
316
  fi
@@ -307,7 +318,7 @@ if [ "$MODE" = "pre-commit" ]; then
307
318
  fi
308
319
 
309
320
  # Phase < 1 (pre-PLAN) — no delphi enforcement needed
310
- echo "{\"decision\":\"allow\",\"reason\":\"sprint phase ${PHASE} (pre-PLAN), no delphi enforcement required\"}"
321
+ echo "{\"decision\":\"allow\",\"allow_reason\":\"phase-not-reached\",\"reason\":\"sprint phase ${PHASE} (pre-PLAN), no delphi enforcement required\"}"
311
322
  exit 0
312
323
  fi
313
324