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.
Files changed (39) hide show
  1. package/README.md +8 -2
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +142 -2
  5. package/package.json +4 -2
  6. package/src/cli/command-registry.mjs +3 -3
  7. package/src/cli/feature-commands.mjs +42 -11
  8. package/src/cli/install-helpers.mjs +14 -7
  9. package/src/cli/legacy-main.mjs +5 -4
  10. package/src/commands/codex-app.mjs +30 -0
  11. package/src/commands/codex-lb.mjs +18 -2
  12. package/src/commands/db.mjs +6 -0
  13. package/src/commands/doctor.mjs +46 -0
  14. package/src/commands/proof.mjs +38 -2
  15. package/src/commands/wiki.mjs +53 -2
  16. package/src/core/codex-lb-circuit.mjs +45 -1
  17. package/src/core/db-safety.mjs +17 -2
  18. package/src/core/feature-fixtures.mjs +39 -1
  19. package/src/core/feature-registry.mjs +87 -4
  20. package/src/core/fsx.mjs +1 -1
  21. package/src/core/hooks-runtime.mjs +12 -3
  22. package/src/core/pipeline.mjs +18 -0
  23. package/src/core/proof/evidence-collector.mjs +7 -0
  24. package/src/core/proof/proof-reader.mjs +11 -0
  25. package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
  26. package/src/core/proof/route-adapter.mjs +74 -0
  27. package/src/core/proof/route-proof-gate.mjs +33 -0
  28. package/src/core/proof/route-proof-policy.mjs +96 -0
  29. package/src/core/proof/selftest-proof-fixtures.mjs +54 -0
  30. package/src/core/proof/validation.mjs +1 -0
  31. package/src/core/rust-accelerator.mjs +29 -7
  32. package/src/core/version.mjs +1 -1
  33. package/src/core/wiki-image/callout-parser.mjs +16 -0
  34. package/src/core/wiki-image/computer-use-ledger.mjs +38 -0
  35. package/src/core/wiki-image/image-relation.mjs +2 -0
  36. package/src/core/wiki-image/image-voxel-ledger.mjs +43 -0
  37. package/src/core/wiki-image/proof-linker.mjs +12 -0
  38. package/src/core/wiki-image/validation.mjs +16 -5
  39. package/src/core/wiki-image/visual-anchor.mjs +14 -1
@@ -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 { readLatestProof, readLatestProofMarkdown } from '../core/proof/proof-reader.mjs';
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
 
@@ -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 recent = [...(current.recent_failures || []), redactSecrets({ ts: nowIso(), ...failure })].slice(-10);
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
  };
@@ -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('real_optional', '$Computer-Use mock evidence ledger', ['computer-use-evidence-ledger.json'], 'blocked'),
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
- note: 'Mock selftest verifies the shared contract spine; feature fixtures remain progressive.'
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.12';
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
- const root = await projectRoot(payload.cwd || process.cwd());
170
- const state = await loadState(root);
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;
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import { containsPlaintextSecret, redactSecrets } from '../secret-redaction.mjs';
2
+
3
+ export function assertProofRedaction(value) {
4
+ const redacted = redactSecrets(value);
5
+ return {
6
+ ok: !containsPlaintextSecret(redacted),
7
+ redacted
8
+ };
9
+ }