sneakoscope 0.9.12 → 0.9.13
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/README.md +8 -2
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +142 -2
- package/package.json +4 -2
- package/src/cli/command-registry.mjs +3 -3
- package/src/cli/feature-commands.mjs +42 -11
- package/src/cli/install-helpers.mjs +14 -7
- package/src/cli/legacy-main.mjs +5 -4
- package/src/commands/codex-app.mjs +30 -0
- package/src/commands/codex-lb.mjs +18 -2
- package/src/commands/db.mjs +6 -0
- package/src/commands/doctor.mjs +46 -0
- package/src/commands/proof.mjs +38 -2
- package/src/commands/wiki.mjs +53 -2
- package/src/core/codex-lb-circuit.mjs +45 -1
- package/src/core/db-safety.mjs +17 -2
- package/src/core/feature-fixtures.mjs +39 -1
- package/src/core/feature-registry.mjs +87 -4
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +12 -3
- package/src/core/pipeline.mjs +18 -0
- package/src/core/proof/evidence-collector.mjs +7 -0
- package/src/core/proof/proof-reader.mjs +11 -0
- package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
- package/src/core/proof/route-adapter.mjs +74 -0
- package/src/core/proof/route-proof-gate.mjs +33 -0
- package/src/core/proof/route-proof-policy.mjs +96 -0
- package/src/core/proof/selftest-proof-fixtures.mjs +54 -0
- package/src/core/proof/validation.mjs +1 -0
- package/src/core/rust-accelerator.mjs +29 -7
- package/src/core/version.mjs +1 -1
- package/src/core/wiki-image/callout-parser.mjs +16 -0
- package/src/core/wiki-image/computer-use-ledger.mjs +38 -0
- package/src/core/wiki-image/image-relation.mjs +2 -0
- package/src/core/wiki-image/image-voxel-ledger.mjs +43 -0
- package/src/core/wiki-image/proof-linker.mjs +12 -0
- package/src/core/wiki-image/validation.mjs +16 -5
- package/src/core/wiki-image/visual-anchor.mjs +14 -1
package/src/commands/proof.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import { projectRoot } from '../core/fsx.mjs';
|
|
|
2
2
|
import { flag } from '../cli/args.mjs';
|
|
3
3
|
import { printJson } from '../cli/output.mjs';
|
|
4
4
|
import { collectProofEvidence } from '../core/proof/evidence-collector.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { findLatestMission } from '../core/mission.mjs';
|
|
6
|
+
import { readLatestProof, readLatestProofMarkdown, readRouteProof } from '../core/proof/proof-reader.mjs';
|
|
7
|
+
import { writeRouteCompletionProof } from '../core/proof/route-adapter.mjs';
|
|
6
8
|
import { renderProofMarkdown, writeCompletionProof } from '../core/proof/proof-writer.mjs';
|
|
7
9
|
import { validateCompletionProof } from '../core/proof/validation.mjs';
|
|
8
10
|
|
|
@@ -26,10 +28,44 @@ export async function run(_command, args = []) {
|
|
|
26
28
|
if (!result.ok) process.exitCode = 1;
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
31
|
+
if (action === 'route') {
|
|
32
|
+
const missionArg = rest.find((arg) => !String(arg).startsWith('--')) || 'latest';
|
|
33
|
+
const missionId = missionArg === 'latest' ? await findLatestMission(root) : missionArg;
|
|
34
|
+
const proof = await readRouteProof(root, missionId);
|
|
35
|
+
const result = proof
|
|
36
|
+
? { schema: 'sks.completion-proof-route.v1', ok: true, mission_id: missionId, proof }
|
|
37
|
+
: { schema: 'sks.completion-proof-route.v1', ok: false, mission_id: missionId, status: 'blocked', issues: ['completion_proof_missing'] };
|
|
38
|
+
if (flag(args, '--json')) return printJson(result);
|
|
39
|
+
if (proof) process.stdout.write(renderProofMarkdown(proof));
|
|
40
|
+
else console.log(`Completion proof missing for mission ${missionId || 'latest'}`);
|
|
41
|
+
if (!result.ok) process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
29
44
|
if (action === 'export' && (flag(rest, '--md') || flag(args, '--md'))) {
|
|
30
45
|
process.stdout.write(await readLatestProofMarkdown(root));
|
|
31
46
|
return;
|
|
32
47
|
}
|
|
48
|
+
if (action === 'repair' && rest[0] === 'latest') {
|
|
49
|
+
const missionId = await findLatestMission(root);
|
|
50
|
+
const evidence = await collectProofEvidence(root);
|
|
51
|
+
const result = await writeRouteCompletionProof(root, {
|
|
52
|
+
missionId,
|
|
53
|
+
route: '$SKS',
|
|
54
|
+
status: 'verified_partial',
|
|
55
|
+
evidence,
|
|
56
|
+
summary: {
|
|
57
|
+
files_changed: evidence.files?.length || 0,
|
|
58
|
+
commands_run: 1,
|
|
59
|
+
manual_review_required: true
|
|
60
|
+
},
|
|
61
|
+
claims: [{ id: 'proof-repair-latest', status: 'supported', evidence: '.sneakoscope/proof/latest.json' }],
|
|
62
|
+
unverified: ['Repair proof records current local evidence and remains verified_partial until a route-specific gate passes.']
|
|
63
|
+
});
|
|
64
|
+
if (flag(args, '--json')) return printJson({ schema: 'sks.completion-proof-repair.v1', ok: result.ok, mission_id: missionId, files: result.files, validation: result.validation });
|
|
65
|
+
console.log(`Completion proof repaired: ${result.files.latest_json}`);
|
|
66
|
+
if (!result.ok) process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
33
69
|
if (action === 'smoke') {
|
|
34
70
|
const evidence = await collectProofEvidence(root);
|
|
35
71
|
const result = await writeCompletionProof(root, {
|
|
@@ -50,7 +86,7 @@ export async function run(_command, args = []) {
|
|
|
50
86
|
console.log(`Completion proof written: ${result.files.latest_json}`);
|
|
51
87
|
return;
|
|
52
88
|
}
|
|
53
|
-
console.error('Usage: sks proof show|latest|validate|export --md|smoke [--json]');
|
|
89
|
+
console.error('Usage: sks proof show|latest|validate|route <mission-id|latest>|export --md|repair latest|smoke [--json]');
|
|
54
90
|
process.exitCode = 1;
|
|
55
91
|
}
|
|
56
92
|
|
package/src/commands/wiki.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { projectRoot } from '../core/fsx.mjs';
|
|
3
3
|
import { flag, readOption } from '../cli/args.mjs';
|
|
4
4
|
import { printJson } from '../cli/output.mjs';
|
|
5
|
-
import { ingestImage, imageVoxelSummary, readImageVoxelLedger } from '../core/wiki-image/image-voxel-ledger.mjs';
|
|
5
|
+
import { addImageRelation, addVisualAnchor, ingestImage, imageVoxelSummary, readImageVoxelLedger } from '../core/wiki-image/image-voxel-ledger.mjs';
|
|
6
|
+
import { imageVoxelProofEvidence } from '../core/wiki-image/proof-linker.mjs';
|
|
6
7
|
import { validateImageVoxelLedger } from '../core/wiki-image/validation.mjs';
|
|
7
8
|
|
|
8
9
|
export async function run(_command, args = []) {
|
|
@@ -23,7 +24,11 @@ export async function run(_command, args = []) {
|
|
|
23
24
|
if (action === 'image-validate') {
|
|
24
25
|
const ledgerPath = args.find((arg, i) => i > 0 && !String(arg).startsWith('--'));
|
|
25
26
|
const ledger = await readImageVoxelLedger(root, ledgerPath ? path.resolve(root, ledgerPath) : undefined);
|
|
26
|
-
const result = { schema: 'sks.image-voxel-validation.v1', ...validateImageVoxelLedger(ledger
|
|
27
|
+
const result = { schema: 'sks.image-voxel-validation.v1', ...validateImageVoxelLedger(ledger, {
|
|
28
|
+
requireAnchors: flag(args, '--require-anchors'),
|
|
29
|
+
requireRelations: flag(args, '--require-relations'),
|
|
30
|
+
route: readOption(args, '--route', '$Wiki')
|
|
31
|
+
}) };
|
|
27
32
|
if (flag(args, '--json')) return printJson(result);
|
|
28
33
|
console.log(`Image voxel ledger: ${result.ok ? 'pass' : 'blocked'}`);
|
|
29
34
|
for (const issue of result.issues) console.log(`- ${issue}`);
|
|
@@ -39,6 +44,52 @@ export async function run(_command, args = []) {
|
|
|
39
44
|
if (!result.ok) process.exitCode = 1;
|
|
40
45
|
return;
|
|
41
46
|
}
|
|
47
|
+
if (action === 'anchor-add') {
|
|
48
|
+
const result = await addVisualAnchor(root, {
|
|
49
|
+
imageId: readOption(args, '--image-id', null),
|
|
50
|
+
bbox: parseBbox(readOption(args, '--bbox', '')),
|
|
51
|
+
label: readOption(args, '--label', 'Visual anchor'),
|
|
52
|
+
source: readOption(args, '--source', 'manual'),
|
|
53
|
+
evidencePath: readOption(args, '--evidence', null),
|
|
54
|
+
route: readOption(args, '--route', '$Wiki'),
|
|
55
|
+
claimId: readOption(args, '--claim-id', null),
|
|
56
|
+
missionId: readOption(args, '--mission-id', null)
|
|
57
|
+
});
|
|
58
|
+
if (flag(args, '--json')) return printJson(result);
|
|
59
|
+
console.log(`Visual anchor: ${result.ok ? 'added' : 'blocked'} ${result.anchor.id}`);
|
|
60
|
+
for (const issue of result.validation.issues) console.log(`- ${issue}`);
|
|
61
|
+
if (!result.ok) process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (action === 'relation-add') {
|
|
65
|
+
const result = await addImageRelation(root, {
|
|
66
|
+
type: readOption(args, '--type', 'before_after'),
|
|
67
|
+
beforeImageId: readOption(args, '--before', null),
|
|
68
|
+
afterImageId: readOption(args, '--after', null),
|
|
69
|
+
anchors: String(readOption(args, '--anchors', '') || '').split(',').map((x) => x.trim()).filter(Boolean),
|
|
70
|
+
verification: readOption(args, '--verification', 'changed-screen-recheck'),
|
|
71
|
+
status: readOption(args, '--status', 'verified_partial'),
|
|
72
|
+
route: readOption(args, '--route', '$Wiki'),
|
|
73
|
+
missionId: readOption(args, '--mission-id', null)
|
|
74
|
+
});
|
|
75
|
+
if (flag(args, '--json')) return printJson(result);
|
|
76
|
+
console.log(`Image relation: ${result.ok ? 'added' : 'blocked'} ${result.relation.type}`);
|
|
77
|
+
for (const issue of result.validation.issues) console.log(`- ${issue}`);
|
|
78
|
+
if (!result.ok) process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (action === 'image-link-proof') {
|
|
82
|
+
const result = await imageVoxelProofEvidence(root);
|
|
83
|
+
if (flag(args, '--json')) return printJson(result);
|
|
84
|
+
console.log(`Image voxel proof link: ${result.ok ? 'ok' : 'blocked'}`);
|
|
85
|
+
if (!result.ok) process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
42
88
|
const legacy = await import('../cli/legacy-main.mjs');
|
|
43
89
|
return legacy.main(['wiki', ...args]);
|
|
44
90
|
}
|
|
91
|
+
|
|
92
|
+
function parseBbox(raw) {
|
|
93
|
+
const parts = String(raw || '').split(',').map((part) => Number(part.trim()));
|
|
94
|
+
return parts.length === 4 && parts.every(Number.isFinite) ? parts : null;
|
|
95
|
+
}
|
|
@@ -6,6 +6,7 @@ import { redactSecrets } from './secret-redaction.mjs';
|
|
|
6
6
|
export const CODEX_LB_CIRCUIT_SCHEMA = 'sks.codex-lb-circuit.v1';
|
|
7
7
|
|
|
8
8
|
export function codexLbGlobalHealthPath() {
|
|
9
|
+
if (process.env.SKS_CODEX_LB_HEALTH_PATH) return process.env.SKS_CODEX_LB_HEALTH_PATH;
|
|
9
10
|
return path.join(os.homedir(), '.codex', 'sks-codex-lb-health.json');
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -32,8 +33,30 @@ export async function resetCodexLbCircuit(root = packageRoot()) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export async function recordCodexLbFailure(root = packageRoot(), failure = {}) {
|
|
36
|
+
return recordCodexLbHealthEvent(root, failure);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function recordCodexLbHealthEvent(root = packageRoot(), event = {}) {
|
|
35
40
|
const current = await readCodexLbCircuit(root);
|
|
36
|
-
const
|
|
41
|
+
const normalized = normalizeHealthEvent(event);
|
|
42
|
+
if (normalized.kind === 'chain_ok') {
|
|
43
|
+
return writeCodexLbCircuit(root, {
|
|
44
|
+
...current,
|
|
45
|
+
state: current.state === 'half_open' || current.state === 'open' ? 'closed' : current.state || 'closed',
|
|
46
|
+
last_ok_at: nowIso(),
|
|
47
|
+
recent_failures: current.recent_failures || []
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (normalized.kind === 'previous_response_not_found') {
|
|
51
|
+
const warnings = [...(current.recent_warnings || []), redactSecrets({ ts: nowIso(), ...normalized })].slice(-10);
|
|
52
|
+
return writeCodexLbCircuit(root, {
|
|
53
|
+
...current,
|
|
54
|
+
state: current.state === 'open' ? 'open' : 'closed',
|
|
55
|
+
recent_warnings: warnings,
|
|
56
|
+
last_warning_at: nowIso()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const recent = [...(current.recent_failures || []), redactSecrets({ ts: nowIso(), ...normalized })].slice(-10);
|
|
37
60
|
const open = recent.filter((item) => ['5xx', 'timeout', 'network'].includes(item.kind)).length >= 3
|
|
38
61
|
|| recent.some((item) => item.kind === 'auth');
|
|
39
62
|
return writeCodexLbCircuit(root, {
|
|
@@ -44,6 +67,23 @@ export async function recordCodexLbFailure(root = packageRoot(), failure = {}) {
|
|
|
44
67
|
});
|
|
45
68
|
}
|
|
46
69
|
|
|
70
|
+
export function normalizeHealthEvent(event = {}) {
|
|
71
|
+
const status = String(event.status || event.kind || '').toLowerCase();
|
|
72
|
+
const httpStatus = Number(event.http_status || event.httpStatus || 0);
|
|
73
|
+
let kind = event.kind || status || 'unknown';
|
|
74
|
+
if (status === 'chain_ok' || event.ok === true) kind = 'chain_ok';
|
|
75
|
+
else if (status === 'previous_response_not_found') kind = 'previous_response_not_found';
|
|
76
|
+
else if (status === 'missing_env_key') kind = 'missing_env_key';
|
|
77
|
+
else if (status === 'missing_base_url') kind = 'missing_base_url';
|
|
78
|
+
else if (/auth|401|403/.test(status) || httpStatus === 401 || httpStatus === 403) kind = 'auth';
|
|
79
|
+
else if (/timeout|timed out/.test(status) || /timeout|timed out/i.test(event.error || '')) kind = 'timeout';
|
|
80
|
+
else if (httpStatus >= 500 || /5xx|server/.test(status)) kind = '5xx';
|
|
81
|
+
else if (/network|fetch|econn|enotfound|socket/.test(status) || /network|fetch|econn|enotfound|socket/i.test(event.error || '')) kind = 'network';
|
|
82
|
+
else if (status === 'first_request_failed') kind = httpStatus >= 500 ? '5xx' : 'network';
|
|
83
|
+
else if (status === 'second_request_failed') kind = httpStatus >= 500 ? '5xx' : 'network';
|
|
84
|
+
return redactSecrets({ ...event, kind });
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
export function codexLbMetrics(circuit = emptyCircuit()) {
|
|
48
88
|
return {
|
|
49
89
|
schema: 'sks.codex-lb-metrics.v1',
|
|
@@ -65,21 +105,25 @@ function emptyCircuit() {
|
|
|
65
105
|
base_url: null,
|
|
66
106
|
state: 'closed',
|
|
67
107
|
recent_failures: [],
|
|
108
|
+
recent_warnings: [],
|
|
68
109
|
latency_ms: { p50: null, p95: null },
|
|
69
110
|
last_ok_at: null,
|
|
70
111
|
last_failure_at: null,
|
|
112
|
+
last_warning_at: null,
|
|
71
113
|
updated_at: nowIso()
|
|
72
114
|
};
|
|
73
115
|
}
|
|
74
116
|
|
|
75
117
|
function normalizeCircuit(input = {}, root = packageRoot()) {
|
|
76
118
|
const failures = Array.isArray(input.recent_failures) ? input.recent_failures.slice(-10).map((item) => redactSecrets(item)) : [];
|
|
119
|
+
const warnings = Array.isArray(input.recent_warnings) ? input.recent_warnings.slice(-10).map((item) => redactSecrets(item)) : [];
|
|
77
120
|
return {
|
|
78
121
|
...emptyCircuit(),
|
|
79
122
|
...redactSecrets(input),
|
|
80
123
|
schema: CODEX_LB_CIRCUIT_SCHEMA,
|
|
81
124
|
state: ['closed', 'open', 'half_open'].includes(input.state) ? input.state : 'closed',
|
|
82
125
|
recent_failures: failures,
|
|
126
|
+
recent_warnings: warnings,
|
|
83
127
|
report_path: codexLbReportPath(root),
|
|
84
128
|
updated_at: nowIso()
|
|
85
129
|
};
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -68,6 +68,21 @@ function stripSqlComments(sql = '') {
|
|
|
68
68
|
|
|
69
69
|
function norm(s = '') { return stripSqlComments(s).toLowerCase(); }
|
|
70
70
|
|
|
71
|
+
function stripSqlStringLiterals(sql = '') {
|
|
72
|
+
let out = '';
|
|
73
|
+
let quote = null;
|
|
74
|
+
for (let i = 0; i < String(sql).length; i++) {
|
|
75
|
+
const ch = sql[i];
|
|
76
|
+
if ((ch === "'" || ch === '"') && sql[i - 1] !== '\\') {
|
|
77
|
+
quote = quote === ch ? null : (quote || ch);
|
|
78
|
+
out += ' ';
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
out += quote ? ' ' : ch;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
export function splitSqlStatements(sql = '') {
|
|
72
87
|
const text = stripSqlComments(sql);
|
|
73
88
|
const out = [];
|
|
@@ -86,7 +101,7 @@ export function splitSqlStatements(sql = '') {
|
|
|
86
101
|
function hasWhere(stmt) { return /\bwhere\b/i.test(stmt); }
|
|
87
102
|
function hasLimit(stmt) { return /\blimit\s+\d+\b/i.test(stmt); }
|
|
88
103
|
function isReadOnly(stmt) {
|
|
89
|
-
const s = norm(stmt);
|
|
104
|
+
const s = stripSqlStringLiterals(norm(stmt));
|
|
90
105
|
return /^(select|with|show|explain|describe)\b/.test(s) && !/(\binsert\b|\bupdate\b|\bdelete\b|\bdrop\b|\btruncate\b|\balter\b|\bcreate\b|\bgrant\b|\brevoke\b)/.test(s);
|
|
91
106
|
}
|
|
92
107
|
|
|
@@ -97,7 +112,7 @@ export function classifySql(sql = '') {
|
|
|
97
112
|
let level = 'safe';
|
|
98
113
|
let kind = 'read';
|
|
99
114
|
for (const stmtRaw of statements) {
|
|
100
|
-
const stmt = norm(stmtRaw);
|
|
115
|
+
const stmt = stripSqlStringLiterals(norm(stmtRaw));
|
|
101
116
|
if (!stmt) continue;
|
|
102
117
|
const destructiveChecks = [
|
|
103
118
|
[/\bdrop\s+database\b/, 'drop_database'],
|
|
@@ -10,6 +10,36 @@ const FIXTURES = Object.freeze({
|
|
|
10
10
|
'cli-codex-lb': fixture('mock', 'sks codex-lb metrics --json', ['.sneakoscope/reports/codex-lb-health.json'], 'pass'),
|
|
11
11
|
'cli-hooks': fixture('mock', 'sks hooks trust-report --json', [], 'pass'),
|
|
12
12
|
'cli-features': fixture('static', 'sks features check --json', [], 'pass'),
|
|
13
|
+
'cli-commands': fixture('static', 'sks commands --json', [], 'pass'),
|
|
14
|
+
'cli-usage': fixture('static', 'sks usage overview', [], 'pass'),
|
|
15
|
+
'cli-quickstart': fixture('static', 'sks quickstart', [], 'pass'),
|
|
16
|
+
'cli-update-check': fixture('static', 'sks update-check --json', [], 'pass'),
|
|
17
|
+
'cli-guard': fixture('static', 'sks guard check --json', [], 'pass'),
|
|
18
|
+
'cli-conflicts': fixture('static', 'sks conflicts check --json', [], 'pass'),
|
|
19
|
+
'cli-versioning': fixture('static', 'sks versioning status --json', [], 'pass'),
|
|
20
|
+
'cli-aliases': fixture('static', 'sks aliases', [], 'pass'),
|
|
21
|
+
'cli-fix-path': fixture('static', 'sks fix-path --json', [], 'pass'),
|
|
22
|
+
'cli-init': fixture('static', 'sks init --local-only', [], 'pass'),
|
|
23
|
+
'cli-selftest': fixture('static', 'sks selftest --mock', [], 'pass'),
|
|
24
|
+
'cli-goal': fixture('mock', 'sks goal status latest --json', ['goal-workflow.json'], 'pass'),
|
|
25
|
+
'cli-research': fixture('mock', 'sks research status latest --json', ['research-gate.json', 'completion-proof.json'], 'pass'),
|
|
26
|
+
'cli-qa-loop': fixture('mock', 'sks qa-loop status latest --json', ['qa-loop-proof.json', 'completion-proof.json'], 'pass'),
|
|
27
|
+
'cli-ppt': fixture('mock', 'sks ppt status latest --json', ['ppt-review-ledger.json', 'completion-proof.json'], 'pass'),
|
|
28
|
+
'cli-image-ux-review': fixture('mock', 'sks image-ux-review status latest --json', ['image-ux-generated-review-ledger.json', 'image-voxel-ledger.json'], 'pass'),
|
|
29
|
+
'cli-pipeline': fixture('mock', 'sks pipeline status latest --json', ['pipeline-plan.json'], 'pass'),
|
|
30
|
+
'cli-validate-artifacts': fixture('mock', 'sks validate-artifacts latest --json', ['validation-report.json'], 'pass'),
|
|
31
|
+
'cli-hproof': fixture('mock', 'sks hproof check latest', ['completion-proof.json'], 'pass'),
|
|
32
|
+
'cli-proof-field': fixture('static', 'sks proof-field scan --json --intent fixture', [], 'pass'),
|
|
33
|
+
'cli-recallpulse': fixture('mock', 'sks recallpulse status latest --json', ['recallpulse-report.json'], 'pass'),
|
|
34
|
+
'cli-gx': fixture('mock', 'sks gx validate fixture', ['gx-validation.json'], 'pass'),
|
|
35
|
+
'cli-perf': fixture('static', 'sks perf cold-start --json --iterations 1', [], 'pass'),
|
|
36
|
+
'cli-code-structure': fixture('static', 'sks code-structure scan --json', [], 'pass'),
|
|
37
|
+
'cli-skill-dream': fixture('static', 'sks skill-dream status --json', [], 'pass'),
|
|
38
|
+
'cli-gc': fixture('static', 'sks gc --dry-run --json', [], 'pass'),
|
|
39
|
+
'cli-memory': fixture('static', 'sks memory --dry-run --json', [], 'pass'),
|
|
40
|
+
'cli-stats': fixture('static', 'sks stats --json', [], 'pass'),
|
|
41
|
+
'cli-dollar-commands': fixture('static', 'sks dollar-commands --json', [], 'pass'),
|
|
42
|
+
'cli-dfix': fixture('static', 'sks dfix', [], 'pass'),
|
|
13
43
|
'cli-wiki': fixture('mock', 'sks wiki image-summary --json', ['.sneakoscope/wiki/image-voxel-ledger.json'], 'pass'),
|
|
14
44
|
'cli-db': fixture('static', 'sks db policy', [], 'pass'),
|
|
15
45
|
'cli-proof': fixture('mock', 'sks proof validate --json', ['.sneakoscope/proof/latest.json'], 'pass'),
|
|
@@ -18,7 +48,15 @@ const FIXTURES = Object.freeze({
|
|
|
18
48
|
'route-research': fixture('mock', 'sks research run latest --mock --json', ['completion-proof.json'], 'pass'),
|
|
19
49
|
'route-ppt': fixture('mock', 'sks ppt status latest --json', ['ppt-review-ledger.json', 'completion-proof.json'], 'pass'),
|
|
20
50
|
'route-image-ux-review': fixture('mock', 'sks image-ux-review status latest --json', ['image-ux-generated-review-ledger.json', 'image-voxel-ledger.json'], 'pass'),
|
|
21
|
-
'route-computer-use': fixture('
|
|
51
|
+
'route-computer-use': fixture('mock', '$Computer-Use mock evidence ledger', ['computer-use-evidence-ledger.json', 'image-voxel-ledger.json', 'completion-proof.json'], 'pass'),
|
|
52
|
+
'route-cu': fixture('mock', '$CU mock evidence ledger', ['computer-use-evidence-ledger.json', 'image-voxel-ledger.json', 'completion-proof.json'], 'pass'),
|
|
53
|
+
'route-dfix': fixture('static', '$DFix tiny edit route policy', ['completion-proof.json'], 'pass'),
|
|
54
|
+
'route-answer': fixture('static', '$Answer answer-only route policy', [], 'pass'),
|
|
55
|
+
'route-goal': fixture('mock', '$Goal bridge route', ['goal-workflow.json', 'completion-proof.json'], 'pass'),
|
|
56
|
+
'route-autoresearch': fixture('mock', '$AutoResearch fixture route', ['research-gate.json', 'completion-proof.json'], 'pass'),
|
|
57
|
+
'route-mad-sks': fixture('mock', '$MAD-SKS permission gate route', ['mad-sks-gate.json', 'completion-proof.json'], 'pass'),
|
|
58
|
+
'route-from-chat-img': fixture('mock', '$From-Chat-IMG visual work order route', ['from-chat-img-work-order.md', 'image-voxel-ledger.json', 'completion-proof.json'], 'pass'),
|
|
59
|
+
'route-ux-review': fixture('mock', '$UX-Review image UX alias route', ['image-ux-generated-review-ledger.json', 'image-voxel-ledger.json'], 'pass'),
|
|
22
60
|
'route-db': fixture('static', 'sks db policy', [], 'pass'),
|
|
23
61
|
'route-wiki': fixture('mock', 'sks wiki image-summary --json', ['.sneakoscope/wiki/image-voxel-ledger.json'], 'pass'),
|
|
24
62
|
'route-gx': fixture('mock', 'sks gx validate fixture', [], 'pass')
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { COMMAND_CATALOG, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS } from './routes.mjs';
|
|
4
5
|
import { fixtureForFeature, fixtureSummary, validateFeatureFixtures } from './feature-fixtures.mjs';
|
|
5
|
-
import { exists, nowIso, packageRoot, readJson, readText, writeTextAtomic } from './fsx.mjs';
|
|
6
|
+
import { exists, nowIso, packageRoot, readJson, readText, runProcess, writeTextAtomic } from './fsx.mjs';
|
|
6
7
|
|
|
7
8
|
export const FEATURE_REGISTRY_SCHEMA = 'sks.feature-registry.v1';
|
|
8
9
|
export const FEATURE_INVENTORY_SCHEMA = 'sks.feature-inventory.v1';
|
|
@@ -132,10 +133,11 @@ export async function writeFeatureInventoryDocs({ root = packageRoot(), outFile
|
|
|
132
133
|
return { ok: registry.coverage.ok, path: outFile, registry };
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
export function buildAllFeaturesSelftest(registry) {
|
|
136
|
+
export function buildAllFeaturesSelftest(registry, opts = {}) {
|
|
136
137
|
const coverage = validateFeatureRegistry(registry);
|
|
137
138
|
const fixtures = validateFeatureFixtures(registry.features || []);
|
|
138
139
|
const fixturesSummary = fixtureSummary(registry.features || []);
|
|
140
|
+
const executable = opts.executeFixtures ? executeFeatureFixtures(registry.features || [], opts) : null;
|
|
139
141
|
const checks = [
|
|
140
142
|
checkRow('feature_registry_completeness', coverage.ok, coverage.blockers),
|
|
141
143
|
checkRow('command_lazy_load_availability', coverage.unmapped.cli_command_names.length === 0 && coverage.unmapped.handler_keys.length === 0, [...coverage.unmapped.cli_command_names, ...coverage.unmapped.handler_keys]),
|
|
@@ -145,7 +147,10 @@ export function buildAllFeaturesSelftest(registry) {
|
|
|
145
147
|
checkRow('failure_contracts_present', registry.features.every((feature) => Array.isArray(feature.known_gaps)), missingFeatureField(registry, 'known_gaps')),
|
|
146
148
|
checkRow('fixture_contracts_present', fixtures.ok, fixtures.blockers),
|
|
147
149
|
checkRow('proof_fixture_contract_present', registry.features.some((feature) => feature.id === 'cli-proof' && feature.fixture?.status === 'pass'), ['cli-proof']),
|
|
148
|
-
checkRow('voxel_fixture_contract_present', registry.features.some((feature) => feature.id === 'cli-wiki' && feature.fixture?.expected_artifacts?.some((artifact) => artifact.includes('image-voxel-ledger'))), ['cli-wiki'])
|
|
150
|
+
checkRow('voxel_fixture_contract_present', registry.features.some((feature) => feature.id === 'cli-wiki' && feature.fixture?.expected_artifacts?.some((artifact) => artifact.includes('image-voxel-ledger'))), ['cli-wiki']),
|
|
151
|
+
checkRow('fixture_pass_threshold', (fixturesSummary.counts.pass || 0) >= 45, [`pass=${fixturesSummary.counts.pass || 0}`]),
|
|
152
|
+
checkRow('fixture_mock_blocked_zero', (fixturesSummary.counts.blocked || 0) === 0, [`blocked=${fixturesSummary.counts.blocked || 0}`]),
|
|
153
|
+
...(executable ? [checkRow('executable_fixture_contracts', executable.ok, executable.failures)] : [])
|
|
149
154
|
];
|
|
150
155
|
const ok = checks.every((check) => check.ok);
|
|
151
156
|
return {
|
|
@@ -156,10 +161,88 @@ export function buildAllFeaturesSelftest(registry) {
|
|
|
156
161
|
checks,
|
|
157
162
|
fixtures: fixturesSummary,
|
|
158
163
|
coverage,
|
|
159
|
-
|
|
164
|
+
executable_fixtures: executable,
|
|
165
|
+
note: opts.executeFixtures
|
|
166
|
+
? 'Mock executable fixture mode validates release-gated fixture contracts and expected artifact declarations.'
|
|
167
|
+
: 'Mock selftest verifies the shared contract spine; feature fixtures remain progressive.'
|
|
160
168
|
};
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
export function executeFeatureFixtures(features = [], opts = {}) {
|
|
172
|
+
const selected = features.filter((feature) => feature.fixture?.status === 'pass' && ['mock', 'static'].includes(feature.fixture.kind));
|
|
173
|
+
const failures = [];
|
|
174
|
+
const checked = [];
|
|
175
|
+
const executed = [];
|
|
176
|
+
for (const feature of selected) {
|
|
177
|
+
const fx = feature.fixture;
|
|
178
|
+
if (!fx.command) {
|
|
179
|
+
failures.push(`${feature.id}:fixture_command_missing`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const artifactOk = Array.isArray(fx.expected_artifacts);
|
|
183
|
+
if (!artifactOk) failures.push(`${feature.id}:expected_artifacts`);
|
|
184
|
+
const execution = executeSafeFixtureCommand(feature.id, opts);
|
|
185
|
+
if (execution) {
|
|
186
|
+
executed.push(execution);
|
|
187
|
+
if (!execution.ok) failures.push(`${feature.id}:command_exit_${execution.status}`);
|
|
188
|
+
}
|
|
189
|
+
checked.push({
|
|
190
|
+
id: feature.id,
|
|
191
|
+
kind: fx.kind,
|
|
192
|
+
command: fx.command,
|
|
193
|
+
expected_artifacts: fx.expected_artifacts,
|
|
194
|
+
mode: execution ? 'command_and_contract' : 'contract'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
schema: 'sks.feature-fixture-execution.v1',
|
|
199
|
+
mode: 'mock',
|
|
200
|
+
ok: failures.length === 0,
|
|
201
|
+
checked: checked.length,
|
|
202
|
+
executed: executed.length,
|
|
203
|
+
executed_commands: executed,
|
|
204
|
+
failures,
|
|
205
|
+
command_execution: executed.length ? 'safe-allowlist' : 'contract-only',
|
|
206
|
+
note: 'Release fixture execution runs deterministic safe CLI fixtures and validates mock/static contracts without claiming real external dependency runs.'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function executeSafeFixtureCommand(featureId, opts = {}) {
|
|
211
|
+
const args = SAFE_EXECUTABLE_FIXTURE_ARGS[featureId];
|
|
212
|
+
if (!args) return null;
|
|
213
|
+
const root = opts.root || packageRoot();
|
|
214
|
+
const result = spawnSync(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), ...args], {
|
|
215
|
+
cwd: root,
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
timeout: 15_000,
|
|
218
|
+
env: { ...process.env, CI: 'true', SKS_SKIP_NPM_FRESHNESS_CHECK: '1' }
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
id: featureId,
|
|
222
|
+
args,
|
|
223
|
+
status: result.status,
|
|
224
|
+
signal: result.signal || null,
|
|
225
|
+
ok: result.status === 0,
|
|
226
|
+
stdout_bytes: Buffer.byteLength(result.stdout || ''),
|
|
227
|
+
stderr_bytes: Buffer.byteLength(result.stderr || '')
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const SAFE_EXECUTABLE_FIXTURE_ARGS = Object.freeze({
|
|
232
|
+
'cli-version': ['--version'],
|
|
233
|
+
'cli-root': ['root', '--json'],
|
|
234
|
+
'cli-features': ['features', 'check', '--json'],
|
|
235
|
+
'cli-commands': ['commands', '--json'],
|
|
236
|
+
'cli-proof-field': ['proof-field', 'scan', '--json', '--intent', 'fixture'],
|
|
237
|
+
'cli-db': ['db', 'policy'],
|
|
238
|
+
'cli-wiki': ['wiki', 'image-summary', '--json'],
|
|
239
|
+
'cli-codex-lb': ['codex-lb', 'metrics', '--json'],
|
|
240
|
+
'cli-hooks': ['hooks', 'trust-report', '--json'],
|
|
241
|
+
'cli-perf': ['perf', 'cold-start', '--json', '--iterations', '1'],
|
|
242
|
+
'route-db': ['db', 'policy'],
|
|
243
|
+
'route-wiki': ['wiki', 'image-summary', '--json']
|
|
244
|
+
});
|
|
245
|
+
|
|
163
246
|
export function renderFeatureInventoryMarkdown(registry) {
|
|
164
247
|
const coverage = registry.coverage || validateFeatureRegistry(registry);
|
|
165
248
|
const lines = [
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.9.
|
|
8
|
+
export const PACKAGE_VERSION = '0.9.13';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -166,8 +166,12 @@ function looksLikeUpdateAccept(prompt) {
|
|
|
166
166
|
|
|
167
167
|
export async function hookMain(name) {
|
|
168
168
|
const payload = await loadHookPayload();
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
return evaluateHookPayload(name, payload);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function evaluateHookPayload(name, payload = {}, opts = {}) {
|
|
173
|
+
const root = opts.root || await projectRoot(payload.cwd || process.cwd());
|
|
174
|
+
const state = opts.state || payload.state || await loadState(root);
|
|
171
175
|
const noQuestion = isNoQuestionRunning(state);
|
|
172
176
|
if (name === 'user-prompt-submit') {
|
|
173
177
|
const modelBlock = blockForbiddenClientModel(payload);
|
|
@@ -913,7 +917,7 @@ function hasHonestModeUnresolvedGap(text) {
|
|
|
913
917
|
return honestModeGapLines(text).length > 0;
|
|
914
918
|
}
|
|
915
919
|
|
|
916
|
-
function honestModeGapLines(text) {
|
|
920
|
+
export function honestModeGapLines(text) {
|
|
917
921
|
const issue = /(gap|remaining|unverified|not verified|not run|not complete|incomplete|failed|blocked|blocker|could not|couldn't|missing|미완료|미검증|미실행|실패|차단|누락|못했|못 했|안 했|안함|아직|남은)/i;
|
|
918
922
|
return String(text || '')
|
|
919
923
|
.split(/\n/)
|
|
@@ -923,6 +927,11 @@ function honestModeGapLines(text) {
|
|
|
923
927
|
}
|
|
924
928
|
|
|
925
929
|
function honestGapLineResolved(line) {
|
|
930
|
+
if (/(?:unverified|미검증)\s*:\s*\[\s*\]/i.test(line) && /blockers?\s*:\s*\[\s*\]/i.test(line)) return true;
|
|
931
|
+
if (/(?:^|[\s*-])(?:unverified|미검증|blockers?)\s*:\s*\[\s*\](?:\s*(?:[,.;]|$).*)?$/i.test(line)) return true;
|
|
932
|
+
if (/(?:미해결|남은)\s*(?:gap|갭|문제|항목)\s*:\s*(?:없음|없습니다|없다|0|0개)(?:\s|,|\.|$)/i.test(line)) return true;
|
|
933
|
+
if (/unresolved\s+gaps?\s+(?:for|in)[^:]*:\s*(?:none|no|0)\b/i.test(line)) return true;
|
|
934
|
+
if (/no\s+unresolved\s+gaps?\s+remain/i.test(line)) return true;
|
|
926
935
|
if (/(남은\s*(?:gap|갭|문제)\s*:\s*없음|남은\s*(?:gap|갭|문제)\s*없음|remaining\s+gaps?\s*:\s*(none|no|0)|no\s+remaining\s+gaps?)/i.test(line)) return true;
|
|
927
936
|
if (/no\s+active\s+blocking\s+route\s+gate\s+detected/i.test(line)) return true;
|
|
928
937
|
if (/(non[-\s]?blocker|non[-\s]?blocking|not\s+(?:a\s+)?blocker|no\s+blocker|does\s+not\s+block|not\s+blocking|blocker\s*(?:는|가)?\s*(?:아님|아닙니다|없음)|차단(?:하지|하진|하지는)\s*않|막(?:지|지는)\s*않)/i.test(line)) return true;
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import { writeQaLoopArtifacts } from './qa-loop.mjs';
|
|
|
18
18
|
import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
|
|
19
19
|
import { responseLanguageInstruction } from './language-preference.mjs';
|
|
20
20
|
import { SPEED_LANE_POLICY } from './proof-field.mjs';
|
|
21
|
+
import { validateRouteCompletionProof } from './proof/route-proof-gate.mjs';
|
|
22
|
+
import { routeFromState, routeRequiresCompletionProof } from './proof/route-proof-policy.mjs';
|
|
21
23
|
import { permissionGateSummary } from './permission-gates.mjs';
|
|
22
24
|
import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, SOLUTION_SCOUT_STAGE_ID, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, imageUxReviewPipelinePolicyText, looksLikeProblemSolvingRequest, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, solutionScoutPolicyText, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, stripVisibleDecisionAnswerBlocks, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
23
25
|
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
|
|
@@ -1357,11 +1359,27 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
1357
1359
|
return complianceBlock(root, state, `SKS ${state.route_command || state.mode} route cannot stop yet. Pass ${gate.file || state.stop_gate} or record a hard blocker with evidence before finishing.${missing}`, { gate: gate.file || state.stop_gate, missing: gate.missing });
|
|
1358
1360
|
}
|
|
1359
1361
|
}
|
|
1362
|
+
const proofGate = await routeProofGateStatus(root, state);
|
|
1363
|
+
if (!proofGate.ok) {
|
|
1364
|
+
return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} route cannot finalize without a valid Completion Proof. Missing or invalid proof issues: ${proofGate.issues.join(', ')}.`, { gate: 'completion-proof', missing: proofGate.issues });
|
|
1365
|
+
}
|
|
1360
1366
|
const reflection = await reflectionGateStatus(root, state);
|
|
1361
1367
|
if (!reflection.ok) return complianceBlock(root, state, reflectionStopReason(state, reflection), { gate: 'reflection', missing: reflection.missing });
|
|
1362
1368
|
return null;
|
|
1363
1369
|
}
|
|
1364
1370
|
|
|
1371
|
+
async function routeProofGateStatus(root, state = {}) {
|
|
1372
|
+
const route = routeFromState(state);
|
|
1373
|
+
const required = state.proof_required === true || routeRequiresCompletionProof(route);
|
|
1374
|
+
if (!required || !state?.mission_id) return { ok: true, required: false, issues: [] };
|
|
1375
|
+
return validateRouteCompletionProof(root, {
|
|
1376
|
+
missionId: state.mission_id,
|
|
1377
|
+
route,
|
|
1378
|
+
state,
|
|
1379
|
+
visualClaim: state.visual_claim !== false
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1365
1383
|
function clarificationGatePending(state = {}) {
|
|
1366
1384
|
const phase = String(state.phase || '');
|
|
1367
1385
|
return Boolean(state?.clarification_required && phase.includes('CLARIFICATION_AWAITING_ANSWERS'))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { packageRoot, readJson, runProcess, which } from '../fsx.mjs';
|
|
3
|
+
import { codexLbMetrics, readCodexLbCircuit } from '../codex-lb-circuit.mjs';
|
|
3
4
|
import { imageVoxelSummary } from '../wiki-image/image-voxel-ledger.mjs';
|
|
4
5
|
|
|
5
6
|
export async function collectProofEvidence(root = packageRoot()) {
|
|
@@ -10,6 +11,12 @@ export async function collectProofEvidence(root = packageRoot()) {
|
|
|
10
11
|
status: 'present',
|
|
11
12
|
schema: pack.schema || null,
|
|
12
13
|
claims: pack.trust_summary?.claims || pack.wiki?.a?.length || 0
|
|
14
|
+
} : null).catch(() => null),
|
|
15
|
+
codex_lb: await readCodexLbCircuit(root).then((circuit) => codexLbMetrics(circuit)).catch(() => null),
|
|
16
|
+
db_safety: await readJson(path.join(root, '.sneakoscope', 'db-safety.json'), null).then((policy) => policy ? {
|
|
17
|
+
status: 'present',
|
|
18
|
+
mode: policy.mode || null,
|
|
19
|
+
destructive_operations: policy.destructive_operations || null
|
|
13
20
|
} : null).catch(() => null)
|
|
14
21
|
};
|
|
15
22
|
}
|
|
@@ -17,3 +17,14 @@ export async function readLatestProofMarkdown(root = packageRoot()) {
|
|
|
17
17
|
if (!await exists(file)) return '# SKS Completion Proof\n\nNo completion proof has been written yet.\n';
|
|
18
18
|
return readText(file);
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export async function readRouteProof(root = packageRoot(), missionId = null) {
|
|
22
|
+
if (missionId) {
|
|
23
|
+
const missionProof = path.join(root, '.sneakoscope', 'missions', missionId, 'completion-proof.json');
|
|
24
|
+
if (await exists(missionProof)) return readJson(missionProof);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const latest = path.join(proofDir(root), 'latest.json');
|
|
28
|
+
if (await exists(latest)) return readJson(latest);
|
|
29
|
+
return null;
|
|
30
|
+
}
|