@boyingliu01/xp-gate 0.35.15 → 0.35.17

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 (44) hide show
  1. package/bin/xp-gate.js +67 -6
  2. package/gates/gate-9.ts +26 -4
  3. package/hooks/pre-commit +33 -10
  4. package/lib/__tests__/audit-cli-contract.test.ts +117 -0
  5. package/lib/__tests__/gate-coverage-hook-floor.test.js +94 -0
  6. package/lib/__tests__/gate-entry-provenance.test.js +71 -0
  7. package/lib/gate-coverage.js +46 -3
  8. package/lib/gate-runner.js +10 -2
  9. package/lib/mattpocock-license-verdict.json +1 -1
  10. package/mock-policy/AGENTS.md +3 -3
  11. package/mutation/AGENTS.md +3 -3
  12. package/mutation/runners/mutmut-runner.ts +165 -70
  13. package/package.json +1 -1
  14. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  15. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  16. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  17. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  18. package/plugins/dsh/package.json +1 -1
  19. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  20. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  21. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  22. package/plugins/opencode/README.md +22 -1
  23. package/plugins/opencode/contract-v2.test.ts +125 -0
  24. package/plugins/opencode/exec-injection.test.ts +110 -0
  25. package/plugins/opencode/index.ts +39 -4
  26. package/plugins/opencode/package.json +2 -7
  27. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  28. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  29. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  30. package/plugins/qoder/plugin.json +1 -1
  31. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  32. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  33. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  34. package/principles/AGENTS.md +3 -3
  35. package/principles/adapters/import-clause.ts +57 -0
  36. package/principles/adapters/member-scan.ts +73 -0
  37. package/principles/adapters/position-analysis.ts +43 -0
  38. package/principles/adapters/quote-state.ts +80 -0
  39. package/principles/adapters/typescript.ts +57 -116
  40. package/principles/rules/clean-code/god-class.ts +37 -14
  41. package/principles/rules/solid/dip.ts +15 -2
  42. package/skills/delphi-review/AGENTS.md +3 -3
  43. package/skills/sprint-flow/AGENTS.md +3 -3
  44. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -1,5 +1,11 @@
1
1
  import { BaseAdapter } from './base';
2
2
  import { Adapter } from '../types';
3
+ import { isCallPosition } from './position-analysis';
4
+ import { claimMember, countTopLevelMembers } from './member-scan';
5
+ import { parseImportClause } from './import-clause';
6
+ import {
7
+ type QuoteState, newQuoteState, inQuotedState, advanceQuoteState, applyDelimiter, isQuote,
8
+ } from './quote-state';
3
9
 
4
10
  /** IO-ish callees that the missing-error-handling rule cares about. */
5
11
  const IO_CALLEES = [
@@ -181,77 +187,6 @@ function lineOf(src: string, index: number): number {
181
187
  }
182
188
 
183
189
  /** The three quoted states shared by the parameter/paren scanners. */
184
- interface QuoteState {
185
- inSingle: boolean;
186
- inDouble: boolean;
187
- inTemplate: boolean;
188
- }
189
-
190
- function newQuoteState(): QuoteState {
191
- return { inSingle: false, inDouble: false, inTemplate: false };
192
- }
193
-
194
- function inQuotedState(state: QuoteState): boolean {
195
- return state.inSingle || state.inDouble || state.inTemplate;
196
- }
197
-
198
- /** The three quote kinds tracked by QuoteState, with the flag that marks each. */
199
- const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
200
- { quote: "'", flag: 'inSingle' },
201
- { quote: '"', flag: 'inDouble' },
202
- { quote: '`', flag: 'inTemplate' },
203
- ];
204
-
205
- /**
206
- * Advance one character of quote tracking.
207
- *
208
- * Extracted because `countParams` and `readParenGroup` carried the same
209
- * six-branch block verbatim (#602): both must ignore delimiters that appear
210
- * inside a string, and neither may treat an escape as a closing quote.
211
- *
212
- * Table-driven (#620): all three quote kinds behave identically -- an escape
213
- * skips two characters, the matching quote closes the literal -- so they are one
214
- * rule applied to a list rather than three copies of it.
215
- *
216
- * @returns the number of characters consumed (2 when the next char is escaped).
217
- */
218
- function advanceQuoteState(src: string, i: number, state: QuoteState): number {
219
- const c = src[i];
220
-
221
- for (const kind of QUOTE_KINDS) {
222
- if (!state[kind.flag]) continue;
223
- if (c === '\\') return 2;
224
- if (c === kind.quote) state[kind.flag] = false;
225
- return 1;
226
- }
227
-
228
- // Not inside any literal: open the one this character starts, if any.
229
- const opener = QUOTE_KINDS.find(kind => kind.quote === c);
230
- if (opener) state[opener.flag] = true;
231
- return 1;
232
- }
233
-
234
- /** Opening delimiters that raise the generic/bracket depth in a param list. */
235
- const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
236
- /** Closing delimiters that lower it. */
237
- const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
238
-
239
- /**
240
- * Adjust `depth` for one delimiter.
241
- * @returns true when the character was a delimiter (so it cannot also be a
242
- * separator), false otherwise.
243
- */
244
- function applyDelimiter(c: string, depth: { value: number }): boolean {
245
- if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
246
- if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
247
- return false;
248
- }
249
-
250
- /** True when the character opens a quote/template the scanners must skip. */
251
- function isQuote(c: string): boolean {
252
- return c === "'" || c === '"' || c === '`';
253
- }
254
-
255
190
  /** Count top-level, comma-separated parameters in a raw parameter list. */
