@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
@@ -470,11 +470,24 @@ function gateArchTool(directory: string) {
470
470
 
471
471
  function readCurrentSessionModel(dbPath: string, sessionId: string): string {
472
472
  try {
473
+ // execFileSync, NOT execSync (#639). `sessionId` comes from the caller's
474
+ // argument, and the previous form interpolated it into a shell string:
475
+ //
476
+ // `sqlite3 "${dbPath}" "SELECT model FROM session WHERE id = '${escapedId}'"`
477
+ //
478
+ // Escaping only the SQL single quote left every SHELL metacharacter live --
479
+ // a session id containing `"`, `$( )` or a backtick escaped the quoting and
480
+ // executed. Passing argv as an array removes the shell from the path
481
+ // entirely, so no escaping is needed or possible to get wrong.
482
+ //
483
+ // The SQL single quote is still escaped: that is a SQL concern, not a shell
484
+ // one, and it survives the move to argv unchanged.
473
485
  const escapedId = sessionId.replace(/'/g, "''")
474
- const currentRaw = execSync(
475
- `sqlite3 "${dbPath}" "SELECT model FROM session WHERE id = '${escapedId}'"`,
476
- { encoding: "utf8", timeout: SQLITE_TIMEOUT_MS }
477
- ).trim()
486
+ const sql = `SELECT model FROM session WHERE id = '${escapedId}'`
487
+ const currentRaw = execFileSync("sqlite3", [dbPath, sql], {
488
+ encoding: "utf8",
489
+ timeout: SQLITE_TIMEOUT_MS,
490
+ }).trim()
478
491
  if (!currentRaw) return "unknown"
479
492
  try {
480
493
  const parsed = JSON.parse(currentRaw)
@@ -664,9 +677,31 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
664
677
  }
665
678
  }
666
679
 
680
+ /**
681
+ * The plugin module, shaped for BOTH OpenCode generations (#639).
682
+ *
683
+ * v1 read `server`. v2 validates the default export against a schema that
684
+ * requires `id` plus exactly one of `effect` / `setup`, and rejects anything
685
+ * else outright:
686
+ *
687
+ * PluginModule.LoadError: Plugin must export a default definition with an id
688
+ * and an effect or setup function.
689
+ * (cause: SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))
690
+ *
691
+ * The failure mode is total, not partial: the plugin does not load at all, so
692
+ * `gate-check` / `gate-principles` / `gate-arch` simply vanish from v2 sessions
693
+ * and every startup prints a load error.
694
+ *
695
+ * `setup` and the v1 `server` take the same input and return the same shape, so
696
+ * the same function serves both keys. Both are exported deliberately: dropping
697
+ * `server` would break every v1 user on upgrade, which is a migration nobody
698
+ * asked for. `pluginModule` is not frozen, so this stays a plain object literal
699
+ * that both runtimes can read.
700
+ */
667
701
  const pluginModule = {
668
702
  id: "xp-gate",
669
703
  server: XpGatePlugin,
704
+ setup: XpGatePlugin,
670
705
  }
671
706
 
672
707
  /**
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.35.15",
3
+ "version": "0.35.17",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
- "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
6
+ "description": "XP-Gate quality gates + AI workflow skills for OpenCode",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/boyingliu01/xp-gate",
@@ -17,15 +17,10 @@
17
17
  ".": {
18
18
  "import": "./index.ts",
19
19
  "types": "./index.ts"
20
- },
21
- "./tui": {
22
- "import": "./tui-plugin.tsx",
23
- "types": "./tui-plugin.tsx"
24
20
  }
25
21
  },
26
22
  "files": [
27
23
  "index.ts",
28
- "tui-plugin.ts",
29
24
  "skills/",
30
25
  "tsconfig.json",
31
26
  "README.md"
@@ -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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.15",
3
+ "version": "0.35.17",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -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.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
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
  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,57 @@
1
+ /**
2
+ * Import-clause parsing for the TypeScript adapter's `imports` getter.
3
+ *
4
+ * Extracted from `typescript.ts` so that one function does not have to carry
5
+ * both the parsing rules and the usage-scoring loop: together they pushed the
6
+ * getter past the 50-line budget and to 5 nesting levels. Splitting also lets
7
+ * the clause grammar be tested without reading a file.
8
+ */
9
+
10
+ /** One symbol bound by an import statement. */
11
+ export interface ImportBinding {
12
+ name: string;
13
+ type?: string;
14
+ }
15
+
16
+ /** A plain identifier -- these are the only names an import can bind. */
17
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
18
+
19
+ /**
20
+ * Parse the clause between `import` and `from` into the symbols it binds.
21
+ *
22
+ * Covers the four shapes TypeScript allows: a default binding, a named brace
23
+ * list (with per-member `type` and `as` renames), and a namespace binding.
24
+ *
25
+ * @param clause text between `import` / `import type` and `from`
26
+ * @param isTypeOnly whether the whole statement was `import type ...`
27
+ */
28
+ export function parseImportClause(clause: string, isTypeOnly: boolean): ImportBinding[] {
29
+ const bindings: ImportBinding[] = [];
30
+ const marker = isTypeOnly ? 'type' : undefined;
31
+ const push = (name: string, type?: string): void => {
32
+ if (IDENTIFIER.test(name)) bindings.push({ name, ...(type ? { type } : {}) });
33
+ };
34
+
35
+ // default import
36
+ const def = /^(\w+)/.exec(clause);
37
+ if (def) push(def[1], marker);
38
+
39
+ // named imports { a, b as c }
40
+ const braces = /\{([\s\S]*?)\}/.exec(clause);
41
+ if (braces) {
42
+ for (const raw of braces[1].split(',')) {
43
+ const part = raw.trim();
44
+ if (!part) continue;
45
+ const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
46
+ const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
47
+ const memberTypeOnly = /^type\s+/.test(part);
48
+ push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
49
+ }
50
+ }
51
+
52
+ // namespace import * as ns
53
+ const ns = /\*\s+as\s+(\w+)/.exec(clause);
54
+ if (ns) push(ns[1], marker);
55
+
56
+ return bindings;
57
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Class-member scanning for the TypeScript adapter.
3
+ *
4
+ * Extracted from `typescript.ts` to keep that file inside its line budget, and
5
+ * because this is the piece that was wrong: counting members by looking at the
6
+ * character BEFORE a name reported every call site inside a method body as a
7
+ * member. `{ helper(1); }` and `{ async run() { ... }` differ in what follows the
8
+ * name, not in what precedes it -- so depth is what separates them.
9
+ */
10
+
11
+ /** Words that look like `name(` but are control flow, never members. */
12
+ const CONTROL_KEYWORDS = new Set([
13
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new',
14
+ ]);
15
+
16
+ /** Class body members live one level in: the body string starts AT its `{`. */
17
+ const MEMBER_DEPTH = 1;
18
+
19
+ /**
20
+ * Names of the members declared directly in a class body.
21
+ *
22
+ * Depth-aware rather than prefix-aware: a member declaration sits at brace
23
+ * depth 1 of the body, and a call inside a method sits deeper. Nesting is
24
+ * tracked over the MASKED body, so braces inside strings and comments cannot
25
+ * shift the depth -- prose in a JSDoc line like
26
+ * "three separate events (close / error / timeout)" otherwise reads as
27
+ * `events(` and is counted as a method, as is `unknown` from `(): unknown {`.
28
+ *
29
+ * Arrow-function properties (`handler = () => {}`) carry no `name(` shape and
30
+ * are not counted; they were not counted before either.
31
+ */
32
+ export function countTopLevelMembers(body: string): string[] {
33
+ const names: string[] = [];
34
+ let depth = 0;
35
+ for (let i = 0; i < body.length; i += 1) {
36
+ const c = body[i];
37
+ if (c === '{') { depth += 1; continue; }
38
+ if (c === '}') { depth -= 1; continue; }
39
+ if (depth !== MEMBER_DEPTH || c !== '(') continue;
40
+ const name = memberNameBefore(body, i);
41
+ if (name && !CONTROL_KEYWORDS.has(name.value)) names.push(name.value);
42
+ }
43
+ return names;
44
+ }
45
+
46
+ /**
47
+ * The identifier immediately before the `(` at `parenIndex`, skipping modifiers
48
+ * (`public`, `static`, `async`, `readonly`, `get`, `set`, `*`).
49
+ *
50
+ * @returns the name and its offset, or null when no identifier precedes the paren.
51
+ */
52
+ function memberNameBefore(
53
+ body: string,
54
+ parenIndex: number,
55
+ ): { value: string; index: number } | null {
56
+ let end = parenIndex;
57
+ while (end > 0 && /\s/.test(body[end - 1])) end -= 1;
58
+ let start = end;
59
+ while (start > 0 && /[\w$]/.test(body[start - 1])) start -= 1;
60
+ if (start === end) return null;
61
+ return { value: body.substring(start, end), index: start };
62
+ }
63
+
64
+ /**
65
+ * Claim a `name@offset` key once, so overlapping extraction shapes cannot
66
+ * double-count the same declaration.
67
+ */
68
+ export function claimMember(seen: Set<string>, name: string, nameIndex: number): boolean {
69
+ const key = `${name}@${nameIndex}`;
70
+ if (seen.has(key)) return false;
71
+ seen.add(key);
72
+ return true;
73
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Position analysis for TypeScript's regex-based extraction.
3
+ *
4
+ * The extraction regexes match `name(` for both a method DECLARATION and a CALL.
5
+ * This module answers which one it is, keeping `typescript.ts` inside its line
6
+ * budget and giving the rule a place to be tested directly.
7
+ */
8
+
9
+ /**
10
+ * Characters that can only precede a CALL, never a declaration.
11
+ *
12
+ * `.` covers member calls (`this.c()`, `obj.method()`), `(` a call used as an
13
+ * argument (`if (existsSync(p))`), `,` a call in a list (`foo(a, bar(b))`), and
14
+ * the operators cover a call in a condition or expression
15
+ * (`hadExisting && existsSync(p)`, `x = read(p)`, `!isReady(q)`). No member
16
+ * declaration begins with any of them.
17
+ *
18
+ * Deliberately NOT included: `{`, `;`, `}` and plain whitespace. Those precede
19
+ * BOTH shapes -- `{ helper(1); }` is a call, `{ run(): void {` is a declaration
20
+ * -- so using them makes the answer depend on formatting. Depth is what
21
+ * separates those two, which is why extractClasses counts by brace depth.
22
+ */
23
+ const CALL_PRECEDERS = new Set([
24
+ '.', '(', ',',
25
+ '&', '|', '=', '!', '?', ':', '+', '-', '*', '/', '<', '>', '[',
26
+ ]);
27
+
28
+ /**
29
+ * Is the identifier starting at `nameIndex` a call rather than a declaration?
30
+ *
31
+ * Without this check `if (existsSync(p))` was extracted as a method named
32
+ * `existsSync` -- the extraction regex's lookbehind permits `(` before the name
33
+ * -- which then borrowed the enclosing block's brace as a "body" and reported a
34
+ * phantom `missing-error-handling` warning that no edit could clear. It also
35
+ * inflated `extractClasses().methodCount` with the names of whatever the class
36
+ * body happened to call (66 reported for a class declaring 30).
37
+ */
38
+ export function isCallPosition(masked: string, nameIndex: number): boolean {
39
+ let i = nameIndex - 1;
40
+ while (i >= 0 && /\s/.test(masked[i])) i -= 1;
41
+ return i >= 0 && CALL_PRECEDERS.has(masked[i]);
42
+ }
43
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Character-level quote and delimiter tracking for TypeScript source scanning.
3
+ *
4
+ * The param-list and brace scanners all need "is this delimiter inside a string
5
+ * or template?" before they can act on it. That logic is identical for every
6
+ * caller, so it lives here instead of being repeated -- and it keeps
7
+ * `typescript.ts` inside the 1050-line budget.
8
+ */
9
+
10
+ export interface QuoteState {
11
+ inSingle: boolean;
12
+ inDouble: boolean;
13
+ inTemplate: boolean;
14
+ }
15
+
16
+ export function newQuoteState(): QuoteState {
17
+ return { inSingle: false, inDouble: false, inTemplate: false };
18
+ }
19
+
20
+ export function inQuotedState(state: QuoteState): boolean {
21
+ return state.inSingle || state.inDouble || state.inTemplate;
22
+ }
23
+
24
+ /** The three quote kinds tracked by QuoteState, with the flag that marks each. */
25
+ const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
26
+ { quote: "'", flag: 'inSingle' },
27
+ { quote: '"', flag: 'inDouble' },
28
+ { quote: '`', flag: 'inTemplate' },
29
+ ];
30
+
31
+ /**
32
+ * Advance one character of quote tracking.
33
+ *
34
+ * Extracted because `countParams` and `readParenGroup` carried the same
35
+ * six-branch block verbatim (#602): both must ignore delimiters that appear
36
+ * inside a string, and neither may treat an escape as a closing quote.
37
+ *
38
+ * Table-driven (#620): all three quote kinds behave identically -- an escape
39
+ * skips two characters, the matching quote closes the literal -- so they are one
40
+ * rule applied to a list rather than three copies of it.
41
+ *
42
+ * @returns the number of characters consumed (2 when the next char is escaped).
43
+ */
44
+ export function advanceQuoteState(src: string, i: number, state: QuoteState): number {
45
+ const c = src[i];
46
+
47
+ for (const kind of QUOTE_KINDS) {
48
+ if (!state[kind.flag]) continue;
49
+ if (c === '\\') return 2;
50
+ if (c === kind.quote) state[kind.flag] = false;
51
+ return 1;
52
+ }
53
+
54
+ // Not inside any literal: open the one this character starts, if any.
55
+ const opener = QUOTE_KINDS.find(kind => kind.quote === c);
56
+ if (opener) state[opener.flag] = true;
57
+ return 1;
58
+ }
59
+
60
+ /** Opening delimiters that raise the generic/bracket depth in a param list. */
61
+ const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
62
+ /** Closing delimiters that lower it. */
63
+ const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
64
+
65
+ /**
66
+ * Adjust `depth` for one delimiter.
67
+ * @returns true when the character was a delimiter (so it cannot also be a
68
+ * separator), false otherwise.
69
+ */
70
+ export function applyDelimiter(c: string, depth: { value: number }): boolean {
71
+ if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
72
+ if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
73
+ return false;
74
+ }
75
+
76
+ /** True when the character opens a quote/template the scanners must skip. */
77
+ export function isQuote(c: string): boolean {
78
+ return c === "'" || c === '"' || c === '`';
79
+ }
80
+