@boyingliu01/xp-gate 0.15.5 → 0.18.1
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.
- package/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +29 -1
- package/gate-3.sh +0 -0
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +0 -0
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-3.sh +0 -0
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +0 -0
- package/hooks/lib/now-ms.sh +33 -0
- package/hooks/pre-commit +63 -47
- package/hooks/pre-push +0 -0
- package/hooks/sprint-gate.sh +291 -0
- package/lib/__tests__/check-version.test.js +122 -0
- package/lib/__tests__/doctor.test.js +13 -15
- package/lib/__tests__/gate-audit.test.ts +38 -0
- package/lib/__tests__/phase-transition.test.js +512 -272
- package/lib/__tests__/retro.test.js +131 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/__tests__/sprint-status-rework.test.js +184 -0
- package/lib/check-version.js +9 -0
- package/lib/doctor-tui.js +268 -198
- package/lib/doctor.js +15 -3
- package/lib/gate-audit.ts +18 -7
- package/lib/gate-runner.js +77 -30
- package/lib/init.js +10 -0
- package/lib/language-tools.js +833 -833
- package/lib/mattpocock-license-verdict.json +7 -0
- package/lib/phase-transition.js +499 -249
- package/lib/retro.js +222 -0
- package/lib/shared-utils.js +7 -0
- package/lib/sprint-audit.js +235 -235
- package/lib/sprint-init.js +207 -0
- package/lib/sprint-state-migrator.js +1 -0
- package/lib/sprint-status.js +111 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +3 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
- package/plugins/opencode/index.ts +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
- package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/opencode/skills/grilling/SKILL.md +12 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
- package/plugins/qoder/agents/delphi-architecture.md +48 -48
- package/plugins/qoder/agents/delphi-feasibility.md +48 -48
- package/plugins/qoder/agents/delphi-technical.md +48 -48
- package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
- package/scripts/__tests__/sync-package-content.test.js +50 -0
- package/scripts/check-mattpocock-license.cjs +302 -0
- package/scripts/delphi-external-review.cjs +432 -432
- package/scripts/migrate-audit-anomalies.mjs +78 -0
- package/scripts/sync-package-content.js +52 -5
- package/skills/batch-grill-me/SKILL.md +15 -0
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +59 -4
- package/skills/delphi-review/references/requirements.md +390 -0
- package/skills/delphi-review/references/round-templates.md +67 -0
- package/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +74 -0
- package/skills/grill-with-docs/SKILL.md +7 -0
- package/skills/grilling/SKILL.md +12 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +168 -43
- package/skills/sprint-flow/references/components/memory.md +1 -1
- package/skills/sprint-flow/references/components/middleware.md +1 -1
- package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/skills/sprint-flow/references/phase-overview.md +58 -53
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/skills/test-specification-alignment/SKILL.md +8 -3
- package/skills/to-issues/SKILL.md +1 -1
- package/adapters/gate-3.sh +0 -68
- package/adapters/gate-4.sh +0 -80
- package/adapters/gate-7.sh +0 -45
- package/adapters/gate-8.sh +0 -56
- package/adapters/gate-9.sh +0 -104
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-25
|
|
4
|
+
**Commit:** 3150b22
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.18.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-25
|
|
4
|
+
**Commit:** 3150b22
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.18.0.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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test C0-mattpocock-license-gate
|
|
3
|
+
* @intent Verify check-mattpocock-license.cjs enforces the C0 publish gate:
|
|
4
|
+
* (a) valid MIT license → PASS (exit 0);
|
|
5
|
+
* (b) corrupted license → FAIL (exit 1), missing license → clean-room path (exit 2);
|
|
6
|
+
* (c) network failure with valid cache → offline fallback (exit 0, source "cache").
|
|
7
|
+
* All scenarios run hermetically: no real GitHub calls (local 404 server / --offline).
|
|
8
|
+
* @covers AC-C0-01, AC-C0-02, AC-C0-03
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const SCRIPT = path.resolve(__dirname, '..', 'check-mattpocock-license.cjs');
|
|
19
|
+
|
|
20
|
+
// Valid MIT license text (mirrors mattpocock/skills LICENSE, verified 2026-07-26).
|
|
21
|
+
const VALID_MIT_LICENSE = `MIT License
|
|
22
|
+
|
|
23
|
+
Copyright (c) 2026 Matt Pocock
|
|
24
|
+
|
|
25
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
26
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
27
|
+
in the Software without restriction, including without limitation the rights
|
|
28
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
29
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
30
|
+
furnished to do so, subject to the following conditions:
|
|
31
|
+
|
|
32
|
+
The above copyright notice and this permission notice shall be included in all
|
|
33
|
+
copies or substantial portions of the Software.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
38
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
39
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
40
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
41
|
+
SOFTWARE.
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const CORRUPTED_LICENSE = 'PROPRIETARY LICENSE\nAll rights reserved. Redistribution prohibited.\n';
|
|
45
|
+
|
|
46
|
+
function createTempDir() {
|
|
47
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'license-check-test-'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cleanupTempDir(dir) {
|
|
51
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build hermetic env overrides so the script never touches the real network
|
|
56
|
+
* or the real repo paths. Default URL points at a closed localhost port
|
|
57
|
+
* (instant ECONNREFUSED) — individual tests override with a local 404 server.
|
|
58
|
+
*/
|
|
59
|
+
function buildEnv(tmpDir, licenseUrl) {
|
|
60
|
+
return {
|
|
61
|
+
...process.env,
|
|
62
|
+
XPGATE_LICENSE_URL: licenseUrl || 'http://127.0.0.1:9/LICENSE',
|
|
63
|
+
XPGATE_LICENSE_CACHE: path.join(tmpDir, 'mattpocock-license.cache.md'),
|
|
64
|
+
XPGATE_LICENSE_VERDICT: path.join(tmpDir, 'mattpocock-license-verdict.json'),
|
|
65
|
+
XPGATE_CLEAN_ROOM_DECISION: path.join(tmpDir, 'clean-room-decision.json'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runScript(args, env) {
|
|
70
|
+
const result = spawnSync(process.execPath, [SCRIPT, ...args], {
|
|
71
|
+
env,
|
|
72
|
+
stdio: 'pipe',
|
|
73
|
+
timeout: 30000,
|
|
74
|
+
});
|
|
75
|
+
const verdictPath = env.XPGATE_LICENSE_VERDICT;
|
|
76
|
+
return {
|
|
77
|
+
status: result.status,
|
|
78
|
+
stderr: (result.stderr || Buffer.alloc(0)).toString(),
|
|
79
|
+
stdout: (result.stdout || Buffer.alloc(0)).toString(),
|
|
80
|
+
verdict: fs.existsSync(verdictPath) ? JSON.parse(fs.readFileSync(verdictPath, 'utf8')) : null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Start a localhost HTTP server that always returns 404 (fake dead license URL). */
|
|
85
|
+
function start404Server() {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const server = http.createServer((req, res) => {
|
|
88
|
+
res.statusCode = 404;
|
|
89
|
+
res.end('Not Found');
|
|
90
|
+
});
|
|
91
|
+
server.listen(0, '127.0.0.1', () => {
|
|
92
|
+
const { port } = server.address();
|
|
93
|
+
resolve({ server, url: `http://127.0.0.1:${port}/LICENSE` });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stopServer(server) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
server.close(() => resolve());
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('check-mattpocock-license (C0 gate)', () => {
|
|
105
|
+
let tmpDir;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
tmpDir = createTempDir();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
cleanupTempDir(tmpDir);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Scenario (a): valid MIT license → PASS ────────────────────────────
|
|
116
|
+
it('(a) exits 0 with PASS verdict when the cache holds a valid MIT license (--offline)', () => {
|
|
117
|
+
const env = buildEnv(tmpDir);
|
|
118
|
+
fs.writeFileSync(env.XPGATE_LICENSE_CACHE, VALID_MIT_LICENSE, 'utf8');
|
|
119
|
+
|
|
120
|
+
const r = runScript(['--offline'], env);
|
|
121
|
+
|
|
122
|
+
expect(r.status).toBe(0);
|
|
123
|
+
expect(r.verdict).not.toBeNull();
|
|
124
|
+
expect(r.verdict.verdict).toBe('PASS');
|
|
125
|
+
expect(r.verdict.license_type).toBe('MIT');
|
|
126
|
+
expect(r.verdict.source).toBe('cache');
|
|
127
|
+
expect(r.verdict.sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
128
|
+
expect(r.verdict.checked_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Scenario (b): corrupted / missing LICENSE → FAIL ──────────────────
|
|
132
|
+
it('(b1) exits 1 with FAIL verdict and clean-room mitigation when the license is corrupted', () => {
|
|
133
|
+
const env = buildEnv(tmpDir);
|
|
134
|
+
fs.writeFileSync(env.XPGATE_LICENSE_CACHE, CORRUPTED_LICENSE, 'utf8');
|
|
135
|
+
|
|
136
|
+
const r = runScript(['--offline'], env);
|
|
137
|
+
|
|
138
|
+
expect(r.status).toBe(1);
|
|
139
|
+
expect(r.verdict).not.toBeNull();
|
|
140
|
+
expect(r.verdict.verdict).toBe('FAIL');
|
|
141
|
+
expect(r.stderr).toMatch(/clean-room/i);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('(b2) exits 2 with clean-room rewrite instruction when the license is missing (404 + no cache)', async () => {
|
|
145
|
+
const { server, url } = await start404Server();
|
|
146
|
+
try {
|
|
147
|
+
const env = buildEnv(tmpDir, url);
|
|
148
|
+
// No cache file written → license entirely missing.
|
|
149
|
+
|
|
150
|
+
const r = runScript([], env);
|
|
151
|
+
|
|
152
|
+
// Exit 2 per MUST-DO §6: "LICENSE missing or unrecognizable" triggers the clean-room path.
|
|
153
|
+
expect(r.status).toBe(2);
|
|
154
|
+
expect(r.verdict).not.toBeNull();
|
|
155
|
+
expect(r.verdict.verdict).toBe('FAIL');
|
|
156
|
+
expect(r.verdict.source).toBe('none');
|
|
157
|
+
expect(r.stderr).toMatch(/clean-room rewrite/i);
|
|
158
|
+
} finally {
|
|
159
|
+
await stopServer(server);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Scenario (c): offline fallback to cache ───────────────────────────
|
|
164
|
+
it('(c) exits 0 with source "cache" when the network fails but a valid cache exists', async () => {
|
|
165
|
+
const { server, url } = await start404Server();
|
|
166
|
+
try {
|
|
167
|
+
const env = buildEnv(tmpDir, url);
|
|
168
|
+
fs.writeFileSync(env.XPGATE_LICENSE_CACHE, VALID_MIT_LICENSE, 'utf8');
|
|
169
|
+
|
|
170
|
+
// No --offline flag: network is attempted first, fails (404), then falls back to cache.
|
|
171
|
+
const r = runScript([], env);
|
|
172
|
+
|
|
173
|
+
expect(r.status).toBe(0);
|
|
174
|
+
expect(r.verdict).not.toBeNull();
|
|
175
|
+
expect(r.verdict.verdict).toBe('PASS');
|
|
176
|
+
expect(r.verdict.license_type).toBe('MIT');
|
|
177
|
+
expect(r.verdict.source).toBe('cache');
|
|
178
|
+
expect(r.stderr).toMatch(/offline cache/i);
|
|
179
|
+
} finally {
|
|
180
|
+
await stopServer(server);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── --force-clean-room override ───────────────────────────────────────
|
|
185
|
+
it('records a clean-room decision and exits 0 with --force-clean-room', () => {
|
|
186
|
+
const env = buildEnv(tmpDir);
|
|
187
|
+
// No cache, dead network URL — without the override this would exit 2.
|
|
188
|
+
|
|
189
|
+
const r = runScript(['--force-clean-room'], env);
|
|
190
|
+
|
|
191
|
+
expect(r.status).toBe(0);
|
|
192
|
+
expect(fs.existsSync(env.XPGATE_CLEAN_ROOM_DECISION)).toBe(true);
|
|
193
|
+
const decision = JSON.parse(fs.readFileSync(env.XPGATE_CLEAN_ROOM_DECISION, 'utf8'));
|
|
194
|
+
expect(decision.decision).toBe('clean-room-rewrite');
|
|
195
|
+
expect(decision.recorded_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── Unit tests for the pure validation function (in-process) ──────────────
|
|
200
|
+
describe('validateLicense (unit)', () => {
|
|
201
|
+
const { validateLicense, sha256, EXIT_PASS, EXIT_FAIL, EXIT_MISSING } = require('../check-mattpocock-license.cjs');
|
|
202
|
+
|
|
203
|
+
it('accepts a valid MIT license', () => {
|
|
204
|
+
const result = validateLicense(VALID_MIT_LICENSE);
|
|
205
|
+
expect(result.valid).toBe(true);
|
|
206
|
+
expect(result.licenseType).toBe('MIT');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('rejects a proprietary license', () => {
|
|
210
|
+
const result = validateLicense(CORRUPTED_LICENSE);
|
|
211
|
+
expect(result.valid).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('rejects empty input', () => {
|
|
215
|
+
expect(validateLicense('').valid).toBe(false);
|
|
216
|
+
expect(validateLicense(null).valid).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('computes a stable SHA-256 hex digest', () => {
|
|
220
|
+
expect(sha256('MIT License')).toBe(sha256('MIT License'));
|
|
221
|
+
expect(sha256('MIT License')).toMatch(/^[0-9a-f]{64}$/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('exports distinct exit code constants', () => {
|
|
225
|
+
expect(EXIT_PASS).toBe(0);
|
|
226
|
+
expect(EXIT_FAIL).toBe(1);
|
|
227
|
+
expect(EXIT_MISSING).toBe(2);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -262,6 +262,56 @@ describe('syncGateScripts', () => {
|
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* @test REQ-450 sync-hooks
|
|
267
|
+
* @intent Verify syncHooks() copies githooks pre-commit, pre-push, adapter-common.sh,
|
|
268
|
+
* and lib/ subdirectory to src/npm-package/hooks/
|
|
269
|
+
* @covers AC-450-01, AC-450-02, AC-450-03, AC-450-04
|
|
270
|
+
*/
|
|
271
|
+
describe('syncHooks', () => {
|
|
272
|
+
const WORKTREE_PATH = path.resolve(__dirname, '..', '..', '..', '..');
|
|
273
|
+
const PKG_ROOT = path.join(WORKTREE_PATH, 'src', 'npm-package');
|
|
274
|
+
const HOOKS_DIR = path.join(PKG_ROOT, 'hooks');
|
|
275
|
+
const syncScript = path.join(WORKTREE_PATH, 'src', 'npm-package', 'scripts', 'sync-package-content.js');
|
|
276
|
+
const nodeExe = process.execPath;
|
|
277
|
+
|
|
278
|
+
// Ensure hooks are re-synced once before tests run; share the result.
|
|
279
|
+
let syncOk = false;
|
|
280
|
+
beforeAll(() => {
|
|
281
|
+
const result = spawnSync(nodeExe, [syncScript], {
|
|
282
|
+
cwd: WORKTREE_PATH,
|
|
283
|
+
stdio: 'pipe',
|
|
284
|
+
timeout: 15000,
|
|
285
|
+
});
|
|
286
|
+
syncOk = result.status === 0;
|
|
287
|
+
if (!syncOk) {
|
|
288
|
+
console.error('sync stderr:', result.stderr?.toString() || '(empty)');
|
|
289
|
+
console.error('sync stdout:', result.stdout?.toString() || '(empty)');
|
|
290
|
+
}
|
|
291
|
+
}, 15000);
|
|
292
|
+
|
|
293
|
+
it('syncHooks copies pre-commit to hooks dir', () => {
|
|
294
|
+
expect(syncOk, 'sync script must exit 0').toBe(true);
|
|
295
|
+
expect(fs.existsSync(path.join(HOOKS_DIR, 'pre-commit')), 'pre-commit must exist').toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('syncHooks copies pre-push to hooks dir', () => {
|
|
299
|
+
expect(syncOk, 'sync script must exit 0').toBe(true);
|
|
300
|
+
expect(fs.existsSync(path.join(HOOKS_DIR, 'pre-push')), 'pre-push must exist').toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('syncHooks copies adapter-common.sh to hooks dir', () => {
|
|
304
|
+
expect(syncOk, 'sync script must exit 0').toBe(true);
|
|
305
|
+
expect(fs.existsSync(path.join(HOOKS_DIR, 'adapter-common.sh')), 'adapter-common.sh must exist').toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('syncHooks copies lib/now-ms.sh to hooks/lib dir', () => {
|
|
309
|
+
expect(syncOk, 'sync script must exit 0').toBe(true);
|
|
310
|
+
const libFile = path.join(HOOKS_DIR, 'lib', 'now-ms.sh');
|
|
311
|
+
expect(fs.existsSync(libFile), `lib/now-ms.sh must exist in hooks dir: ${libFile}`).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
265
315
|
/**
|
|
266
316
|
* @test REQ-329 adapter-mirror-drift
|
|
267
317
|
* @intent Verify checkAdapterDrift() detects file count and content mismatches
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* C0 License Verification Gate (v0.18.0 Phase C0)
|
|
6
|
+
*
|
|
7
|
+
* Verifies that mattpocock/skills ships a permissive (MIT) license before the
|
|
8
|
+
* 4 built-in Matt Pocock skills (grilling, grill-with-docs, batch-grill-me,
|
|
9
|
+
* domain-modeling) are packed into the npm distribution. Design doc §6/§12/§16.4:
|
|
10
|
+
* a confirmed permissive license is a publish blocker; without it the skills
|
|
11
|
+
* must be clean-room rewritten from the functional spec instead of copied.
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Fetch https://raw.githubusercontent.com/mattpocock/skills/main/LICENSE
|
|
15
|
+
* (10s timeout, one retry). On success, refresh the offline cache.
|
|
16
|
+
* 2. On network failure, fall back to the offline cache
|
|
17
|
+
* (.xp-gate/mattpocock-license.cache.md).
|
|
18
|
+
* 3. Validate the text is a compliant MIT license (regex: "MIT License"
|
|
19
|
+
* header + grant clause) and record SHA-256 as evidence.
|
|
20
|
+
* 4. Write a verdict JSON to lib/mattpocock-license-verdict.json.
|
|
21
|
+
*
|
|
22
|
+
* Exit codes:
|
|
23
|
+
* 0 PASS — MIT license confirmed (network or cache)
|
|
24
|
+
* 1 FAIL — license obtained but NOT a compliant MIT license
|
|
25
|
+
* 2 MISSING — license could not be obtained at all (triggers clean-room path)
|
|
26
|
+
*
|
|
27
|
+
* CLI flags:
|
|
28
|
+
* --offline Skip network; use the offline cache only.
|
|
29
|
+
* --refresh Re-fetch from network and update the cache
|
|
30
|
+
* (default flow already does this; explicit for clarity).
|
|
31
|
+
* --force-clean-room Record a clean-room rewrite decision to
|
|
32
|
+
* .xp-gate/clean-room-decision.json and exit 0 for this
|
|
33
|
+
* publish only (original skill text must NOT be copied).
|
|
34
|
+
*
|
|
35
|
+
* Env overrides (used by tests for hermetic execution):
|
|
36
|
+
* XPGATE_LICENSE_URL license URL to fetch
|
|
37
|
+
* XPGATE_LICENSE_CACHE offline cache file path
|
|
38
|
+
* XPGATE_LICENSE_VERDICT verdict JSON output path
|
|
39
|
+
* XPGATE_CLEAN_ROOM_DECISION clean-room decision JSON path
|
|
40
|
+
*
|
|
41
|
+
* Zero runtime dependencies (Node built-ins only).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const crypto = require('crypto');
|
|
47
|
+
const https = require('https');
|
|
48
|
+
const http = require('http');
|
|
49
|
+
|
|
50
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
51
|
+
const REPO_ROOT = path.resolve(PKG_ROOT, '..', '..');
|
|
52
|
+
|
|
53
|
+
const DEFAULT_LICENSE_URL = 'https://raw.githubusercontent.com/mattpocock/skills/main/LICENSE';
|
|
54
|
+
const NETWORK_TIMEOUT_MS = 10000;
|
|
55
|
+
const MAX_RETRIES = 1;
|
|
56
|
+
const MAX_REDIRECTS = 3;
|
|
57
|
+
|
|
58
|
+
const LICENSE_URL = process.env.XPGATE_LICENSE_URL || DEFAULT_LICENSE_URL;
|
|
59
|
+
const CACHE_PATH = process.env.XPGATE_LICENSE_CACHE || path.join(REPO_ROOT, '.xp-gate', 'mattpocock-license.cache.md');
|
|
60
|
+
const VERDICT_PATH = process.env.XPGATE_LICENSE_VERDICT || path.join(PKG_ROOT, 'lib', 'mattpocock-license-verdict.json');
|
|
61
|
+
const CLEAN_ROOM_PATH = process.env.XPGATE_CLEAN_ROOM_DECISION || path.join(REPO_ROOT, '.xp-gate', 'clean-room-decision.json');
|
|
62
|
+
|
|
63
|
+
const EXIT_PASS = 0;
|
|
64
|
+
const EXIT_FAIL = 1; // license obtained but non-compliant
|
|
65
|
+
const EXIT_MISSING = 2; // license missing/unrecognizable → clean-room path
|
|
66
|
+
|
|
67
|
+
function sha256(text) {
|
|
68
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate that license text is a compliant MIT license.
|
|
73
|
+
* Regex-based (tolerates copyright-year changes upstream) rather than a fixed
|
|
74
|
+
* hash, with the SHA-256 recorded separately as tamper evidence.
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @returns {{ valid: boolean, licenseType: string|null, reason: string }}
|
|
77
|
+
*/
|
|
78
|
+
function validateLicense(text) {
|
|
79
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
80
|
+
return { valid: false, licenseType: null, reason: 'license text is empty' };
|
|
81
|
+
}
|
|
82
|
+
const hasMitHeader = /MIT License/i.test(text);
|
|
83
|
+
const hasGrantClause = /Permission is hereby granted, free of charge/i.test(text);
|
|
84
|
+
const hasNoWarranty = /THE SOFTWARE IS PROVIDED "AS IS"/i.test(text);
|
|
85
|
+
const hasExpectedCopyright = /Copyright \(c\)\s+\d{4}\s+Matt Pocock/i.test(text);
|
|
86
|
+
|
|
87
|
+
if (hasMitHeader && hasGrantClause && hasNoWarranty) {
|
|
88
|
+
return {
|
|
89
|
+
valid: true,
|
|
90
|
+
licenseType: 'MIT',
|
|
91
|
+
reason: hasExpectedCopyright
|
|
92
|
+
? 'MIT License with Matt Pocock copyright notice'
|
|
93
|
+
: 'MIT License (copyright holder differs from expected "Matt Pocock")',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
valid: false,
|
|
98
|
+
licenseType: null,
|
|
99
|
+
reason: 'text does not match MIT License terms (missing "MIT License" header, grant clause, or warranty disclaimer)',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Fetch a URL, following up to MAX_REDIRECTS redirects.
|
|
105
|
+
* Supports https: and http: (http only used by tests against a local server).
|
|
106
|
+
* @param {string} url
|
|
107
|
+
* @param {{ timeoutMs?: number, redirectsLeft?: number }} [options]
|
|
108
|
+
* @returns {Promise<string>}
|
|
109
|
+
*/
|
|
110
|
+
function fetchUrl(url, options) {
|
|
111
|
+
const timeoutMs = (options && options.timeoutMs) || NETWORK_TIMEOUT_MS;
|
|
112
|
+
const redirectsLeft = options && typeof options.redirectsLeft === 'number' ? options.redirectsLeft : MAX_REDIRECTS;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let mod;
|
|
116
|
+
if (url.startsWith('https:')) mod = https;
|
|
117
|
+
else if (url.startsWith('http:')) mod = http;
|
|
118
|
+
else {
|
|
119
|
+
reject(new Error(`unsupported URL scheme: ${url}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const req = mod.get(url, (res) => {
|
|
124
|
+
const status = res.statusCode || 0;
|
|
125
|
+
if (status >= 300 && status < 400 && res.headers.location && redirectsLeft > 0) {
|
|
126
|
+
res.resume();
|
|
127
|
+
const next = new URL(res.headers.location, url).toString();
|
|
128
|
+
fetchUrl(next, { timeoutMs, redirectsLeft: redirectsLeft - 1 }).then(resolve, reject);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (status !== 200) {
|
|
132
|
+
res.resume();
|
|
133
|
+
reject(new Error(`HTTP ${status} for ${url}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const chunks = [];
|
|
137
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
138
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
139
|
+
res.on('error', reject);
|
|
140
|
+
});
|
|
141
|
+
req.setTimeout(timeoutMs, () => {
|
|
142
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms fetching ${url}`));
|
|
143
|
+
});
|
|
144
|
+
req.on('error', reject);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fetch with one retry after the initial attempt fails.
|
|
150
|
+
* @param {string} url
|
|
151
|
+
* @param {number} [retries]
|
|
152
|
+
* @returns {Promise<string>}
|
|
153
|
+
*/
|
|
154
|
+
async function fetchWithRetry(url, retries) {
|
|
155
|
+
const attempts = (typeof retries === 'number' ? retries : MAX_RETRIES) + 1;
|
|
156
|
+
let lastErr;
|
|
157
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
return await fetchUrl(url);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
lastErr = err;
|
|
162
|
+
console.error(`[license-check] network attempt ${attempt}/${attempts} failed: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw lastErr;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeJson(filePath, obj) {
|
|
169
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
170
|
+
fs.writeFileSync(filePath, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleForceCleanRoom() {
|
|
174
|
+
const recordedAt = new Date().toISOString();
|
|
175
|
+
const decision = {
|
|
176
|
+
decision: 'clean-room-rewrite',
|
|
177
|
+
recorded_at: recordedAt,
|
|
178
|
+
note: 'mattpocock/skills LICENSE missing or non-compliant at publish time. '
|
|
179
|
+
+ 'The 4 built-in skills (grilling, grill-with-docs, batch-grill-me, domain-modeling) '
|
|
180
|
+
+ 'must be clean-room rewritten from the functional spec (interview discipline / '
|
|
181
|
+
+ 'CONTEXT.md+ADR formats / batch decision protocol) — original expression must NOT be copied.',
|
|
182
|
+
};
|
|
183
|
+
writeJson(CLEAN_ROOM_PATH, decision);
|
|
184
|
+
writeJson(VERDICT_PATH, {
|
|
185
|
+
verdict: 'FAIL',
|
|
186
|
+
license_type: null,
|
|
187
|
+
sha256: null,
|
|
188
|
+
checked_at: recordedAt,
|
|
189
|
+
source: 'clean-room-decision',
|
|
190
|
+
clean_room: true,
|
|
191
|
+
});
|
|
192
|
+
console.error(`[license-check] clean-room decision recorded: ${CLEAN_ROOM_PATH}`);
|
|
193
|
+
console.error('[license-check] WARNING: publishing under clean-room override — verbatim skill text must NOT be shipped until the rewrite lands.');
|
|
194
|
+
process.exit(EXIT_PASS);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function main() {
|
|
198
|
+
const args = process.argv.slice(2);
|
|
199
|
+
const offline = args.includes('--offline');
|
|
200
|
+
const refresh = args.includes('--refresh');
|
|
201
|
+
|
|
202
|
+
if (args.includes('--force-clean-room')) {
|
|
203
|
+
handleForceCleanRoom();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let text = null;
|
|
208
|
+
let source = null;
|
|
209
|
+
|
|
210
|
+
if (offline) {
|
|
211
|
+
console.error('[license-check] --offline: skipping network fetch');
|
|
212
|
+
} else {
|
|
213
|
+
if (refresh) console.error('[license-check] --refresh: re-fetching license from network');
|
|
214
|
+
try {
|
|
215
|
+
text = await fetchWithRetry(LICENSE_URL);
|
|
216
|
+
source = 'network';
|
|
217
|
+
// Refresh the offline cache on every successful network fetch.
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
|
|
220
|
+
fs.writeFileSync(CACHE_PATH, text, 'utf8');
|
|
221
|
+
console.error(`[license-check] offline cache updated: ${CACHE_PATH}`);
|
|
222
|
+
} catch (cacheErr) {
|
|
223
|
+
console.error(`[license-check] WARN: failed to update offline cache: ${cacheErr.message}`);
|
|
224
|
+
}
|
|
225
|
+
} catch (netErr) {
|
|
226
|
+
console.error(`[license-check] network unavailable: ${netErr.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (text === null && fs.existsSync(CACHE_PATH)) {
|
|
231
|
+
text = fs.readFileSync(CACHE_PATH, 'utf8');
|
|
232
|
+
source = 'cache';
|
|
233
|
+
console.error(`[license-check] using offline cache: ${CACHE_PATH}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const checkedAt = new Date().toISOString();
|
|
237
|
+
|
|
238
|
+
if (text === null) {
|
|
239
|
+
writeJson(VERDICT_PATH, {
|
|
240
|
+
verdict: 'FAIL',
|
|
241
|
+
license_type: null,
|
|
242
|
+
sha256: null,
|
|
243
|
+
checked_at: checkedAt,
|
|
244
|
+
source: 'none',
|
|
245
|
+
error: 'license missing: network fetch failed and no offline cache available',
|
|
246
|
+
});
|
|
247
|
+
console.error('[license-check] ERROR: mattpocock/skills LICENSE could not be obtained (network failed, no cache).');
|
|
248
|
+
console.error('[license-check] This is a publish blocker (design §6 C0 license gate).');
|
|
249
|
+
console.error('[license-check] Mitigation: start a clean-room rewrite of the 4 built-in skills from the functional spec,');
|
|
250
|
+
console.error('[license-check] or run `npm run license-check -- --force-clean-room` to record the clean-room rewrite path for this publish.');
|
|
251
|
+
process.exit(EXIT_MISSING);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = validateLicense(text);
|
|
255
|
+
const digest = sha256(text);
|
|
256
|
+
|
|
257
|
+
if (!result.valid) {
|
|
258
|
+
writeJson(VERDICT_PATH, {
|
|
259
|
+
verdict: 'FAIL',
|
|
260
|
+
license_type: result.licenseType,
|
|
261
|
+
sha256: digest,
|
|
262
|
+
checked_at: checkedAt,
|
|
263
|
+
source,
|
|
264
|
+
reason: result.reason,
|
|
265
|
+
});
|
|
266
|
+
console.error(`[license-check] FAIL: ${result.reason}`);
|
|
267
|
+
console.error(`[license-check] sha256: ${digest} (source: ${source})`);
|
|
268
|
+
console.error('[license-check] The license is NOT a compliant MIT license — verbatim redistribution of the skills is prohibited.');
|
|
269
|
+
console.error('[license-check] Mitigation: run `npm run license-check -- --force-clean-room` to record the clean-room rewrite path.');
|
|
270
|
+
process.exit(EXIT_FAIL);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
writeJson(VERDICT_PATH, {
|
|
274
|
+
verdict: 'PASS',
|
|
275
|
+
license_type: 'MIT',
|
|
276
|
+
sha256: digest,
|
|
277
|
+
checked_at: checkedAt,
|
|
278
|
+
source,
|
|
279
|
+
});
|
|
280
|
+
console.error(`[license-check] PASS: MIT License confirmed (${result.reason})`);
|
|
281
|
+
console.error(`[license-check] sha256: ${digest}`);
|
|
282
|
+
console.error(`[license-check] source: ${source}`);
|
|
283
|
+
console.error(`[license-check] verdict written: ${VERDICT_PATH}`);
|
|
284
|
+
process.exit(EXIT_PASS);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (require.main === module) {
|
|
288
|
+
main().catch((err) => {
|
|
289
|
+
console.error(`[license-check] unexpected error: ${err.message}`);
|
|
290
|
+
process.exit(EXIT_MISSING);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
validateLicense,
|
|
296
|
+
sha256,
|
|
297
|
+
fetchUrl,
|
|
298
|
+
fetchWithRetry,
|
|
299
|
+
EXIT_PASS,
|
|
300
|
+
EXIT_FAIL,
|
|
301
|
+
EXIT_MISSING,
|
|
302
|
+
};
|