256
191
  function countParams(rawList: string): number {
257
192
  const trimmed = rawList.trim();
@@ -726,37 +661,13 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
726
661
  const importRe = /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"][^'"]+['"]/gm;
727
662
  const importSpans: Array<[number, number]> = [];
728
663
  let m: RegExpExecArray | null;
729
- const idRe = /^[A-Za-z_$][\w$]*$/;
730
664
 
731
665
  while ((m = importRe.exec(src)) !== null) {
732
- const clause = m[1].trim();
733
666
  const line = lineOf(src, m.index);
734
667
  const isTypeOnly = /^import\s+type\b/.test(m[0]);
735
- const push = (name: string, type?: string) => {
736
- if (idRe.test(name)) entries.push({ name, line, used: false, ...(type ? { type } : {}) });
737
- };
738
-
739
- // default import
740
- const def = /^(\w+)/.exec(clause);
741
- if (def) push(def[1], isTypeOnly ? 'type' : undefined);
742
-
743
- // named imports { a, b as c }
744
- const braces = /\{([\s\S]*?)\}/.exec(clause);
745
- if (braces) {
746
- for (const raw of braces[1].split(',')) {
747
- const part = raw.trim();
748
- if (!part) continue;
749
- const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
750
- const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
751
- const memberTypeOnly = /^type\s+/.test(part);
752
- push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
753
- }
668
+ for (const binding of parseImportClause(m[1].trim(), isTypeOnly)) {
669
+ entries.push({ ...binding, line, used: false });
754
670
  }
755
-
756
- // namespace import * as ns
757
- const ns = /\*\s+as\s+(\w+)/.exec(clause);
758
- if (ns) push(ns[1], isTypeOnly ? 'type' : undefined);
759
-
760
671
  importSpans.push([m.index, m.index + m[0].length]);
761
672
  }
762
673
 
@@ -831,7 +742,9 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
831
742
  length: hasBraces ? Math.max(1, endLine - startLine + 1) : 1,
832
743
  paramCount: countParams(inner),
833
744
  nestingDepth: hasBraces ? maxNesting(maskLiterals(body)) : 0,
834
- hasTryCatch: /\btry\s*\{/.test(masked.substring(bodyOpen, bodyEnd)),
745
+ // The RAW body: `on('error', ...)` needs its string literal intact, and
746
+ // masking blanks string bodies. try/catch is a keyword and is unaffected.
747
+ hasTryCatch: hasErrorHandling(body),
835
748
  ioOperations: ioOperationsIn(masked.substring(bodyOpen, bodyEnd)),
836
749
  code: body,
837
750
  };
@@ -853,13 +766,6 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
853
766
  }
854
767
 
855
768
  /** Claim a `name@offset` key once, so overlapping shapes cannot double-count. */
856
- private claim(seen: Set<string>, name: string, nameIndex: number): boolean {
857
- const key = `${name}@${nameIndex}`;
858
- if (seen.has(key)) return false;
859
- seen.add(key);
860
- return true;
861
- }
862
-
863
769
  /** `function name(...)` declarations, including export / async / generators. */
864
770
  private collectFunctionDecls(seen: Set<string>, found: FunctionMetrics[]): void {
865
771
  const masked = this.maskedSource;
@@ -871,7 +777,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
871
777
  if (parenIndex === -1) continue;
872
778
  const bodyOpen = findBodyBrace(masked, parenIndex);
873
779
  if (bodyOpen === -1) continue;
874
- if (!this.claim(seen, m[1], nameIndex)) continue;
780
+ if (!claimMember(seen, m[1], nameIndex)) continue;
875
781
  found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
876
782
  }
877
783
  }
@@ -892,10 +798,11 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
892
798
  // parameter list or a type annotation, which corrupted the reported span.
893
799
  const parenIndex = m.index + m[0].length - 1;
894
800
  const nameIndex = parenIndex - m[1].length;
