@boyingliu01/xp-gate 0.14.22 → 0.14.23

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.
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PKG_ROOT = path.resolve(__dirname, '..');
8
+ const REPO_ROOT = path.resolve(PKG_ROOT, '..', '..');
9
+
10
+ const CORE_SKILLS = [
11
+ 'admin-template-guidelines',
12
+ 'delphi-review',
13
+ 'improve-codebase-architecture',
14
+ 'ralph-loop',
15
+ 'sprint-flow',
16
+ 'test-driven-development',
17
+ 'test-specification-alignment',
18
+ 'to-issues',
19
+ ];
20
+
21
+ const PLUGINS = ['claude-code', 'opencode', 'qoder'];
22
+
23
+ // Plugins whose `skills/` subtree is gitignored (build artifact). For these,
24
+ // the source-of-truth skill content lives in repo-root `skills/` and must be
25
+ // injected during sync so the npm-package mirror is complete even on a clean
26
+ // checkout (e.g. CI Mirror Parity job). Plugins not listed here are expected
27
+ // to ship their own committed `skills/` subtree (e.g. `plugins/qoder/skills/`).
28
+ const PLUGINS_WITH_GITIGNORED_SKILLS = new Set(['claude-code', 'opencode']);
29
+
30
+ function rmrf(target) {
31
+ if (!fs.existsSync(target)) return;
32
+ fs.rmSync(target, { recursive: true, force: true });
33
+ }
34
+
35
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.opencode']);
36
+ const SKIP_FILES = new Set(['package-lock.json']);
37
+ const SKIP_FILE_SUFFIXES = ['.lock', '.js.map'];
38
+
39
+ function shouldSkipFile(name) {
40
+ if (SKIP_FILES.has(name)) return true;
41
+ return SKIP_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
42
+ }
43
+
44
+ function copyDir(src, dest) {
45
+ if (!fs.existsSync(src)) {
46
+ console.error(`[sync] SKIP (missing): ${src}`);
47
+ return false;
48
+ }
49
+ fs.mkdirSync(dest, { recursive: true });
50
+ const entries = fs.readdirSync(src, { withFileTypes: true });
51
+ for (const entry of entries) {
52
+ if (SKIP_DIRS.has(entry.name)) continue;
53
+ const srcPath = path.join(src, entry.name);
54
+ const destPath = path.join(dest, entry.name);
55
+ if (entry.isDirectory()) {
56
+ copyDir(srcPath, destPath);
57
+ } else if (entry.isFile()) {
58
+ if (shouldSkipFile(entry.name)) continue;
59
+ fs.copyFileSync(srcPath, destPath);
60
+ }
61
+ }
62
+ return true;
63
+ }
64
+
65
+ function syncSkills() {
66
+ const destRoot = path.join(PKG_ROOT, 'skills');
67
+ rmrf(destRoot);
68
+ fs.mkdirSync(destRoot, { recursive: true });
69
+ let copied = 0;
70
+ for (const name of CORE_SKILLS) {
71
+ const src = path.join(REPO_ROOT, 'skills', name);
72
+ const dest = path.join(destRoot, name);
73
+ if (copyDir(src, dest)) {
74
+ copied += 1;
75
+ console.error(`[sync] skills/${name}`);
76
+ }
77
+ }
78
+ return copied;
79
+ }
80
+
81
+ function injectCanonicalSkills(pluginDest) {
82
+ const skillsDest = path.join(pluginDest, 'skills');
83
+ rmrf(skillsDest);
84
+ fs.mkdirSync(skillsDest, { recursive: true });
85
+ for (const name of CORE_SKILLS) {
86
+ const src = path.join(REPO_ROOT, 'skills', name);
87
+ const dest = path.join(skillsDest, name);
88
+ if (!copyDir(src, dest)) {
89
+ console.error(`[sync] ERROR: missing canonical skill ${name}`);
90
+ process.exit(1);
91
+ }
92
+ }
93
+ }
94
+
95
+ // tui-plugin.ts was removed in v0.13.0 (slimming). The TUI panel approach
96
+ // was abandoned after multiple unsuccessful integration attempts.
97
+
98
+ function syncPlugins() {
99
+ const destRoot = path.join(PKG_ROOT, 'plugins');
100
+ rmrf(destRoot);
101
+ fs.mkdirSync(destRoot, { recursive: true });
102
+ let copied = 0;
103
+ for (const name of PLUGINS) {
104
+ const src = path.join(REPO_ROOT, 'plugins', name);
105
+ const dest = path.join(destRoot, name);
106
+ if (copyDir(src, dest)) {
107
+ if (PLUGINS_WITH_GITIGNORED_SKILLS.has(name)) {
108
+ injectCanonicalSkills(dest);
109
+ }
110
+ // tui-plugin.ts was removed in v0.13.0 — no import rewriting needed.
111
+ copied += 1;
112
+ console.error(`[sync] plugins/${name}`);
113
+ }
114
+ }
115
+ return copied;
116
+ }
117
+
118
+ function syncScripts() {
119
+ const srcRoot = path.join(REPO_ROOT, 'scripts');
120
+ const destRoot = path.join(PKG_ROOT, 'scripts');
121
+ const scriptsToSync = ['delphi-external-review.cjs'];
122
+ let copied = 0;
123
+ for (const name of scriptsToSync) {
124
+ const src = path.join(srcRoot, name);
125
+ if (fs.existsSync(src)) {
126
+ fs.mkdirSync(destRoot, { recursive: true });
127
+ fs.copyFileSync(src, path.join(destRoot, name));
128
+ copied += 1;
129
+ console.error(`[sync] scripts/${name}`);
130
+ }
131
+ }
132
+ return copied;
133
+ }
134
+
135
+ function syncAdapters() {
136
+ const srcRoot = path.join(REPO_ROOT, 'githooks', 'adapters');
137
+ const destRoot = path.join(PKG_ROOT, 'adapters');
138
+ if (!fs.existsSync(srcRoot)) {
139
+ console.error(`[sync] SKIP adapters (missing): ${srcRoot}`);
140
+ return 0;
141
+ }
142
+ rmrf(destRoot);
143
+ fs.mkdirSync(destRoot, { recursive: true });
144
+ const entries = fs.readdirSync(srcRoot, { withFileTypes: true });
145
+ let copied = 0;
146
+ for (const entry of entries) {
147
+ const srcPath = path.join(srcRoot, entry.name);
148
+ const destPath = path.join(destRoot, entry.name);
149
+ if (entry.isDirectory()) {
150
+ copyDir(srcPath, destPath);
151
+ copied += 1;
152
+ } else if (entry.isFile() && entry.name.endsWith('.sh')) {
153
+ fs.copyFileSync(srcPath, destPath);
154
+ copied += 1;
155
+ }
156
+ }
157
+ console.error(`[sync] adapters/ (${copied} entries)`);
158
+
159
+ // Sync gate scripts (gate-*.sh) from githooks/ to package root so they ship
160
+ // with the npm package and can be installed to the global adapter dir.
161
+ const githooksDir = path.join(REPO_ROOT, 'githooks');
162
+ const gateFiles = fs.readdirSync(githooksDir).filter(f =>
163
+ (f.startsWith('gate-') || f === 'sprint-gate.sh') && f.endsWith('.sh')
164
+ );
165
+ for (const f of gateFiles) {
166
+ fs.copyFileSync(path.join(githooksDir, f), path.join(PKG_ROOT, f));
167
+ }
168
+ if (gateFiles.length > 0) {
169
+ console.error(`[sync] gate scripts: ${gateFiles.join(', ')}`);
170
+ }
171
+ return copied + gateFiles.length;
172
+ }
173
+
174
+ function syncModules(moduleName) {
175
+ const src = path.join(REPO_ROOT, 'src', moduleName);
176
+ const dest = path.join(PKG_ROOT, moduleName);
177
+ if (!copyDir(src, dest)) {
178
+ console.error(`[sync] ERROR: missing module ${moduleName}`);
179
+ process.exit(1);
180
+ }
181
+ console.error(`[sync] ${moduleName}/`);
182
+ return 1;
183
+ }
184
+
185
+ function extractPreCommitGateNumbers(preCommitContent) {
186
+ const SCRIPT_META_GATES = new Set([]);
187
+ const gates = new Set();
188
+ for (const m of preCommitContent.matchAll(/Gate (\d+):/g)) {
189
+ if (!SCRIPT_META_GATES.has(m[1])) {
190
+ gates.add(m[1]);
191
+ }
192
+ }
193
+ return gates;
194
+ }
195
+
196
+ function extractReadmeGateNumbers(readmeContent, section) {
197
+ const gates = new Set();
198
+ for (const m of readmeContent.matchAll(section)) {
199
+ gates.add(m[1]);
200
+ }
201
+ return gates;
202
+ }
203
+
204
+ function compareGateSets(scriptGates, readmeGates, label, sortFn) {
205
+ const scriptOnly = [...scriptGates].filter((g) => !readmeGates.has(g));
206
+ const readmeOnly = [...readmeGates].filter((g) => !scriptGates.has(g));
207
+
208
+ if (scriptOnly.length > 0 || readmeOnly.length > 0) {
209
+ const hookFile = label.toLowerCase();
210
+ console.error(`[drift-check] ERROR: ${label} Gate table drift detected!`);
211
+ const sortedScript = [...scriptGates].sort(sortFn);
212
+ const sortedReadme = [...readmeGates].sort(sortFn);
213
+ console.error(` Script githooks/${hookFile} has ${scriptGates.size} gates: [${sortedScript.join(', ')}]`);
214
+ console.error(` README.md documents ${readmeGates.size} gates: [${sortedReadme.join(', ')}]`);
215
+ if (scriptOnly.length > 0) {
216
+ const sorted = sortFn ? scriptOnly.sort(sortFn) : scriptOnly;
217
+ console.error(` In script but NOT in README: Gate ${sorted.join(', Gate ')}`);
218
+ }
219
+ if (readmeOnly.length > 0) {
220
+ const sorted = sortFn ? readmeOnly.sort(sortFn) : readmeOnly;
221
+ console.error(` In README but NOT in script: Gate ${sorted.join(', Gate ')}`);
222
+ }
223
+ console.error(` Fix: Update README.md Gate table to match githooks/${hookFile}`);
224
+ }
225
+ return { scriptOnly, readmeOnly };
226
+ }
227
+
228
+ function checkPreCommitDrift(preCommitPath, readmePath) {
229
+ let scriptContent = '';
230
+ if (fs.existsSync(preCommitPath)) {
231
+ scriptContent = fs.readFileSync(preCommitPath, 'utf8');
232
+ } else {
233
+ console.error('[drift-check] WARN: githooks/pre-commit not found, skipping pre-commit check');
234
+ return null;
235
+ }
236
+
237
+ const scriptGates = extractPreCommitGateNumbers(scriptContent);
238
+
239
+ let readmeContent = '';
240
+ if (fs.existsSync(readmePath)) {
241
+ readmeContent = fs.readFileSync(readmePath, 'utf8');
242
+ } else {
243
+ console.error('[drift-check] WARN: README.md not found, skipping drift check');
244
+ return true;
245
+ }
246
+
247
+ const readmeGates = extractReadmeGateNumbers(readmeContent, /\| Gate (\d+) \|/g);
248
+
249
+ if (scriptGates.size > 0 && readmeGates.size > 0) {
250
+ const numericSort = (a, b) => +a - +b;
251
+ const { scriptOnly, readmeOnly } = compareGateSets(scriptGates, readmeGates, 'Pre-commit', numericSort);
252
+ if (scriptOnly.length > 0 || readmeOnly.length > 0) {
253
+ return false;
254
+ }
255
+ console.error(`[drift-check] OK: pre-commit ${scriptGates.size} gates match README`);
256
+ }
257
+ return true;
258
+ }
259
+
260
+ function checkPrePushDrift(prePushPath, readmePath) {
261
+ let scriptContent = '';
262
+ if (fs.existsSync(prePushPath)) {
263
+ scriptContent = fs.readFileSync(prePushPath, 'utf8');
264
+ } else {
265
+ console.error('[drift-check] WARN: githooks/pre-push not found, skipping pre-push check');
266
+ return null;
267
+ }
268
+
269
+ const scriptGates = extractReadmeGateNumbers(scriptContent, /[Gg][Aa][Tt][Ee] (M\d*)/g);
270
+
271
+ let readmeContent = '';
272
+ if (fs.existsSync(readmePath)) {
273
+ readmeContent = fs.readFileSync(readmePath, 'utf8');
274
+ } else {
275
+ return true;
276
+ }
277
+
278
+ const readmeGates = extractReadmeGateNumbers(readmeContent, /\| Gate (M\d*) \|/g);
279
+
280
+ if (scriptGates.size > 0 && readmeGates.size > 0) {
281
+ const { scriptOnly, readmeOnly } = compareGateSets(scriptGates, readmeGates, 'Pre-push');
282
+ if (scriptOnly.length > 0 || readmeOnly.length > 0) {
283
+ return false;
284
+ }
285
+ console.error(`[drift-check] OK: pre-push ${scriptGates.size} gates match README`);
286
+ }
287
+ return true;
288
+ }
289
+
290
+ /**
291
+ * Docs-drift check (REQ-TDD-005): compare Gate table in README.md against
292
+ * actual gate definitions in githooks/pre-commit and githooks/pre-push.
293
+ * Blocks publish if counts diverge.
294
+ */
295
+ /**
296
+ * Adapter mirror drift check (REQ-329): compares githooks/adapters/ (source of truth)
297
+ * against src/npm-package/adapters/ (npm mirror). Content must be byte-identical;
298
+ * any mismatch blocks publish because syncAdapters() would overwrite the mirror.
299
+ */
300
+ function checkAdapterDrift(srcRootPath, mirrorRootPath) {
301
+ const srcRoot = srcRootPath || path.join(REPO_ROOT, 'githooks', 'adapters');
302
+ const mirrorRoot = mirrorRootPath || path.join(PKG_ROOT, 'adapters');
303
+
304
+ if (!fs.existsSync(srcRoot)) {
305
+ console.error('[drift-check] WARN: githooks/adapters not found, skipping adapter drift check');
306
+ return true;
307
+ }
308
+ if (!fs.existsSync(mirrorRoot)) {
309
+ console.error('[drift-check] WARN: src/npm-package/adapters not found, skipping adapter drift check');
310
+ return true;
311
+ }
312
+
313
+ const crypto = require('crypto');
314
+
315
+ /**
316
+ * Recursively compute a sorted, deterministic hash table for a directory.
317
+ * Keys are relative paths (POSIX separators); values are SHA-256 hex digests.
318
+ */
319
+ function hashDirectory(root) {
320
+ const table = {};
321
+ function walk(dir, base) {
322
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
323
+ // Sort for determinism
324
+ entries.sort((a, b) => a.name.localeCompare(b.name));
325
+ for (const entry of entries) {
326
+ if (SKIP_DIRS.has(entry.name)) continue;
327
+ const full = path.join(dir, entry.name);
328
+ const rel = path.posix.join(base, entry.name);
329
+ if (entry.isDirectory()) {
330
+ walk(full, rel);
331
+ } else if (entry.isFile()) {
332
+ if (shouldSkipFile(entry.name)) continue;
333
+ const buf = fs.readFileSync(full);
334
+ table[rel] = crypto.createHash('sha256').update(buf).digest('hex');
335
+ }
336
+ }
337
+ }
338
+ walk(root, '');
339
+ return table;
340
+ }
341
+
342
+ const srcHashes = hashDirectory(srcRoot);
343
+ const mirrorHashes = hashDirectory(mirrorRoot);
344
+
345
+ // Check for files only in one side
346
+ const srcOnly = Object.keys(srcHashes).filter((f) => !(f in mirrorHashes));
347
+ const mirrorOnly = Object.keys(mirrorHashes).filter((f) => !(f in srcHashes));
348
+
349
+ // Check for content mismatches
350
+ const mismatched = [];
351
+ for (const f of Object.keys(srcHashes)) {
352
+ if (mirrorHashes[f] && srcHashes[f] !== mirrorHashes[f]) {
353
+ mismatched.push(f);
354
+ }
355
+ }
356
+
357
+ if (srcOnly.length > 0) {
358
+ console.error(`[drift-check] ERROR: ${srcOnly.length} file(s) in githooks/adapters/ missing from npm-package mirror:`);
359
+ for (const f of srcOnly) console.error(` - ${f}`);
360
+ }
361
+ if (mirrorOnly.length > 0) {
362
+ console.error(`[drift-check] ERROR: ${mirrorOnly.length} file(s) in npm-package mirror NOT in githooks/adapters/:`);
363
+ for (const f of mirrorOnly) console.error(` - ${f}`);
364
+ }
365
+ if (mismatched.length > 0) {
366
+ console.error(`[drift-check] ERROR: ${mismatched.length} adapter file(s) have drifted (SHA-256 mismatch):`);
367
+ for (const f of mismatched) console.error(` - ${f}`);
368
+ console.error('[drift-check] Fix: Run `node src/npm-package/scripts/sync-package-content.js` (or `npm pack`) to resync.');
369
+ }
370
+
371
+ if (srcOnly.length > 0 || mirrorOnly.length > 0 || mismatched.length > 0) {
372
+ return false;
373
+ }
374
+
375
+ const totalSrc = Object.keys(srcHashes).length;
376
+ const totalMirror = Object.keys(mirrorHashes).length;
377
+ console.error(`[drift-check] OK: ${totalSrc} adapter files match between githooks/adapters/ and npm-package mirror (${totalMirror} files)`);
378
+ return true;
379
+ }
380
+
381
+ function checkDocsDrift(preCommitPath, prePushPath, readmePath, agentsPath) {
382
+ const preCommitScript = preCommitPath || path.join(REPO_ROOT, 'githooks', 'pre-commit');
383
+ const prePushScript = prePushPath || path.join(REPO_ROOT, 'githooks', 'pre-push');
384
+ const readme = readmePath || path.join(REPO_ROOT, 'README.md');
385
+
386
+ const preCommitResult = checkPreCommitDrift(preCommitScript, readme);
387
+ if (preCommitResult === false) return false;
388
+
389
+ const prePushResult = checkPrePushDrift(prePushScript, readme);
390
+ if (prePushResult === false) return false;
391
+
392
+ return true;
393
+ }
394
+
395
+ function main() {
396
+ console.error(`[sync] repo root: ${REPO_ROOT}`);
397
+ console.error(`[sync] package root: ${PKG_ROOT}`);
398
+ if (checkDocsDrift() === false) {
399
+ process.exit(1);
400
+ }
401
+ if (checkAdapterDrift() === false) {
402
+ process.exit(1);
403
+ }
404
+ const skills = syncSkills();
405
+ const plugins = syncPlugins();
406
+ const scripts = syncScripts();
407
+ const adapters = syncAdapters();
408
+ const principles = syncModules('principles');
409
+ const mutation = syncModules('mutation');
410
+ const mockPolicy = syncModules('mock-policy');
411
+ const buildIntegrity = syncModules('build-integrity');
412
+ console.error(`[sync] done: ${skills} skill(s), ${plugins} plugin(s), ${scripts} script(s), ${adapters} adapter entries, ${principles + mutation + mockPolicy + buildIntegrity} module(s)`);
413
+ if (skills !== CORE_SKILLS.length) {
414
+ console.error(`[sync] ERROR: expected ${CORE_SKILLS.length} skills, copied ${skills}`);
415
+ process.exit(1);
416
+ }
417
+ if (plugins !== PLUGINS.length) {
418
+ console.error(`[sync] ERROR: expected ${PLUGINS.length} plugins, copied ${plugins}`);
419
+ process.exit(1);
420
+ }
421
+
422
+ // tui-plugin removed in v0.13.0 — validation skipped.
423
+ }
424
+
425
+ if (require.main !== module) {
426
+ module.exports = { checkDocsDrift, checkAdapterDrift };
427
+ }
428
+
429
+ main();
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-21
4
- **Commit:** 03d6f26
4
+ **Commit:** 03e9223
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.23.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-21
4
- **Commit:** 03d6f26
4
+ **Commit:** 03e9223
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.23.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
3
  **Generated:** 2026-07-21
4
- **Commit:** 03d6f26
4
+ **Commit:** 03e9223
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.23.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.