sneakoscope 0.9.11 → 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 +28 -2
- package/crates/sks-core/Cargo.lock +7 -0
- package/crates/sks-core/Cargo.toml +10 -0
- package/crates/sks-core/src/main.rs +202 -0
- package/package.json +15 -3
- package/src/cli/args.mjs +49 -0
- package/src/cli/command-registry.mjs +128 -0
- package/src/cli/feature-commands.mjs +112 -6
- package/src/cli/install-helpers.mjs +14 -7
- package/src/cli/legacy-main.mjs +4147 -0
- package/src/cli/main.mjs +7 -4138
- package/src/cli/output.mjs +9 -0
- package/src/cli/router.mjs +30 -0
- package/src/commands/all-features.mjs +6 -0
- package/src/commands/codex-app.mjs +30 -0
- package/src/commands/codex-lb.mjs +47 -0
- package/src/commands/db.mjs +6 -0
- package/src/commands/doctor.mjs +46 -0
- package/src/commands/features.mjs +6 -0
- package/src/commands/help.mjs +77 -0
- package/src/commands/hooks.mjs +6 -0
- package/src/commands/perf.mjs +91 -0
- package/src/commands/proof.mjs +103 -0
- package/src/commands/root.mjs +24 -0
- package/src/commands/version.mjs +5 -0
- package/src/commands/wiki.mjs +95 -0
- package/src/core/codex-lb-circuit.mjs +130 -0
- package/src/core/db-safety.mjs +18 -3
- package/src/core/feature-fixtures.mjs +103 -0
- package/src/core/feature-registry.mjs +117 -11
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +17 -6
- package/src/core/language-preference.mjs +106 -0
- package/src/core/pipeline.mjs +24 -0
- package/src/core/proof/claim-ledger.mjs +9 -0
- package/src/core/proof/command-ledger.mjs +17 -0
- package/src/core/proof/evidence-collector.mjs +33 -0
- package/src/core/proof/file-change-ledger.mjs +6 -0
- package/src/core/proof/proof-reader.mjs +30 -0
- package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
- package/src/core/proof/proof-schema.mjs +42 -0
- package/src/core/proof/proof-writer.mjs +81 -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 +20 -0
- package/src/core/routes.mjs +4 -3
- package/src/core/rust-accelerator.mjs +55 -3
- package/src/core/secret-redaction.mjs +69 -0
- package/src/core/version-manager.mjs +11 -7
- package/src/core/version.mjs +1 -0
- package/src/core/wiki-image/bbox.mjs +10 -0
- 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-hash.mjs +42 -0
- package/src/core/wiki-image/image-relation.mjs +2 -0
- package/src/core/wiki-image/image-voxel-ledger.mjs +147 -0
- package/src/core/wiki-image/image-voxel-schema.mjs +16 -0
- package/src/core/wiki-image/proof-linker.mjs +12 -0
- package/src/core/wiki-image/validation.mjs +53 -0
- package/src/core/wiki-image/visual-anchor.mjs +42 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { writeRouteCompletionProof } from './route-adapter.mjs';
|
|
2
|
+
|
|
3
|
+
const CLAIM_TEXT = {
|
|
4
|
+
hard_blocker: 'Hard blocker unblocks incomplete active gate after repeated identical compliance stops.',
|
|
5
|
+
team_gate: 'Team selftest fixture reached Completion Proof gate before reflection validation.',
|
|
6
|
+
subagent_gate: 'Subagent selftest fixture records Completion Proof after subagent evidence.'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULTS = {
|
|
10
|
+
hard_blocker: {
|
|
11
|
+
artifacts: ['hard-blocker.json', 'compliance-loop-guard.json'],
|
|
12
|
+
gateSource: 'selftest-hard-blocker',
|
|
13
|
+
unverified: ['selftest fixture does not claim a real Team run completed']
|
|
14
|
+
},
|
|
15
|
+
team_gate: {
|
|
16
|
+
artifacts: ['team-gate.json', 'team-session-cleanup.json'],
|
|
17
|
+
gateSource: 'selftest-route-gate',
|
|
18
|
+
unverified: ['selftest fixture does not claim a real Team implementation run completed']
|
|
19
|
+
},
|
|
20
|
+
subagent_gate: {
|
|
21
|
+
artifacts: ['team-gate.json', 'team-session-cleanup.json', 'reflection-gate.json'],
|
|
22
|
+
gateSource: 'selftest-subagent-gate',
|
|
23
|
+
unverified: ['selftest fixture records mocked subagent evidence only']
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function writeSelftestRouteProof(root, {
|
|
28
|
+
missionId,
|
|
29
|
+
route = '$Team',
|
|
30
|
+
kind = 'team_gate',
|
|
31
|
+
artifacts = null,
|
|
32
|
+
gateSource = null,
|
|
33
|
+
unverified = null
|
|
34
|
+
} = {}) {
|
|
35
|
+
const defaults = DEFAULTS[kind] || DEFAULTS.team_gate;
|
|
36
|
+
return writeRouteCompletionProof(root, {
|
|
37
|
+
missionId,
|
|
38
|
+
route,
|
|
39
|
+
status: 'verified_partial',
|
|
40
|
+
gate: { passed: true, source: gateSource || defaults.gateSource },
|
|
41
|
+
summary: { selftest: true, tests_passed: 1, manual_review_required: true },
|
|
42
|
+
artifacts: artifacts || defaults.artifacts,
|
|
43
|
+
evidence: {
|
|
44
|
+
route_gate: { passed: true },
|
|
45
|
+
commands: [{ cmd: 'sks selftest --mock', status: 'verified_partial' }]
|
|
46
|
+
},
|
|
47
|
+
claims: [{
|
|
48
|
+
id: `selftest-${kind.replaceAll('_', '-')}-proof`,
|
|
49
|
+
text: CLAIM_TEXT[kind] || CLAIM_TEXT.team_gate,
|
|
50
|
+
status: 'verified_partial'
|
|
51
|
+
}],
|
|
52
|
+
unverified: unverified || defaults.unverified
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { containsPlaintextSecret } from '../secret-redaction.mjs';
|
|
2
|
+
import { COMPLETION_PROOF_SCHEMA, COMPLETION_PROOF_STATUSES } from './proof-schema.mjs';
|
|
3
|
+
|
|
4
|
+
export function validateCompletionProof(proof = {}) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
if (proof.schema !== COMPLETION_PROOF_SCHEMA) issues.push('schema');
|
|
7
|
+
if (!COMPLETION_PROOF_STATUSES.includes(proof.status)) issues.push('status');
|
|
8
|
+
if (!proof.summary || typeof proof.summary !== 'object') issues.push('summary');
|
|
9
|
+
if (!proof.evidence || typeof proof.evidence !== 'object') issues.push('evidence');
|
|
10
|
+
if (!Array.isArray(proof.claims)) issues.push('claims');
|
|
11
|
+
if (!Array.isArray(proof.unverified)) issues.push('unverified');
|
|
12
|
+
if (!Array.isArray(proof.blockers)) issues.push('blockers');
|
|
13
|
+
if (containsPlaintextSecret(proof)) issues.push('plaintext_secret');
|
|
14
|
+
if (proof.status === 'failed') issues.push('proof_failed');
|
|
15
|
+
return {
|
|
16
|
+
ok: issues.length === 0,
|
|
17
|
+
status: issues.length ? 'failed' : proof.status,
|
|
18
|
+
issues
|
|
19
|
+
};
|
|
20
|
+
}
|
package/src/core/routes.mjs
CHANGED
|
@@ -532,8 +532,8 @@ export const COMMAND_CATALOG = [
|
|
|
532
532
|
{ name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
|
|
533
533
|
{ name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser tooling, Computer Use, tmux, and Homebrew on macOS.' },
|
|
534
534
|
{ name: 'codex-app', usage: 'sks codex-app [check|open|pat status|remote-control]', description: 'Check Codex App install, PAT-safe status, first-party MCP/plugin readiness, and Codex CLI 0.130.0+ remote-control availability.' },
|
|
535
|
-
{ name: 'hooks', usage: 'sks hooks explain [--json]', description: 'Explain Codex hook events, config locations, handler support, and SKS hook policies without storing raw payloads.' },
|
|
536
|
-
{ name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup
|
|
535
|
+
{ name: 'hooks', usage: 'sks hooks explain|status|trust-report|replay ... [--json]', description: 'Explain Codex hook events, config locations, handler support, replay fixtures, and SKS hook policies without storing raw payloads.' },
|
|
536
|
+
{ name: 'codex-lb', usage: 'sks codex-lb status|health|metrics|doctor|circuit|repair|setup ...', description: 'Configure, health-check, repair, and record circuit evidence for codex-lb provider auth without confusing ChatGPT OAuth and proxy keys.' },
|
|
537
537
|
{ name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb provider auth status, health, repair, and setup commands.' },
|
|
538
538
|
{ name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
|
|
539
539
|
{ name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
|
|
@@ -563,7 +563,8 @@ export const COMMAND_CATALOG = [
|
|
|
563
563
|
{ name: 'db', usage: 'sks db policy|scan|mcp-config|classify|check ...', description: 'Inspect and enforce database/Supabase safety policy.' },
|
|
564
564
|
{ name: 'eval', usage: 'sks eval run|compare|thresholds ...', description: 'Run deterministic context-quality and performance evidence checks.' },
|
|
565
565
|
{ name: 'harness', usage: 'sks harness fixture|review [--json]', description: 'Run Harness Growth Factory fixtures for forgetting, skills, experiments, tool taxonomy, permissions, MultiAgentV2, and tmux views.' },
|
|
566
|
-
{ name: 'perf', usage: 'sks perf run|workflow [--json] [--iterations N]
|
|
566
|
+
{ name: 'perf', usage: 'sks perf run|workflow|cold-start [--json] [--iterations N]', description: 'Measure structured GPT-5.5/SKS performance budgets, including cold-start, Proof Field workflow decisions, and fast-lane evidence.' },
|
|
567
|
+
{ name: 'proof', usage: 'sks proof show|latest|validate|export|smoke [--json|--md]', description: 'Show, validate, export, or smoke-write the unified Completion Proof Engine surface.' },
|
|
567
568
|
{ name: 'proof-field', usage: 'sks proof-field scan [--json] [--intent "task"] [--changed file1,file2]', description: 'Analyze Potential Proof Field cones, negative-work cache, and fast-lane eligibility for a change set.' },
|
|
568
569
|
{ name: 'skill-dream', usage: 'sks skill-dream status|run|record [--json]', description: 'Track generated-skill usage in lightweight JSON and periodically report keep, merge, prune, and improvement candidates without deleting skills automatically.' },
|
|
569
570
|
{ name: 'code-structure', usage: 'sks code-structure scan [--json]', description: 'Scan handwritten source files for 1000/2000/3000-line structure gates and split-review exceptions.' },
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { exists, packageRoot, runProcess, which } from './fsx.mjs';
|
|
2
|
+
import { exists, packageRoot, readText, runProcess, which } from './fsx.mjs';
|
|
3
|
+
import { sha256File } from './wiki-image/image-hash.mjs';
|
|
4
|
+
import { validateImageVoxelLedger } from './wiki-image/validation.mjs';
|
|
5
|
+
import { readImageVoxelLedger } from './wiki-image/image-voxel-ledger.mjs';
|
|
3
6
|
|
|
4
7
|
export async function findRustAccelerator() {
|
|
5
8
|
const env = process.env.SKS_RS_BIN || process.env.DCODEX_RS_BIN;
|
|
@@ -11,9 +14,58 @@ export async function findRustAccelerator() {
|
|
|
11
14
|
return null;
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
export async function runRustOrFallback(command, args = [], fallbackFn = async () => null) {
|
|
18
|
+
const bin = await findRustAccelerator();
|
|
19
|
+
if (!bin) return normalizeAcceleratorResult(command, { engine: 'js', available: false, result: await fallbackFn() });
|
|
20
|
+
const result = await runProcess(bin, [command, ...args], { timeoutMs: 10000, maxOutputBytes: 1024 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
21
|
+
if (result.code !== 0) return normalizeAcceleratorResult(command, { engine: 'js', available: true, rust_error: classifyRustError(command, result.stderr || result.stdout), result: await fallbackFn() });
|
|
22
|
+
return normalizeAcceleratorResult(command, { engine: 'rust', available: true, stdout: result.stdout.trim(), result: parseRustJson(result.stdout) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function rustImageHash(file) {
|
|
26
|
+
return runRustOrFallback('image-hash', [file], async () => ({ ok: true, engine: 'js', path: file, sha256: await sha256File(file) }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function rustVoxelValidate(file) {
|
|
30
|
+
return runRustOrFallback('voxel-validate', [file], async () => {
|
|
31
|
+
const validation = validateImageVoxelLedger(await readImageVoxelLedger(packageRoot(), file));
|
|
32
|
+
return { ok: validation.ok, engine: 'js', schema: 'sks.image-voxel-ledger.v1', images: validation.summary.images, anchors: validation.summary.anchors, issues: validation.issues };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function rustSecretScan(file) {
|
|
37
|
+
return runRustOrFallback('secret-scan', [file], async () => {
|
|
38
|
+
const text = await readText(file, '');
|
|
39
|
+
return { ok: !/(CODEX_ACCESS_TOKEN|OPENAI_API_KEY|CODEX_LB_API_KEY|sk-proj-|sk-clb-|github_pat_)/.test(text) };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
export async function rustInfo() {
|
|
15
44
|
const bin = await findRustAccelerator();
|
|
16
|
-
|
|
45
|
+
const capabilities = ['compact-info', 'jsonl-tail', 'secret-scan', 'image-hash', 'voxel-validate'];
|
|
46
|
+
if (!bin) return { available: false, capabilities, packaging: 'source_checkout_or_optional_path', note: 'Rust accelerator available only from source checkout or SKS_RS_BIN until prebuilt packages exist.' };
|
|
17
47
|
const result = await runProcess(bin, ['--version'], { timeoutMs: 3000, maxOutputBytes: 20_000 });
|
|
18
|
-
return { available: result.code === 0, bin, version: `${result.stdout}${result.stderr}`.trim() };
|
|
48
|
+
return { available: result.code === 0, bin, version: `${result.stdout}${result.stderr}`.trim(), capabilities, packaging: 'source_checkout_or_optional_path' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseRustJson(text = '') {
|
|
52
|
+
try { return JSON.parse(text); } catch { return text.trim(); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function classifyRustError(command, text = '') {
|
|
56
|
+
const s = String(text || '');
|
|
57
|
+
if (/unknown|Commands:|optional accelerator/i.test(s)) return { kind: 'command_missing', command, message: s };
|
|
58
|
+
return { kind: 'runtime_error', command, message: s };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeAcceleratorResult(command, value) {
|
|
62
|
+
const result = value.result && typeof value.result === 'object' ? value.result : { ok: false, value: value.result };
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
engine: value.engine,
|
|
66
|
+
available: Boolean(value.available),
|
|
67
|
+
rust_error: value.rust_error || null,
|
|
68
|
+
stdout: value.stdout || null,
|
|
69
|
+
result
|
|
70
|
+
};
|
|
19
71
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SECRET_ENV_NAMES = [
|
|
2
|
+
'CODEX_ACCESS_TOKEN',
|
|
3
|
+
'OPENAI_API_KEY',
|
|
4
|
+
'CODEX_LB_API_KEY',
|
|
5
|
+
'ANTHROPIC_API_KEY',
|
|
6
|
+
'GITHUB_TOKEN',
|
|
7
|
+
'GITHUB_PAT'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
/\bsk-proj-[A-Za-z0-9_-]{12,}\b/g,
|
|
12
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
13
|
+
/\bsk-clb-[A-Za-z0-9_-]{8,}\b/g,
|
|
14
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
15
|
+
/\bghp_[A-Za-z0-9_]{20,}\b/g,
|
|
16
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/gi,
|
|
17
|
+
/\b(?:access[_-]?token|api[_-]?key|secret|password|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{12,}["']?/gi
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const REDACTION_MARKER = '[redacted]';
|
|
21
|
+
|
|
22
|
+
export function redactSecrets(value, env = process.env) {
|
|
23
|
+
if (value == null) return value;
|
|
24
|
+
if (typeof value === 'string') return redactString(value, env);
|
|
25
|
+
if (Array.isArray(value)) return value.map((item) => redactSecrets(item, env));
|
|
26
|
+
if (typeof value === 'object') {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
29
|
+
out[key] = secretKeyName(key) ? REDACTION_MARKER : redactSecrets(candidate, env);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function redactString(input = '', env = process.env) {
|
|
37
|
+
let out = String(input);
|
|
38
|
+
for (const name of SECRET_ENV_NAMES) {
|
|
39
|
+
const raw = env?.[name];
|
|
40
|
+
if (raw && raw.length >= 4) out = out.split(raw).join(REDACTION_MARKER);
|
|
41
|
+
out = out.replace(new RegExp(`(${name}\\s*[:=]\\s*)[^\\s"',}]+`, 'gi'), `$1${REDACTION_MARKER}`);
|
|
42
|
+
}
|
|
43
|
+
for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, (match) => redactKeyValue(match));
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function containsPlaintextSecret(value, env = process.env) {
|
|
48
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value || {});
|
|
49
|
+
for (const name of SECRET_ENV_NAMES) {
|
|
50
|
+
const raw = env?.[name];
|
|
51
|
+
if (raw && raw.length >= 4 && text.includes(raw)) return true;
|
|
52
|
+
}
|
|
53
|
+
return SECRET_PATTERNS.some((pattern) => {
|
|
54
|
+
pattern.lastIndex = 0;
|
|
55
|
+
return pattern.test(text);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function secretKeyName(key = '') {
|
|
60
|
+
return /(?:access[_-]?token|api[_-]?key|secret|password|token)$/i.test(String(key || ''))
|
|
61
|
+
|| SECRET_ENV_NAMES.includes(String(key || '').toUpperCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function redactKeyValue(match) {
|
|
65
|
+
const keyValue = String(match).match(/^([^:=]+[:=]\s*)/);
|
|
66
|
+
if (keyValue) return `${keyValue[1]}${REDACTION_MARKER}`;
|
|
67
|
+
if (/^Bearer\s+/i.test(match)) return `Bearer ${REDACTION_MARKER}`;
|
|
68
|
+
return REDACTION_MARKER;
|
|
69
|
+
}
|
|
@@ -308,13 +308,17 @@ async function syncPackageLockVersions(root, version) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
async function syncSourcePackageVersion(root, version) {
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
const files = [];
|
|
312
|
+
for (const rel of ['src/core/fsx.mjs', 'src/core/version.mjs']) {
|
|
313
|
+
const file = path.join(root, rel);
|
|
314
|
+
const text = await readFileMaybe(file);
|
|
315
|
+
if (!text) continue;
|
|
316
|
+
const next = text.replace(/export const PACKAGE_VERSION = ['"][^'"]+['"];/, `export const PACKAGE_VERSION = '${version}';`);
|
|
317
|
+
if (next === text) continue;
|
|
318
|
+
await writeTextAtomic(file, next);
|
|
319
|
+
files.push(file);
|
|
320
|
+
}
|
|
321
|
+
return { files, relative_files: files.map((file) => path.relative(root, file)) };
|
|
318
322
|
}
|
|
319
323
|
|
|
320
324
|
async function syncChangelogVersionSection(root, version) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PACKAGE_VERSION = '0.9.13';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function validateBbox(bbox, image = {}) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) return { ok: false, issues: ['bbox_shape'] };
|
|
4
|
+
const [x, y, width, height] = bbox.map(Number);
|
|
5
|
+
if (![x, y, width, height].every(Number.isFinite)) issues.push('bbox_number');
|
|
6
|
+
if (x < 0 || y < 0 || width <= 0 || height <= 0) issues.push('bbox_positive');
|
|
7
|
+
if (Number.isFinite(Number(image.width)) && x + width > Number(image.width)) issues.push('bbox_width_out_of_bounds');
|
|
8
|
+
if (Number.isFinite(Number(image.height)) && y + height > Number(image.height)) issues.push('bbox_height_out_of_bounds');
|
|
9
|
+
return { ok: issues.length === 0, issues };
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function parseGeneratedReviewCallouts(ledger = {}) {
|
|
2
|
+
const items = Array.isArray(ledger.callouts)
|
|
3
|
+
? ledger.callouts
|
|
4
|
+
: Array.isArray(ledger.issues)
|
|
5
|
+
? ledger.issues
|
|
6
|
+
: [];
|
|
7
|
+
return items.map((item, index) => ({
|
|
8
|
+
id: item.id || `callout-${String(index + 1).padStart(3, '0')}`,
|
|
9
|
+
image_id: item.image_id || item.imageId || ledger.image_id || null,
|
|
10
|
+
bbox: item.bbox || item.box || null,
|
|
11
|
+
label: item.label || item.title || item.issue || `Callout ${index + 1}`,
|
|
12
|
+
source: item.source || ledger.source || 'gpt-image-2-annotated-review',
|
|
13
|
+
evidence_path: item.evidence_path || ledger.path || null,
|
|
14
|
+
trust_score: item.trust_score ?? 0.82
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, packageRoot, readJson } from '../fsx.mjs';
|
|
3
|
+
import { addVisualAnchor, readImageVoxelLedger, writeImageVoxelLedger } from './image-voxel-ledger.mjs';
|
|
4
|
+
import { validateImageVoxelLedger } from './validation.mjs';
|
|
5
|
+
|
|
6
|
+
export async function importComputerUseEvidence(root = packageRoot(), file, opts = {}) {
|
|
7
|
+
const ledger = await readJson(path.resolve(root, file), {});
|
|
8
|
+
const current = await readImageVoxelLedger(root);
|
|
9
|
+
const screens = Array.isArray(ledger.screens) ? ledger.screens : [];
|
|
10
|
+
const images = [
|
|
11
|
+
...(current.images || []),
|
|
12
|
+
...screens.map((screen) => ({
|
|
13
|
+
id: screen.id,
|
|
14
|
+
path: screen.path,
|
|
15
|
+
sha256: screen.sha256 || 'fixture',
|
|
16
|
+
width: screen.width,
|
|
17
|
+
height: screen.height,
|
|
18
|
+
source: screen.source || 'codex-computer-use',
|
|
19
|
+
captured_at: screen.captured_at || nowIso()
|
|
20
|
+
}))
|
|
21
|
+
].filter((image, index, all) => image.id && all.findIndex((entry) => entry.id === image.id) === index);
|
|
22
|
+
let next = await writeImageVoxelLedger(root, { ...current, mission_id: opts.missionId || current.mission_id || null, images });
|
|
23
|
+
for (const action of ledger.actions || []) {
|
|
24
|
+
if (!action.bbox) continue;
|
|
25
|
+
const result = await addVisualAnchor(root, {
|
|
26
|
+
imageId: action.screen_id,
|
|
27
|
+
bbox: action.bbox,
|
|
28
|
+
label: action.target || action.type || 'Computer Use action',
|
|
29
|
+
source: 'codex-computer-use',
|
|
30
|
+
evidencePath: file,
|
|
31
|
+
route: opts.route || '$Computer-Use',
|
|
32
|
+
missionId: opts.missionId
|
|
33
|
+
});
|
|
34
|
+
next = result.ledger;
|
|
35
|
+
}
|
|
36
|
+
const validation = validateImageVoxelLedger(next, { requireAnchors: true, route: opts.route || '$Computer-Use' });
|
|
37
|
+
return { schema: 'sks.computer-use-image-voxel-import.v1', ok: validation.ok, mode: ledger.mode || 'mock', ledger: next, validation };
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export async function sha256File(file) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const hash = createHash('sha256');
|
|
7
|
+
const input = fs.createReadStream(file);
|
|
8
|
+
input.on('error', reject);
|
|
9
|
+
input.on('data', (chunk) => hash.update(chunk));
|
|
10
|
+
input.on('end', () => resolve(hash.digest('hex')));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function imageDimensions(file) {
|
|
15
|
+
const handle = await fs.promises.open(file, 'r');
|
|
16
|
+
try {
|
|
17
|
+
const header = Buffer.alloc(32);
|
|
18
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
19
|
+
if (bytesRead >= 24 && header.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
20
|
+
return { width: header.readUInt32BE(16), height: header.readUInt32BE(20), format: 'png' };
|
|
21
|
+
}
|
|
22
|
+
if (bytesRead >= 10 && header[0] === 0xff && header[1] === 0xd8) return jpegDimensions(file);
|
|
23
|
+
return { width: null, height: null, format: 'unknown' };
|
|
24
|
+
} finally {
|
|
25
|
+
await handle.close().catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function jpegDimensions(file) {
|
|
30
|
+
const buf = await fs.promises.readFile(file);
|
|
31
|
+
let offset = 2;
|
|
32
|
+
while (offset < buf.length) {
|
|
33
|
+
if (buf[offset] !== 0xff) break;
|
|
34
|
+
const marker = buf[offset + 1];
|
|
35
|
+
const length = buf.readUInt16BE(offset + 2);
|
|
36
|
+
if (marker >= 0xc0 && marker <= 0xc3) {
|
|
37
|
+
return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5), format: 'jpeg' };
|
|
38
|
+
}
|
|
39
|
+
offset += 2 + length;
|
|
40
|
+
}
|
|
41
|
+
return { width: null, height: null, format: 'jpeg' };
|
|
42
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, exists, nowIso, packageRoot, readJson, writeJsonAtomic } from '../fsx.mjs';
|
|
3
|
+
import { emptyImageVoxelLedger } from './image-voxel-schema.mjs';
|
|
4
|
+
import { sha256File, imageDimensions } from './image-hash.mjs';
|
|
5
|
+
import { validateImageVoxelLedger } from './validation.mjs';
|
|
6
|
+
import { createImageRelation, createVisualAnchor } from './visual-anchor.mjs';
|
|
7
|
+
|
|
8
|
+
export function wikiImageLedgerPath(root = packageRoot()) {
|
|
9
|
+
return path.join(root, '.sneakoscope', 'wiki', 'image-voxel-ledger.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function wikiImageAssetsPath(root = packageRoot()) {
|
|
13
|
+
return path.join(root, '.sneakoscope', 'wiki', 'image-assets.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function wikiVisualAnchorsPath(root = packageRoot()) {
|
|
17
|
+
return path.join(root, '.sneakoscope', 'wiki', 'visual-anchors.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function missionImageLedgerPath(root = packageRoot(), missionId) {
|
|
21
|
+
return path.join(root, '.sneakoscope', 'missions', missionId, 'image-voxel-ledger.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function missionVisualAnchorsPath(root = packageRoot(), missionId) {
|
|
25
|
+
return path.join(root, '.sneakoscope', 'missions', missionId, 'visual-anchors.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readImageVoxelLedger(root = packageRoot(), file = wikiImageLedgerPath(root)) {
|
|
29
|
+
if (!await exists(file)) return emptyImageVoxelLedger();
|
|
30
|
+
return readJson(file);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function writeImageVoxelLedger(root = packageRoot(), ledger = emptyImageVoxelLedger()) {
|
|
34
|
+
await ensureDir(path.dirname(wikiImageLedgerPath(root)));
|
|
35
|
+
const normalized = { ...emptyImageVoxelLedger(), ...ledger, generated_at: nowIso() };
|
|
36
|
+
await writeJsonAtomic(wikiImageLedgerPath(root), normalized);
|
|
37
|
+
await writeJsonAtomic(wikiImageAssetsPath(root), {
|
|
38
|
+
schema: 'sks.image-assets.v1',
|
|
39
|
+
version: normalized.version,
|
|
40
|
+
generated_at: normalized.generated_at,
|
|
41
|
+
images: normalized.images
|
|
42
|
+
});
|
|
43
|
+
await writeJsonAtomic(wikiVisualAnchorsPath(root), {
|
|
44
|
+
schema: 'sks.visual-anchors.v1',
|
|
45
|
+
version: normalized.version,
|
|
46
|
+
generated_at: normalized.generated_at,
|
|
47
|
+
anchors: normalized.anchors
|
|
48
|
+
});
|
|
49
|
+
if (normalized.mission_id) {
|
|
50
|
+
const missionDir = path.dirname(missionImageLedgerPath(root, normalized.mission_id));
|
|
51
|
+
await ensureDir(missionDir);
|
|
52
|
+
await writeJsonAtomic(missionImageLedgerPath(root, normalized.mission_id), normalized);
|
|
53
|
+
await writeJsonAtomic(missionVisualAnchorsPath(root, normalized.mission_id), {
|
|
54
|
+
schema: 'sks.visual-anchors.v1',
|
|
55
|
+
version: normalized.version,
|
|
56
|
+
generated_at: normalized.generated_at,
|
|
57
|
+
anchors: normalized.anchors
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function ingestImage(root = packageRoot(), imagePath, opts = {}) {
|
|
64
|
+
if (!imagePath) throw new Error('image path required');
|
|
65
|
+
const absolute = path.resolve(root, imagePath);
|
|
66
|
+
const dims = await imageDimensions(absolute);
|
|
67
|
+
const sha256 = await sha256File(absolute);
|
|
68
|
+
const ledger = await readImageVoxelLedger(root);
|
|
69
|
+
const rel = path.relative(root, absolute).split(path.sep).join('/');
|
|
70
|
+
const id = opts.id || stableImageId(rel, sha256);
|
|
71
|
+
const image = {
|
|
72
|
+
id,
|
|
73
|
+
path: rel,
|
|
74
|
+
sha256,
|
|
75
|
+
width: dims.width,
|
|
76
|
+
height: dims.height,
|
|
77
|
+
format: dims.format,
|
|
78
|
+
source: opts.source || 'manual',
|
|
79
|
+
captured_at: opts.capturedAt || nowIso()
|
|
80
|
+
};
|
|
81
|
+
const images = [...(ledger.images || []).filter((entry) => entry.id !== id), image];
|
|
82
|
+
const next = await writeImageVoxelLedger(root, { ...ledger, mission_id: opts.missionId || ledger.mission_id || null, images });
|
|
83
|
+
const validation = validateImageVoxelLedger(next);
|
|
84
|
+
return { ok: validation.ok, image, ledger: next, validation };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function imageVoxelSummary(root = packageRoot(), ledgerFile = wikiImageLedgerPath(root)) {
|
|
88
|
+
const ledger = await readImageVoxelLedger(root, ledgerFile);
|
|
89
|
+
const validation = validateImageVoxelLedger(ledger);
|
|
90
|
+
return {
|
|
91
|
+
schema: 'sks.image-voxel-summary.v1',
|
|
92
|
+
status: validation.status,
|
|
93
|
+
ok: validation.ok,
|
|
94
|
+
images: ledger.images?.length || 0,
|
|
95
|
+
anchors: ledger.anchors?.length || 0,
|
|
96
|
+
anchor_count: ledger.anchors?.length || 0,
|
|
97
|
+
relations: ledger.relations?.length || 0,
|
|
98
|
+
issues: validation.issues
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function addVisualAnchor(root = packageRoot(), input = {}) {
|
|
103
|
+
const ledger = await readImageVoxelLedger(root);
|
|
104
|
+
const image = (ledger.images || []).find((entry) => entry.id === input.imageId);
|
|
105
|
+
const anchor = createVisualAnchor({
|
|
106
|
+
id: input.id || stableAnchorId(input.imageId, input.label, ledger.anchors?.length || 0),
|
|
107
|
+
imageId: input.imageId,
|
|
108
|
+
bbox: input.bbox,
|
|
109
|
+
label: input.label,
|
|
110
|
+
source: input.source || 'manual',
|
|
111
|
+
evidencePath: input.evidencePath || null,
|
|
112
|
+
trustScore: input.trustScore ?? 0.82,
|
|
113
|
+
route: input.route || null,
|
|
114
|
+
claimId: input.claimId || null
|
|
115
|
+
});
|
|
116
|
+
const anchors = [...(ledger.anchors || []).filter((entry) => entry.id !== anchor.id), anchor];
|
|
117
|
+
const next = await writeImageVoxelLedger(root, { ...ledger, mission_id: input.missionId || ledger.mission_id || null, anchors });
|
|
118
|
+
const validation = validateImageVoxelLedger(next, { requireAnchors: true, route: input.route || '$Wiki' });
|
|
119
|
+
return { ok: validation.ok && Boolean(image), anchor, ledger: next, validation: image ? validation : { ...validation, ok: false, issues: [...validation.issues, `missing_image:${input.imageId}`] } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function addImageRelation(root = packageRoot(), input = {}) {
|
|
123
|
+
const ledger = await readImageVoxelLedger(root);
|
|
124
|
+
const relation = createImageRelation({
|
|
125
|
+
type: input.type || 'before_after',
|
|
126
|
+
beforeImageId: input.beforeImageId,
|
|
127
|
+
afterImageId: input.afterImageId,
|
|
128
|
+
anchors: input.anchors || [],
|
|
129
|
+
verification: input.verification || 'changed-screen-recheck',
|
|
130
|
+
status: input.status || 'verified_partial'
|
|
131
|
+
});
|
|
132
|
+
const relations = [...(ledger.relations || []), relation];
|
|
133
|
+
const next = await writeImageVoxelLedger(root, { ...ledger, mission_id: input.missionId || ledger.mission_id || null, relations });
|
|
134
|
+
const validation = validateImageVoxelLedger(next, { requireAnchors: true, requireRelations: true, route: input.route || '$Wiki' });
|
|
135
|
+
return { ok: validation.ok, relation, ledger: next, validation };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stableImageId(rel, sha256) {
|
|
139
|
+
const base = path.basename(rel).replace(/\.[^.]+$/, '').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-|-$/g, '') || 'image';
|
|
140
|
+
return `${base}-${sha256.slice(0, 8)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stableAnchorId(imageId = 'image', label = 'anchor', index = 0) {
|
|
144
|
+
const image = String(imageId || 'image').replace(/[^A-Za-z0-9_-]+/g, '-').slice(0, 40) || 'image';
|
|
145
|
+
const slug = String(label || 'anchor').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-|-$/g, '').slice(0, 32) || 'anchor';
|
|
146
|
+
return `${image}-${slug}-${String(index + 1).padStart(3, '0')}`;
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PACKAGE_VERSION, nowIso } from '../fsx.mjs';
|
|
2
|
+
|
|
3
|
+
export const IMAGE_VOXEL_LEDGER_SCHEMA = 'sks.image-voxel-ledger.v1';
|
|
4
|
+
|
|
5
|
+
export function emptyImageVoxelLedger(overrides = {}) {
|
|
6
|
+
return {
|
|
7
|
+
schema: IMAGE_VOXEL_LEDGER_SCHEMA,
|
|
8
|
+
version: PACKAGE_VERSION,
|
|
9
|
+
generated_at: nowIso(),
|
|
10
|
+
mission_id: null,
|
|
11
|
+
images: [],
|
|
12
|
+
anchors: [],
|
|
13
|
+
relations: [],
|
|
14
|
+
...overrides
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { imageVoxelSummary } from './image-voxel-ledger.mjs';
|
|
2
|
+
|
|
3
|
+
export async function imageVoxelProofEvidence(root, ledgerFile) {
|
|
4
|
+
const summary = await imageVoxelSummary(root, ledgerFile);
|
|
5
|
+
return {
|
|
6
|
+
schema: 'sks.image-voxel-proof-link.v1',
|
|
7
|
+
ok: summary.ok,
|
|
8
|
+
evidence: {
|
|
9
|
+
image_voxels: summary
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IMAGE_VOXEL_LEDGER_SCHEMA } from './image-voxel-schema.mjs';
|
|
2
|
+
import { validateBbox } from './bbox.mjs';
|
|
3
|
+
|
|
4
|
+
export function validateImageVoxelLedger(ledger = {}, opts = {}) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
if (ledger.schema !== IMAGE_VOXEL_LEDGER_SCHEMA) issues.push('schema');
|
|
7
|
+
const images = Array.isArray(ledger.images) ? ledger.images : [];
|
|
8
|
+
const anchors = Array.isArray(ledger.anchors) ? ledger.anchors : [];
|
|
9
|
+
const relations = Array.isArray(ledger.relations) ? ledger.relations : [];
|
|
10
|
+
const imageById = new Map();
|
|
11
|
+
const anchorById = new Map();
|
|
12
|
+
for (const image of images) {
|
|
13
|
+
if (!image.id) issues.push('image_id');
|
|
14
|
+
if (image.id && imageById.has(image.id)) issues.push(`duplicate_image:${image.id}`);
|
|
15
|
+
if (image.id) imageById.set(image.id, image);
|
|
16
|
+
if (!image.path) issues.push(`image_path:${image.id || 'unknown'}`);
|
|
17
|
+
if (!image.sha256) issues.push(`image_sha256:${image.id || 'unknown'}`);
|
|
18
|
+
if (!Number.isFinite(Number(image.width)) || !Number.isFinite(Number(image.height))) issues.push(`image_dimensions:${image.id || 'unknown'}`);
|
|
19
|
+
}
|
|
20
|
+
if (opts.requireAnchors && anchors.length === 0) issues.push(`missing_anchors:${opts.route || 'visual-route'}`);
|
|
21
|
+
for (const anchor of anchors) {
|
|
22
|
+
if (!anchor.id) issues.push('anchor_id');
|
|
23
|
+
if (anchor.id && anchorById.has(anchor.id)) issues.push(`duplicate_anchor:${anchor.id}`);
|
|
24
|
+
if (anchor.id) anchorById.set(anchor.id, anchor);
|
|
25
|
+
if (!anchor.image_id || !imageById.has(anchor.image_id)) issues.push(`anchor_image_ref:${anchor.id || 'unknown'}`);
|
|
26
|
+
if (anchor.bbox) {
|
|
27
|
+
const image = imageById.get(anchor.image_id) || {};
|
|
28
|
+
if (!Number.isFinite(Number(image.width)) || !Number.isFinite(Number(image.height))) issues.push(`bbox_image_dimensions:${anchor.id || 'unknown'}`);
|
|
29
|
+
const bbox = validateBbox(anchor.bbox, image);
|
|
30
|
+
for (const issue of bbox.issues) issues.push(`${issue}:${anchor.id || 'unknown'}`);
|
|
31
|
+
} else {
|
|
32
|
+
issues.push(`anchor_bbox:${anchor.id || 'unknown'}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (opts.requireRelations && relations.length === 0) issues.push(`missing_relations:${opts.route || 'visual-route'}`);
|
|
36
|
+
for (const relation of relations) {
|
|
37
|
+
if (relation.before_image_id && !imageById.has(relation.before_image_id)) issues.push(`relation_before:${relation.before_image_id}`);
|
|
38
|
+
if (relation.after_image_id && !imageById.has(relation.after_image_id)) issues.push(`relation_after:${relation.after_image_id}`);
|
|
39
|
+
for (const anchorId of relation.changed_anchor_ids || relation.anchors || []) {
|
|
40
|
+
if (!anchorById.has(anchorId)) issues.push(`relation_anchor:${anchorId}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: issues.length === 0,
|
|
45
|
+
status: issues.length ? 'blocked' : (anchors.length ? 'verified_partial' : 'not_verified'),
|
|
46
|
+
issues,
|
|
47
|
+
summary: {
|
|
48
|
+
images: images.length,
|
|
49
|
+
anchors: anchors.length,
|
|
50
|
+
relations: relations.length
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|