801
+ if (isCallPosition(masked, nameIndex)) continue;
895
802
  // Only treat as a method when a body brace follows the parameter list.
896
803
  const bodyOpen = findBodyBrace(masked, parenIndex);
897
804
  if (bodyOpen === -1) continue;
898
- if (!this.claim(seen, m[1], nameIndex)) continue;
805
+ if (!claimMember(seen, m[1], nameIndex)) continue;
899
806
  found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
900
807
  }
901
808
  }
@@ -909,7 +816,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
909
816
  const arrow = this.parseArrowAt(m, arrowRe.lastIndex);
910
817
  if (!arrow) continue;
911
818
  const nameIndex = m.index + m[0].length - m[1].length;
912
- if (!this.claim(seen, m[1], nameIndex)) continue;
819
+ if (!claimMember(seen, m[1], nameIndex)) continue;
913
820
  found.push(this.buildArrowMetrics(m[1], nameIndex, arrow));
914
821
  }
915
822
  }
@@ -1003,14 +910,19 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1003
910
  if (bodyOpen === -1) continue;
1004
911
  const bodyEnd = findBlockEnd(masked, bodyOpen);
1005
912
  const body = src.substring(bodyOpen, bodyEnd);
1006
-
1007
- // Count method-ish members. Not anchored to line start: minified /
1008
- // single-line class bodies (`m1() {} m2() {}`) are valid TS and must count.
1009
- // `(?<![\w$.])` avoids matching call sites like `foo.bar(`.
1010
- const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
1011
- const methods = [...body.matchAll(memberRe)]
1012
- .map(x => x[1])
1013
- .filter(n => !['if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new'].includes(n));
913
+ // Members are counted on the MASKED body: the prose in a JSDoc line like
914
+ // "three separate events (close / error / timeout)" reads as `events(` and
915
+ // was counted as a method, as did `unknown` from a `(): unknown {` return
916
+ // type. Masking blanks comments and string bodies, so only real code has
917
+ // braces and parens left.
918
+ const maskedBody = masked.substring(bodyOpen, bodyEnd);
919
+
920
+ // Count members declared at the class body's TOP brace depth. A call
921
+ // inside a method body sits one level deeper, which is the only signal
922
+ // that separates `{ helper(1); }` from `{ async run() { ... }`.
923
+ // Earlier attempts compared the preceding character and reported 66
924
+ // methods for a class declaring 30 -- every call site counted once.
925
+ const methods = countTopLevelMembers(maskedBody);
1014
926
 
1015
927
  results.push({
1016
928
  name,
@@ -1065,3 +977,32 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1065
977
  return exportMatches;
1066
978
  }
1067
979
  }
