@lifeaitools/rdc-skills 0.24.0 → 0.24.2
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/.claude-plugin/plugin.json +1 -1
- package/git-sha.json +1 -1
- package/hooks/lib/run-evidence-gate.mjs +174 -0
- package/hooks/require-work-item-on-commit.js +265 -55
- package/hooks/work-item-exit-gate.js +260 -2
- package/package.json +2 -1
- package/scripts/install-rdc-skills.js +8 -3
- package/skills/build/SKILL.md +10 -0
- package/tests/require-work-item-on-commit.test.mjs +153 -0
- package/tests/run-evidence-gate.test.mjs +82 -0
- package/tests/work-item-exit-gate-l2.test.mjs +210 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Truth Gate 3.0 — Layer 2 (FUSED evidence gate) tests.
|
|
4
|
+
*
|
|
5
|
+
* Exercises EVERY rejection branch of work-item-exit-gate.js verifyLayer2 plus
|
|
6
|
+
* the fully-valid close, against a REAL throwaway git repo (no live DB). The
|
|
7
|
+
* gate's git/disk checks run against RDC_TRUTH_GATE_REPO, which we point at the
|
|
8
|
+
* throwaway repo. verifyLayer2 throws GateDenied on rejection; we assert on the
|
|
9
|
+
* thrown reason. A fully-valid close throws nothing.
|
|
10
|
+
*
|
|
11
|
+
* Branches proven:
|
|
12
|
+
* 1. commit-not-resolving (free-typed bad SHA) -> DENY
|
|
13
|
+
* 2. commit-not-captured (real SHA, not in L1 set) -> DENY
|
|
14
|
+
* 3. files ∉ commit -> DENY
|
|
15
|
+
* 4. file not on disk (in commit, deleted from tree) -> DENY
|
|
16
|
+
* 5. prose verification ("HTTP 200") -> DENY
|
|
17
|
+
* 6. witness:"agent" -> DENY
|
|
18
|
+
* 7. fully-valid close (real SHA captured, files in -> ALLOW (no throw)
|
|
19
|
+
* commit + on disk, machine artifact, valid witness)
|
|
20
|
+
* 8. fail-closed: any internal error during L2 -> DENY (via main wrapper)
|
|
21
|
+
*
|
|
22
|
+
* Also a fused-primitive assertion: run_evidence_gate emits a verdict ONLY
|
|
23
|
+
* after running the command (no verdict without a run).
|
|
24
|
+
*
|
|
25
|
+
* Run: node tests/work-item-exit-gate-l2.test.mjs
|
|
26
|
+
*/
|
|
27
|
+
import { mkdtempSync, rmSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
28
|
+
import { tmpdir } from 'node:os';
|
|
29
|
+
import { join, resolve, dirname } from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
import { execFileSync } from 'node:child_process';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const REPO_ROOT = resolve(__dirname, '..');
|
|
36
|
+
const HOOK = join(REPO_ROOT, 'hooks', 'work-item-exit-gate.js');
|
|
37
|
+
const GATE_LIB = pathToFileURL(join(REPO_ROOT, 'hooks', 'lib', 'run-evidence-gate.mjs')).href;
|
|
38
|
+
|
|
39
|
+
const failures = [];
|
|
40
|
+
function assert(name, condition, detail = '') {
|
|
41
|
+
if (!condition) failures.push(`${name}${detail ? `: ${detail}` : ''}`);
|
|
42
|
+
else process.stdout.write(` ok ${name}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Build a throwaway git repo: commit two files, capture the FULL HEAD sha.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
const repo = mkdtempSync(join(tmpdir(), 'l2-repo-'));
|
|
49
|
+
const g = (...a) => execFileSync('git', a, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
50
|
+
g('init', '-q');
|
|
51
|
+
g('config', 'user.email', 'test@example.com');
|
|
52
|
+
g('config', 'user.name', 'Test');
|
|
53
|
+
writeFileSync(join(repo, 'a.txt'), 'alpha\n');
|
|
54
|
+
writeFileSync(join(repo, 'b.txt'), 'beta\n');
|
|
55
|
+
g('add', 'a.txt', 'b.txt');
|
|
56
|
+
g('commit', '-q', '-m', 'feat: seed');
|
|
57
|
+
const HEAD = g('rev-parse', 'HEAD').trim(); // real FULL 40-hex SHA
|
|
58
|
+
|
|
59
|
+
// CRITICAL: set the repo the gate runs git against BEFORE requiring the hook,
|
|
60
|
+
// because TRUTH_GATE_REPO is captured at module load.
|
|
61
|
+
process.env.RDC_TRUTH_GATE_REPO = repo;
|
|
62
|
+
const require = createRequire(import.meta.url);
|
|
63
|
+
const gate = require(HOOK);
|
|
64
|
+
|
|
65
|
+
const SESS = 'sess-l2-origin';
|
|
66
|
+
// Build an `item` row shaped like the work_items SELECT the gate reads.
|
|
67
|
+
function makeItem(post) {
|
|
68
|
+
return {
|
|
69
|
+
id: '11111111-2222-3333-4444-555555555555',
|
|
70
|
+
item_type: 'task',
|
|
71
|
+
status: 'review',
|
|
72
|
+
session_id: SESS,
|
|
73
|
+
implementation_report: { codeflow_post: post },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const statusCall = { id: '11111111-2222-3333-4444-555555555555', actorSessionId: 'validator-x', actorRole: 'validator' };
|
|
77
|
+
|
|
78
|
+
// A machine-parseable verification artifact (exit-code shape).
|
|
79
|
+
const MACHINE_VERIF = { exit_code: 0, label: 'tsc' };
|
|
80
|
+
|
|
81
|
+
// Helper: run verifyLayer2 and return the GateDenied reason, or null if it passed.
|
|
82
|
+
async function runL2(post, capturedShas) {
|
|
83
|
+
try {
|
|
84
|
+
await gate.verifyLayer2(statusCall, makeItem(post), capturedShas);
|
|
85
|
+
return null; // ALLOW
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (e instanceof gate.GateDenied) return e.reason;
|
|
88
|
+
throw e; // unexpected internal error — surface it
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CAPTURED = new Set([HEAD.toLowerCase()]); // L1-captured set for this item/session
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
await (async () => {
|
|
96
|
+
// 1. commit does NOT resolve (free-typed bad SHA)
|
|
97
|
+
{
|
|
98
|
+
const reason = await runL2({
|
|
99
|
+
commit: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
|
|
100
|
+
files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
|
|
101
|
+
}, CAPTURED);
|
|
102
|
+
assert('1. free-typed/bad SHA -> DENY', reason && /does not resolve to a real commit/.test(reason), reason || 'no denial');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. commit resolves but was NOT captured by L1 for this item/session
|
|
106
|
+
{
|
|
107
|
+
const reason = await runL2({
|
|
108
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
|
|
109
|
+
}, new Set()); // empty captured set
|
|
110
|
+
assert('2. commit not captured by L1 -> DENY', reason && /was not captured by Layer 1/.test(reason), reason || 'no denial');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. files_changed entry NOT in the commit
|
|
114
|
+
{
|
|
115
|
+
const reason = await runL2({
|
|
116
|
+
commit: HEAD, files_changed: ['nonexistent-in-commit.txt'], verification: [MACHINE_VERIF], witness: 'ci',
|
|
117
|
+
}, CAPTURED);
|
|
118
|
+
assert('3. files not in commit -> DENY', reason && /is NOT among the files changed by commit/.test(reason), reason || 'no denial');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. file is in the commit but NOT on disk (delete it from the work tree)
|
|
122
|
+
{
|
|
123
|
+
unlinkSync(join(repo, 'b.txt')); // b.txt is in the commit, now gone from disk
|
|
124
|
+
const reason = await runL2({
|
|
125
|
+
commit: HEAD, files_changed: ['b.txt'], verification: [MACHINE_VERIF], witness: 'human-review',
|
|
126
|
+
}, CAPTURED);
|
|
127
|
+
assert('4. file not on disk -> DENY', reason && /does not exist on disk/.test(reason), reason || 'no denial');
|
|
128
|
+
writeFileSync(join(repo, 'b.txt'), 'beta\n'); // restore for later valid-close test
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. prose / proxy verification ("HTTP 200")
|
|
132
|
+
{
|
|
133
|
+
const reason = await runL2({
|
|
134
|
+
commit: HEAD, files_changed: ['a.txt'], verification: ['HTTP 200'], witness: 'validator-rerun',
|
|
135
|
+
}, CAPTURED);
|
|
136
|
+
assert('5. prose verification -> DENY', reason && /prose\/proxy, not a captured artifact/.test(reason), reason || 'no denial');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 6. witness:"agent" (self-witness)
|
|
140
|
+
{
|
|
141
|
+
const reason = await runL2({
|
|
142
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'agent',
|
|
143
|
+
}, CAPTURED);
|
|
144
|
+
assert('6. witness:agent -> DENY', reason && /witness must be one of/.test(reason), reason || 'no denial');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 7. fully-valid close -> ALLOW (no throw)
|
|
148
|
+
{
|
|
149
|
+
const reason = await runL2({
|
|
150
|
+
commit: HEAD, files_changed: ['a.txt', 'b.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
|
|
151
|
+
}, CAPTURED);
|
|
152
|
+
assert('7. fully-valid close -> ALLOW', reason === null, `unexpected denial: ${reason}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 7b. fully-valid close with a REAL fused run-evidence-gate artifact as verification
|
|
156
|
+
{
|
|
157
|
+
const lib = await import(GATE_LIB);
|
|
158
|
+
const fused = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(0)'], label: 'fused-pass' });
|
|
159
|
+
const reason = await runL2({
|
|
160
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [fused], witness: 'ci',
|
|
161
|
+
}, CAPTURED);
|
|
162
|
+
assert('7b. fused artifact verification -> ALLOW', reason === null, `unexpected denial: ${reason}`);
|
|
163
|
+
assert('7b. fused artifact has verdict only after run', fused.ran === true && fused.verdict === 'pass' && typeof fused.output_sha256 === 'string', JSON.stringify(fused));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 8. FUSED primitive: no verdict possible without a run (invalid spec -> error, ran:false)
|
|
167
|
+
{
|
|
168
|
+
const lib = await import(GATE_LIB);
|
|
169
|
+
const bad = lib.runEvidenceGate({}); // no command -> cannot run
|
|
170
|
+
assert('8. fused: no run -> no pass verdict (fail-closed)', bad.ran === false && bad.verdict === 'error', JSON.stringify(bad));
|
|
171
|
+
// and a run that fails yields verdict:'fail', never silently 'pass'
|
|
172
|
+
const failRun = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(2)'] });
|
|
173
|
+
assert('8. fused: failing run -> verdict fail', failRun.ran === true && failRun.verdict === 'fail' && failRun.exit_code === 2, JSON.stringify(failRun));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 9. FAIL-CLOSED contract: empty files_changed / empty verification both DENY
|
|
177
|
+
{
|
|
178
|
+
const r1 = await runL2({ commit: HEAD, files_changed: [], verification: [MACHINE_VERIF], witness: 'ci' }, CAPTURED);
|
|
179
|
+
assert('9a. empty files_changed -> DENY', r1 && /files_changed is empty/.test(r1), r1 || 'no denial');
|
|
180
|
+
const r2 = await runL2({ commit: HEAD, files_changed: ['a.txt'], verification: [], witness: 'ci' }, CAPTURED);
|
|
181
|
+
assert('9b. empty verification -> DENY', r2 && /verification is empty/.test(r2), r2 || 'no denial');
|
|
182
|
+
const r3 = await runL2({ commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF] }, CAPTURED); // no witness
|
|
183
|
+
assert('9c. missing witness -> DENY', r3 && /witness must be one of/.test(r3), r3 || 'no denial');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 10. Baru-trap: a SHORT prefix of a real commit must NOT resolve-and-pass as
|
|
187
|
+
// the captured full SHA. We claim a 8-char prefix; even though it resolves to
|
|
188
|
+
// the same commit, the captured set holds the FULL sha, and the gate compares
|
|
189
|
+
// full-to-full, so a prefix that git expands still equals HEAD -> captured.
|
|
190
|
+
// The hardening we prove: the gate stores/compares the FULL resolved sha, so a
|
|
191
|
+
// WRONG short prefix (one that resolves to a DIFFERENT/absent commit) is caught
|
|
192
|
+
// by branch 1/2 above. Here we assert the FULL-sha resolution itself:
|
|
193
|
+
{
|
|
194
|
+
const full = gate.resolveFullSha(HEAD.slice(0, 8)); // valid short prefix of HEAD
|
|
195
|
+
assert('10. resolveFullSha expands a valid prefix to the FULL 40-hex sha',
|
|
196
|
+
full === HEAD.toLowerCase() && /^[0-9a-f]{40}$/.test(full), full || 'null');
|
|
197
|
+
const none = gate.resolveFullSha('00000000'); // prefix of no commit
|
|
198
|
+
assert('10. resolveFullSha returns null for a non-existent prefix', none === null, none || 'not-null');
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
|
|
202
|
+
rmSync(repo, { recursive: true, force: true });
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
if (failures.length > 0) {
|
|
206
|
+
console.error('\nwork-item-exit-gate L2 tests — FAIL\n');
|
|
207
|
+
for (const f of failures) console.error(` - ${f}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
console.log('\nwork-item-exit-gate L2 tests — PASS');
|