@boyingliu01/xp-gate 0.35.19 → 0.35.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/hooks/pre-commit +58 -12
  2. package/lib/__tests__/sprint-status-rework-704.test.js +149 -0
  3. package/lib/__tests__/sprint-status-rework.test.js +32 -7
  4. package/lib/__tests__/test-alignment-java-702.test.ts +182 -0
  5. package/lib/__tests__/test-alignment.test.ts +6 -2
  6. package/lib/mattpocock-license-verdict.json +1 -1
  7. package/lib/sprint-status.js +91 -26
  8. package/lib/test-alignment.ts +152 -6
  9. package/mock-policy/AGENTS.md +3 -3
  10. package/mutation/AGENTS.md +3 -3
  11. package/package.json +1 -1
  12. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  13. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  14. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  15. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  16. package/plugins/dsh/lib/index.d.ts +24 -0
  17. package/plugins/dsh/lib/index.d.ts.map +1 -1
  18. package/plugins/dsh/lib/index.js +37 -1
  19. package/plugins/dsh/package.json +1 -1
  20. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  21. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  22. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/dsh/src/delphi-version-footer.test.ts +42 -0
  24. package/plugins/dsh/src/index.ts +38 -1
  25. package/plugins/opencode/package.json +1 -1
  26. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  27. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  28. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  29. package/plugins/qoder/plugin.json +1 -1
  30. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  31. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  32. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  33. package/principles/AGENTS.md +3 -3
  34. package/skills/delphi-review/AGENTS.md +3 -3
  35. package/skills/sprint-flow/AGENTS.md +3 -3
  36. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -221,32 +221,65 @@ function jsonMode(state) {
221
221
  const FIX_RE = /^(fix(\(.+\))?:)|\b(fix|bugfix|hotfix|patch|修复)\b/i;
222
222
 
223
223
  /**
224
- * Count fix commits in a git repo since a given date (#369).
225
- * Scans all branches (--all) per Round 1 repo-wide decision.
224
+ * Git env with every repo-redirecting variable stripped, so `cwd` is the only
225
+ * thing that decides which repository is inspected.
226
+ */
227
+ function cleanGitEnv() {
228
+ const gitEnv = { ...process.env };
229
+ for (const name of [
230
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
231
+ 'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
232
+ 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
233
+ 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
234
+ 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
235
+ ]) {
236
+ delete gitEnv[name];
237
+ }
238
+ return gitEnv;
239
+ }
240
+
241
+ /**
242
+ * Commit counts for a sprint's own time window: start → completion (#704).
243
+ *
244
+ * The old signature took only a lower bound, and the caller passed
245
+ * `completed_at` — by definition the moment the sprint ENDED. Every sprint
246
+ * commit happened before it, so `git log --since <completed_at>` returned an
247
+ * empty set and the rework rate was structurally 0. The interval that matters
248
+ * is "sprint start → sprint end", so both bounds are now explicit and neither
249
+ * can drift away from the other.
250
+ *
251
+ * Returns BOTH numbers from a single `git log` call: the rate is `fix / total`,
252
+ * and `total` was previously read from `state.metrics.total_sprint_commits`,
253
+ * which nothing ever wrote — it was always 0, then floored to 1 by
254
+ * `Math.max(total, 1)`, which is what turned "no data" into a confident 0%.
255
+ *
256
+ * `total === 0` is a meaningful answer (nothing to measure in the window) and is
257
+ * reported as such rather than being dressed up as 0% rework.
258
+ *
226
259
  * @param {string} repoDir - Repository root
227
- * @param {string} sinceDate - ISO 8601 date string
228
- * @returns {number} Count of fix commits
260
+ * @param {string|undefined} sinceDate - Sprint start (ISO 8601); omitted = unbounded
261
+ * @param {string|undefined} untilDate - Sprint completion (ISO 8601); omitted = unbounded
262
+ * @returns {{fix: number, total: number}} counts within the window
229
263
  */
230
- function getFixCommitCount(repoDir, sinceDate) {
264
+ function getWindowCommitCounts(repoDir, sinceDate, untilDate) {
265
+ const empty = { fix: 0, total: 0 };
266
+ if (!sinceDate) return empty;
231
267
  try {
232
- const gitEnv = { ...process.env };
233
- for (const name of [
234
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
235
- 'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
236
- 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
237
- 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
238
- 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
239
- ]) {
240
- delete gitEnv[name];
241
- }
242
- const out = execFileSync(
243
- 'git',
244
- ['log', '--all', '--since', sinceDate, '--pretty=format:%s'],
245
- { cwd: repoDir, env: gitEnv, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
246
- );
247
- return out.split('\n').filter(l => l.trim() && FIX_RE.test(l.trim())).length;
268
+ const args = ['log', '--all'];
269
+ // --since/--until compare committer dates; missing or unparsable values are
270
+ // ignored by git, which would silently widen the window back to "everything".
271
+ // So only pass a bound that is actually a valid timestamp.
272
+ if (!Number.isNaN(new Date(sinceDate).getTime())) args.push('--since', sinceDate);
273
+ if (untilDate && !Number.isNaN(new Date(untilDate).getTime())) args.push('--until', untilDate);
274
+ args.push('--pretty=format:%s');
275
+
276
+ const out = execFileSync('git', args, {
277
+ cwd: repoDir, env: cleanGitEnv(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
278
+ });
279
+ const subjects = out.split('\n').map(l => l.trim()).filter(Boolean);
280
+ return { fix: subjects.filter(s => FIX_RE.test(s)).length, total: subjects.length };
248
281
  } catch {
249
- return 0;
282
+ return empty;
250
283
  }
251
284
  }
252
285
 
@@ -302,13 +335,37 @@ async function handleReworkCheck(searchDir, windowDays) {
302
335
  console.log('─'.repeat(60));
303
336
 
304
337
  let hasAlert = false;
338
+ let hasUnmeasurable = false;
305
339
  for (const { id, state, file } of inWindow) {
306
- const totalCommits = state.metrics.total_sprint_commits || 0;
307
- const fixCommits = getFixCommitCount(searchDir, state.metrics.completed_at);
308
- const rate = fixCommits / Math.max(totalCommits, 1);
340
+ // #704: the window is the sprint itself (start → completion), and both
341
+ // counts come from one `git log` over that window. Previously the total was
342
+ // read from a field nothing wrote, and the `--since` bound was the END of
343
+ // the sprint, so both numbers were structurally 0 and the rate could only
344
+ // ever be 0 — while the summary line still said "within acceptable rate".
345
+ const { fix: fixCommits, total: totalCommits } = getWindowCommitCounts(
346
+ searchDir,
347
+ state.started_at,
348
+ state.metrics.completed_at,
349
+ );
350
+
351
+ // No commits in the window means the rate is undefined, not zero. Reporting
352
+ // 0.0% here would rebuild the same false reassurance one level down, so the
353
+ // sprint is surfaced as unmeasurable and never counts as "within threshold".
354
+ if (totalCommits === 0) {
355
+ hasUnmeasurable = true;
356
+ console.log(` ${id}: 0 fix / 0 total commits — NOT MEASURABLE (no commits in sprint window)`);
357
+ continue;
358
+ }
359
+
360
+ const rate = fixCommits / totalCommits;
309
361
 
310
- // Write rework_rate back to the source file
362
+ // Persist what was actually measured. The previous code wrote the rate
363
+ // unconditionally, which meant a transposed 0 could overwrite a value a
364
+ // human had already corrected; now the denominator is persisted alongside
365
+ // it so the next run can be checked against its own evidence.
311
366
  state.metrics.rework_rate = rate;
367
+ state.metrics.total_sprint_commits = totalCommits;
368
+ state.metrics.fix_sprint_commits = fixCommits;
312
369
  try {
313
370
  fs.writeFileSync(file, JSON.stringify(state, null, 2));
314
371
  } catch { /* non-fatal */ }
@@ -324,6 +381,10 @@ async function handleReworkCheck(searchDir, windowDays) {
324
381
  console.log('─'.repeat(60));
325
382
  if (hasAlert) {
326
383
  console.log('⚠️ Rework rate alert: one or more sprints exceed 30% threshold');
384
+ } else if (hasUnmeasurable) {
385
+ // "All sprints within acceptable rate" would be a claim about sprints that
386
+ // were never measured. Say which is which instead of picking the cheerful one.
387
+ console.log('⚠️ Some sprints could not be measured (no commits in window) — not certified as within threshold');
327
388
  } else {
328
389
  console.log('All sprints within acceptable rework rate (≤30%)');
329
390
  }
@@ -493,4 +554,8 @@ module.exports = {
493
554
  jsonMode,
494
555
  isStale,
495
556
  handleSprintStatus,
557
+ // Exported for #704: the rework-rate arithmetic was only reachable through
558
+ // the CLI, so the only way to test it was to shell out and parse console
559
+ // output. A function nobody can call directly is a function nobody can pin.
560
+ handleReworkCheck,
496
561
  };
@@ -73,8 +73,42 @@ export interface TestMap {
73
73
  tests: TestCase[];
74
74
  totalTests: number;
75
75
  totalAssertions: number;
76
+ /**
77
+ * Test files whose extension has no parser (#702).
78
+ *
79
+ * These used to be dropped on the floor: the file was read, its extension
80
+ * taken, and then no branch matched. A pure-Java project therefore reported
81
+ * `totalTests: 0` and computed its alignment score over an empty set, with
82
+ * nothing distinguishing "no tests" from "tests I could not read". Callers
83
+ * must surface this instead of treating the map as complete.
84
+ */
85
+ unsupportedFiles: string[];
76
86
  }
77
87
 
88
+ /**
89
+ * Test-file extensions that this module can parse (#702).
90
+ *
91
+ * Listed explicitly rather than implied by the presence of an `if` branch,
92
+ * because that is exactly how `.java` disappeared: an if/else chain with a
93
+ * missing arm looks identical to a complete one at the call site. This list is
94
+ * the contract the branches are checked against.
95
+ */
96
+ export const SUPPORTED_TEST_EXTENSIONS: readonly string[] = [
97
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.java',
98
+ ];
99
+
100
+ /**
101
+ * Test-file extensions this module deliberately does NOT parse, but recognises
102
+ * as tests (#702).
103
+ *
104
+ * Being on this list means "report it", not "ignore it". The distinction
105
+ * matters: before, both "not a test file" and "a test file I cannot read"
106
+ * produced the same silent skip.
107
+ */
108
+ export const UNSUPPORTED_TEST_EXTENSIONS: readonly string[] = [
109
+ '.kt', '.kts', '.cs', '.rb', '.rs', '.swift', '.php', '.scala',
110
+ ];
111
+
78
112
  export type IssueType =
79
113
  | 'MISSING_REQUIREMENT_TEST'
80
114
  | 'UNCOVERED_ACCEPTANCE_CRITERIA'
@@ -211,7 +245,13 @@ function extractAllTags(text: string, tag: string): string[] {
211
245
  }
212
246
 
213
247
  function countAssertions(content: string): number {
214
- // Count common assertion patterns
248
+ // Count common assertion patterns.
249
+ //
250
+ // #702: the JUnit 5 family was absent, so a parsed Java test would have been
251
+ // recorded as zero-assertion — the same "detector that never fires" shape as
252
+ // the missing branch itself. A test with assertions that reports 0 is worse
253
+ // than no test at all: it drags the coverage dimension down without any hint
254
+ // that the number is unmeasured.
215
255
  const patterns = [
216
256
  /expect\(/g,
217
257
  /assert\./g,
@@ -220,6 +260,12 @@ function countAssertions(content: string): number {
220
260
  /expect\./g,
221
261
  /toEqual\(/g,
222
262
  /toBe\(/g,
263
+ // JUnit 5 / Jupiter assertions (Java, Kotlin). Matched by the assert-prefix
264
+ // family rather than an enumeration of every overload, so assertAll,
265
+ // assertThat, assertThrows, assertDoesNotThrow etc. are all counted.
266
+ /\bassert[A-Z]\w*\s*\(/g,
267
+ // AssertJ fluent style, common in the same test suites.
268
+ /\bassertThat\s*\(/g,
223
269
  ];
224
270
  let count = 0;
225
271
  for (const pattern of patterns) {
@@ -387,6 +433,23 @@ function extractGoTestBody(content: string, funcIndex: number): string {
387
433
  return extractCallbackBody(content, braceIdx);
388
434
  }
389
435
 
436
+ /**
437
+ * Body of a Java test method, from its opening brace (#702).
438
+ *
439
+ * Assertions are counted against the per-test body, not the whole file, for the
440
+ * same reason as the Go/Python paths: a file-level count multiplies assertions
441
+ * by the number of tests in the file (#392-#1).
442
+ *
443
+ * Java has no multi-line lambda-less ambiguity here — a method body is the
444
+ * brace-delimited block that follows the parameter list — so the same brace
445
+ * scan used for Go is sufficient.
446
+ */
447
+ function extractJavaTestBody(content: string, methodIndex: number): string {
448
+ const braceIdx = content.indexOf('{', methodIndex);
449
+ if (braceIdx === -1) return '';
450
+ return extractCallbackBody(content, braceIdx);
451
+ }
452
+
390
453
  function pushTestWithCovers(
391
454
  tests: TestCase[],
392
455
  testPath: string,
@@ -423,6 +486,7 @@ function pushTestWithCovers(
423
486
  export function parseTestFiles(testPaths: string[]): TestMap {
424
487
  const tests: TestCase[] = [];
425
488
  const totalAssertionsObj = { value: 0 };
489
+ const unsupportedFiles: string[] = [];
426
490
 
427
491
  for (const testPath of testPaths) {
428
492
  if (!fs.existsSync(testPath)) continue;
@@ -470,6 +534,48 @@ export function parseTestFiles(testPaths: string[]): TestMap {
470
534
  const body = extractGoTestBody(content, funcStart);
471
535
  pushTestWithCovers(tests, testPath, m[3]!, m[2]!, intentMatch?.[1]?.trim(), content, body, totalAssertionsObj);
472
536
  }
537
+ } else if (ext === '.java') {
538
+ // #702: JUnit 5 annotations are matched against the Javadoc block that
539
+ // precedes the method, because that is where this project's @test/@intent
540
+ // contract lives.
541
+ //
542
+ // Two phases rather than one regex, because the Javadoc may sit above the
543
+ // class declaration (`class Foo { @Test void a() {...} }`) or directly
544
+ // above the method. Scanning forward from the end of each Javadoc for the
545
+ // first `@Test` and then for the method name handles both without the
546
+ // fragile "skip N tokens" shape that a single expression needs.
547
+ const javaDocPattern = /\/\*\*([\s\S]*?)\*\//g;
548
+ let docMatch: RegExpExecArray | null;
549
+ while ((docMatch = javaDocPattern.exec(content)) !== null) {
550
+ const docBlock = docMatch[1]!;
551
+ const reqMatch = docBlock.match(/@test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+)/);
552
+ // Without a requirement tag this is not an annotated test in this
553
+ // project's sense; skip it rather than inventing a TestCase.
554
+ if (!reqMatch) continue;
555
+
556
+ const afterDoc = docMatch.index + docMatch[0].length;
557
+ const rest = content.slice(afterDoc);
558
+ const testAnnotation = rest.match(/@Test\b/);
559
+ if (!testAnnotation) continue;
560
+
561
+ // Method name = the identifier right before the first `(` after @Test.
562
+ const afterAnnotation = rest.slice(testAnnotation.index! + '@Test'.length);
563
+ const nameMatch = afterAnnotation.match(/\b(\w+)\s*\(/);
564
+ if (!nameMatch) continue;
565
+
566
+ const intentMatch = docBlock.match(/@intent\s+(.+)/);
567
+ const methodStart = afterDoc + testAnnotation.index! + afterAnnotation.indexOf(nameMatch[1]!);
568
+ const body = extractJavaTestBody(content, methodStart);
569
+ pushTestWithCovers(
570
+ tests, testPath, nameMatch[1]!, reqMatch[1]!, intentMatch?.[1]?.trim(),
571
+ content, body, totalAssertionsObj,
572
+ );
573
+ }
574
+ } else if (UNSUPPORTED_TEST_EXTENSIONS.includes(ext)) {
575
+ // Recognised as a test file, but no parser exists. Recording it is the
576
+ // whole point: previously this arm did not exist, so the file vanished
577
+ // and the caller could not tell "no tests" from "tests I could not read".
578
+ unsupportedFiles.push(testPath);
473
579
  }
474
580
  }
475
581
 
@@ -477,6 +583,7 @@ export function parseTestFiles(testPaths: string[]): TestMap {
477
583
  tests,
478
584
  totalTests: tests.length,
479
585
  totalAssertions: totalAssertionsObj.value,
586
+ unsupportedFiles,
480
587
  };
481
588
  }
482
589
 
@@ -702,6 +809,31 @@ export function writeReport(report: TestAlignmentReportFile, outputPath: string)
702
809
  // Orchestrator: runFullAlignment()
703
810
  // ============================================================================
704
811
 
812
+ /**
813
+ * Whether a file name looks like a test file (#702).
814
+ *
815
+ * There were TWO silent gates, and fixing only the parser would have left the
816
+ * outer one closed: this walker previously matched only
817
+ * `\.(test|spec)\.(ts|tsx|js|jsx|py|go)$`. A JUnit file is conventionally
818
+ * `ProbeTest.java` — no `.test.` infix at all — so it was excluded before
819
+ * parseTestFiles ever saw it, and no amount of parser work could help.
820
+ *
821
+ * Two accepted shapes:
822
+ * - the JS/Python convention: `<name>.test.<ext>` / `<name>.spec.<ext>`
823
+ * - the Java/C#/Kotlin convention: `*Test.<ext>` / `*Tests.<ext>` / `*Spec.<ext>`
824
+ *
825
+ * Both are matched against the same extension lists as the parser, so a file
826
+ * can never be collected but unparseable, or parseable but never collected.
827
+ */
828
+ function isTestFileName(name: string): boolean {
829
+ const ext = path.extname(name);
830
+ const knownExt = SUPPORTED_TEST_EXTENSIONS.includes(ext) || UNSUPPORTED_TEST_EXTENSIONS.includes(ext);
831
+ if (!knownExt) return false;
832
+ if (/\.(test|spec)\.[^.]+$/.test(name)) return true;
833
+ // Java convention: the class name carries the marker, not the file name infix.
834
+ return /(?:Test|Tests|Spec)\.[^.]+$/.test(name);
835
+ }
836
+
705
837
  export interface AlignmentOptions {
706
838
  specPath: string;
707
839
  testDirs?: string[];
@@ -743,7 +875,7 @@ export function runFullAlignment(options: AlignmentOptions): TestAlignmentReport
743
875
  const fullPath = path.join(d, entry.name);
744
876
  if (entry.isDirectory() && entry.name !== 'node_modules') {
745
877
  walkDir(fullPath);
746
- } else if (entry.isFile() && /\.(test|spec)\.(ts|tsx|js|jsx|py|go)$/.test(entry.name)) {
878
+ } else if (entry.isFile() && isTestFileName(entry.name)) {
747
879
  testFiles.push(fullPath);
748
880
  }
749
881
  }
@@ -759,9 +891,23 @@ export function runFullAlignment(options: AlignmentOptions): TestAlignmentReport
759
891
  const alignmentReport = verifyAlignment(spec, testMap);
760
892
  const score = calculateScore(alignmentReport.issues, alignmentReport.coverage, testMap);
761
893
 
894
+ // #702: files that were recognised as tests but could not be parsed must not
895
+ // leave the report looking complete. Without this, `totalTests: 0` from a
896
+ // pure-Java project is indistinguishable from "this project has no tests",
897
+ // and the score is computed over an empty set while the status reads "ran".
898
+ const unsupportedErrors = testMap.unsupportedFiles.map(
899
+ file => `Test file not parsed (no parser for ${path.extname(file)}): ${file}`,
900
+ );
901
+ // A project whose ONLY test files are unparsed is the worst case: the report
902
+ // would otherwise be a confident verdict about nothing at all.
903
+ const noParsedTestsAtAll = testMap.totalTests === 0 && testMap.unsupportedFiles.length > 0;
904
+ // Unparsed files make the score untrustworthy even when it happens to be
905
+ // high, so they block PASS rather than merely being annotated.
906
+ const canPass = score >= 80 && testMap.unsupportedFiles.length === 0;
907
+
762
908
  const report: TestAlignmentReportFile = {
763
- alignment_status: score >= 80 ? 'PASS' : 'FAIL',
764
- phase: score >= 80 ? 2 : 1,
909
+ alignment_status: canPass ? 'PASS' : 'FAIL',
910
+ phase: canPass ? 2 : 1,
765
911
  score,
766
912
  head_commit: headCommit || 'unknown',
767
913
  spec_hash: specHash,
@@ -771,8 +917,8 @@ export function runFullAlignment(options: AlignmentOptions): TestAlignmentReport
771
917
  spec_requirement: issue.requirementId || 'unknown',
772
918
  gap: issue.message,
773
919
  })),
774
- anti_pattern_detected: false,
775
- errors: [],
920
+ anti_pattern_detected: noParsedTestsAtAll,
921
+ errors: unsupportedErrors,
776
922
  };
777
923
 
778
924
  // Step 5: Write output. A failure is recorded on `report.errors` by writeReport
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.35.19",
3
+ "version": "0.35.20",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.19",
3
+ "version": "0.35.20",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. 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-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.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-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.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-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -33,6 +33,30 @@ export declare const inject: string[];
33
33
  * let it through, and a false negative would reject a perfectly valid reference.
34
34
  */
35
35
  export declare function findPlaceholderName(value: string): string | undefined;
36
+ /**
37
+ * The running plugin's version, read from its own package.json (#709).
38
+ *
39
+ * Delphi's tool result used to carry no version at all, which made "the fix did
40
+ * not take effect" indistinguishable from "the fix was never made". That cost a
41
+ * real investigation: four design reviews returned verdicts without a single
42
+ * line of expert reasoning, the design was rewritten twice on the assumption it
43
+ * must be wrong, and the same reviews reached 100% consensus on round two once
44
+ * the reasoning was readable. Nothing was wrong with the design.
45
+ *
46
+ * Read rather than injected so dev runs and packed installs report the same
47
+ * thing, and so a stale load is visible as an old number instead of silence.
48
+ * Falls back to "unknown" rather than throwing: a version banner must never be
49
+ * the reason a review fails to start.
50
+ */
51
+ export declare function pluginVersion(): string;
52
+ /**
53
+ * The provider/seats banner that closes every Delphi tool result (#709).
54
+ *
55
+ * Extracted from the inline template so the version claim has exactly one
56
+ * definition — and so it can be asserted against package.json instead of being
57
+ * pinned to a literal that silently starts lying at the next release.
58
+ */
59
+ export declare function formatPluginFooter(provider: string, described: string): string;
36
60
  /**
37
61
  * Runtime schema for the keys above.
38
62
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,MAAM;;;GAA6C,CAAA;AAElG,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AAkQlD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AA8ID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4BjB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CAgC9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,MAAM;;;GAA6C,CAAA;AAElG,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AAkQlD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9E;AA8ID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4BjB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CAgC9D"}
@@ -1,4 +1,5 @@
1
1
  import { writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import { join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -274,6 +275,41 @@ export function findPlaceholderName(value) {
274
275
  const envStyle = /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/.exec(value);
275
276
  return envStyle ? envStyle[1] : undefined;
276
277
  }
278
+ /**
279
+ * The running plugin's version, read from its own package.json (#709).
280
+ *
281
+ * Delphi's tool result used to carry no version at all, which made "the fix did
282
+ * not take effect" indistinguishable from "the fix was never made". That cost a
283
+ * real investigation: four design reviews returned verdicts without a single
284
+ * line of expert reasoning, the design was rewritten twice on the assumption it
285
+ * must be wrong, and the same reviews reached 100% consensus on round two once
286
+ * the reasoning was readable. Nothing was wrong with the design.
287
+ *
288
+ * Read rather than injected so dev runs and packed installs report the same
289
+ * thing, and so a stale load is visible as an old number instead of silence.
290
+ * Falls back to "unknown" rather than throwing: a version banner must never be
291
+ * the reason a review fails to start.
292
+ */
293
+ export function pluginVersion() {
294
+ try {
295
+ const require_ = createRequire(import.meta.url);
296
+ const pkg = require_("../package.json");
297
+ return typeof pkg.version === "string" && pkg.version !== "" ? pkg.version : "unknown";
298
+ }
299
+ catch {
300
+ return "unknown";
301
+ }
302
+ }
303
+ /**
304
+ * The provider/seats banner that closes every Delphi tool result (#709).
305
+ *
306
+ * Extracted from the inline template so the version claim has exactly one
307
+ * definition — and so it can be asserted against package.json instead of being
308
+ * pinned to a literal that silently starts lying at the next release.
309
+ */
310
+ export function formatPluginFooter(provider, described) {
311
+ return `Provider: ${provider} | plugin: ${pluginVersion()}\nSeats: ${described}`;
312
+ }
277
313
  async function runDelphiReview(subject, request) {
278
314
  const { addressed, cwd, resolveApiKey, signal, writeEvidence } = request;
279
315
  try {
@@ -282,7 +318,7 @@ async function runDelphiReview(subject, request) {
282
318
  const suffix = writeEvidence && cfg !== undefined
283
319
  ? await writeWalkthroughEvidence(cwd, cfg, outcome)
284
320
  : "";
285
- return `${formatOutcome(outcome)}\n\nProvider: ${provider}\nSeats: ${described}${suffix}`;
321
+ return `${formatOutcome(outcome)}\n\n${formatPluginFooter(provider, described)}${suffix}`;
286
322
  }
287
323
  catch (e) {
288
324
  // Config problems are the common failure; report them as guidance
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.19",
3
+ "version": "0.35.20",
4
4
  "description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.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-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.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-11-01
4
- **Commit:** effea31d
3
+ **Generated:** 2026-11-02
4
+ **Commit:** 60545e49
5
5
  **Branch:** main
6
- **Version:** 0.35.19.0
6
+ **Version:** 0.35.20.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.