980
+
981
+
982
+ /**
983
+ * Does this function handle failures?
984
+ *
985
+ * `try`/`catch` is one way, and for a function that owns a child process it is
986
+ * not the idiomatic one: Node reports spawn failures through an `'error'`
987
+ * event, and a `try` around `spawn()` would never see them. Treating the event
988
+ * listener as unhandled reported `runMutmut` for having "I/O without error
989
+ * handling" while it was in fact the only handler for that failure mode --
990
+ * a warning no correct edit could clear, which is how a rule loses its meaning.
991
+ *
992
+ * `'error'` is matched as a QUOTED argument (`on('error', ...)`) so that a
993
+ * string that merely contains the word does not qualify.
994
+ */
995
+ function hasErrorHandling(body: string): boolean {
996
+ return TRY_BLOCK.test(body) || ERROR_LISTENER.test(body);
997
+ }
998
+
999
+ /**
1000
+ * The two handler shapes, as module-level constants.
1001
+ *
1002
+ * ccn-report locates a function body by counting brackets on its lines; a regex
1003
+ * literal INSIDE the body makes that extent ambiguous, and the row comes back
1004
+ * "CCN could not be span-verified" -- a Gate 3 block. Brackets and braces in the
1005
+ * patterns are written as character classes for the same reason.
1006
+ */
1007
+ const TRY_BLOCK = /\btry\s*[{]/;
1008
+ const ERROR_LISTENER = /\bon\s*[(]\s*['"]error['"]\s*,/;
@@ -15,25 +15,26 @@ export const godClassRule: Rule = {
15
15
  code?: string;
16
16
  line: number;
17
17
  name: string;
18
+ /** Method count when the adapter parsed the class structurally. */
19
+ methodCount?: number;
18
20
  }
19
-
21
+
20
22
  interface TypedAdapter {
21
23
  extractClasses?: () => ClassObject[] | undefined;
22
24
  }
23
-
25
+
24
26
  const typedAdapter = adapter as TypedAdapter;
25
27
  const classes = typedAdapter.extractClasses?.() || [];
26
-
28
+
27
29
  for (const cls of classes) {
28
- const methodMatches = cls.code?.match(/(get|set)\s+\w+\s*\(|\w+\s*\([^)]*\)\s*{/g) || [];
29
-
30
- const methodCount = methodMatches.filter((m: string) => {
31
- if (m.startsWith('get ') || m.startsWith('set ')) {
32
- return false;
33
- }
34
- return true;
35
- }).length;
36
-
30
+ // Prefer the adapter's structural count. The regex fallback below cannot
31
+ // see methods whose signature spans lines or carries a return type, so on
32
+ // `TypeScriptAdapter` (66 methods) it counted 0 while `solid.srp` read the
33
+ // adapter's own figure and reported it -- the same class, two different
34
+ // answers, and the wrong one was silent. A count the adapter already
35
+ // computed must not be recomputed by guessing at source text.
36
+ const methodCount = cls.methodCount ?? countMethodsByRegex(cls.code);
37
+
37
38
  if (methodCount > (settings.threshold ?? 0)) {
38
39
  violations.push({
39
40
  file,
@@ -46,7 +47,29 @@ export const godClassRule: Rule = {
46
47
  });
47
48
  }
48
49
  }
49
-
50
+
50
51
  return violations;
51
52
  }
52
- };
53
+ };
54
+
55
+ /**
56
+ * Source-text method matcher.
57
+ *
58
+ * Kept at module scope on purpose: ccn-report locates a function body by counting
59
+ * brackets on its lines, so a regex literal inside the body makes the extent
60
+ * ambiguous and the row comes back "CCN could not be span-verified".
61
+ */
62
+ const METHOD_PATTERN = /(?:get|set)\s+\w+\s*[(]|\w+\s*[(][^)]*[)]\s*[{]/g;
63
+
64
+ /** A matched accessor keyword pair rather than a real method. */
65
+ const ACCESSOR_PATTERN = /^(?:get|set)\s/;
66
+
67
+ /**
68
+ * Count methods by matching source text. Only used when the adapter does not
69
+ * report `methodCount` (Java, C++ and other regex-backed adapters), so the
70
+ * getter/setter exclusion stays meaningful.
71
+ */
72
+ function countMethodsByRegex(code: string | undefined): number {
73
+ const methodMatches = code?.match(METHOD_PATTERN) ?? [];
74
+ return methodMatches.filter((m: string) => !ACCESSOR_PATTERN.test(m)).length;
75
+ }
@@ -1,8 +1,14 @@
1
1
  import { Rule, Violation, Severity } from '../../types';
2
2
  import { getActiveConfig } from '../../config';
3
3
 
4
+ // Value objects, not collaborators. Every entry here is a type that ordinary
5
+ // code CONSTRUCTS from data it already has, and that has no injectable
6
+ // alternative: you cannot pass in a Date/Map/RegExp factory any more usefully
7
+ // than you can pass in the number 1. Constructing one is not the inversion being
8
+ // bypassed, so reporting it teaches the wrong lesson (#676).
4
9
  const DEFAULT_EXCLUDED_CLASSES = [
5
- 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'
10
+ 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise',
11
+ 'RegExp', 'Number', 'String', 'Boolean', 'BigInt', 'Symbol', 'WeakMap', 'WeakSet',
6
12
  ];
7
13
 
8
14
  /**
@@ -10,9 +16,16 @@ const DEFAULT_EXCLUDED_CLASSES = [
10
16
  *
11
17
  * A module-load snapshot was #457: a project's `.principlesrc` override was
12
18
  * parsed but never consulted.
19
+ *
20
+ * The config entry EXTENDS the built-in value-object list rather than replacing
21
+ * it (#676). Replacing meant a project that added one name silently lost every
22
+ * default exemption -- `.principlesrc` here lists 7 entries, so `new RegExp()`
23
+ * was reported even though the default list would have excused it. A project
24
+ * cannot reasonably be expected to restate the built-ins it never knew about.
13
25
  */
14
26
  function excludedClasses(): (string | number)[] {
15
- return getActiveConfig().rules['solid']['dip'].exclude ?? DEFAULT_EXCLUDED_CLASSES;
27
+ const configured = getActiveConfig().rules['solid']['dip'].exclude ?? [];
28
+ return [...new Set([...DEFAULT_EXCLUDED_CLASSES, ...configured])];
16
29
  }
17
30
 
18
31
  function shouldSkipInstantiation(className: string): boolean {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-30
4
- **Commit:** 6091e238
3
+ **Generated:** 2026-10-31
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.15.0
6
+ **Version:** 0.35.17.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.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-30
4
- **Commit:** 6091e238
3
+ **Generated:** 2026-10-31
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.15.0
6
+ **Version:** 0.35.17.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-30
4
- **Commit:** 6091e238
3
+ **Generated:** 2026-10-31
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.15.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.