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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { collectProofEvidence } from './evidence-collector.mjs';
|
|
3
|
+
import { writeCompletionProof } from './proof-writer.mjs';
|
|
4
|
+
import { normalizeProofRoute, routeRequiresImageVoxelAnchors } from './route-proof-policy.mjs';
|
|
5
|
+
|
|
6
|
+
export async function writeRouteCompletionProof(root, {
|
|
7
|
+
missionId = null,
|
|
8
|
+
route = null,
|
|
9
|
+
status = 'verified_partial',
|
|
10
|
+
gate = null,
|
|
11
|
+
summary = {},
|
|
12
|
+
artifacts = [],
|
|
13
|
+
evidence = {},
|
|
14
|
+
claims = [],
|
|
15
|
+
unverified = [],
|
|
16
|
+
blockers = [],
|
|
17
|
+
nextHumanActions = []
|
|
18
|
+
} = {}) {
|
|
19
|
+
const collected = await collectProofEvidence(root);
|
|
20
|
+
const normalizedRoute = normalizeProofRoute(route);
|
|
21
|
+
const mergedEvidence = {
|
|
22
|
+
...collected,
|
|
23
|
+
...evidence,
|
|
24
|
+
route_gate: gate || evidence.route_gate || null,
|
|
25
|
+
artifacts: normalizeArtifacts(root, artifacts)
|
|
26
|
+
};
|
|
27
|
+
const normalizedStatus = normalizeRouteProofStatus(status, {
|
|
28
|
+
route: normalizedRoute,
|
|
29
|
+
evidence: mergedEvidence,
|
|
30
|
+
blockers,
|
|
31
|
+
unverified
|
|
32
|
+
});
|
|
33
|
+
return writeCompletionProof(root, {
|
|
34
|
+
mission_id: missionId,
|
|
35
|
+
route: normalizedRoute,
|
|
36
|
+
status: normalizedStatus,
|
|
37
|
+
summary: {
|
|
38
|
+
files_changed: collected.files?.length || 0,
|
|
39
|
+
commands_run: mergedEvidence.commands?.length || 0,
|
|
40
|
+
tests_passed: 0,
|
|
41
|
+
tests_failed: 0,
|
|
42
|
+
manual_review_required: normalizedStatus !== 'verified',
|
|
43
|
+
...summary
|
|
44
|
+
},
|
|
45
|
+
evidence: mergedEvidence,
|
|
46
|
+
claims,
|
|
47
|
+
unverified,
|
|
48
|
+
blockers,
|
|
49
|
+
next_human_actions: nextHumanActions
|
|
50
|
+
}, {
|
|
51
|
+
command: {
|
|
52
|
+
cmd: `sks proof route ${missionId || 'latest'}`,
|
|
53
|
+
route: normalizedRoute,
|
|
54
|
+
status: normalizedStatus
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRouteProofStatus(status, { route, evidence, blockers, unverified }) {
|
|
60
|
+
if (blockers?.length) return status === 'failed' ? 'failed' : 'blocked';
|
|
61
|
+
if (status === 'verified' && unverified?.length) return 'verified_partial';
|
|
62
|
+
if (routeRequiresImageVoxelAnchors(route)) {
|
|
63
|
+
const anchors = evidence?.image_voxels?.anchors ?? evidence?.image_voxels?.anchor_count ?? 0;
|
|
64
|
+
if (Number(anchors) <= 0) return status === 'verified' ? 'blocked' : status;
|
|
65
|
+
}
|
|
66
|
+
return status;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeArtifacts(root, artifacts = []) {
|
|
70
|
+
return artifacts.map((artifact) => {
|
|
71
|
+
if (typeof artifact !== 'string') return artifact;
|
|
72
|
+
return path.isAbsolute(artifact) ? path.relative(root, artifact).split(path.sep).join('/') : artifact;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { containsPlaintextSecret } from '../secret-redaction.mjs';
|
|
2
|
+
import { readRouteProof } from './proof-reader.mjs';
|
|
3
|
+
import { validateCompletionProof } from './validation.mjs';
|
|
4
|
+
import { proofStatusBlocks, routeRequiresCompletionProof, routeRequiresImageVoxelAnchors } from './route-proof-policy.mjs';
|
|
5
|
+
|
|
6
|
+
export async function validateRouteCompletionProof(root, { missionId = null, route = null, state = {}, visualClaim = true } = {}) {
|
|
7
|
+
const proofRequired = state.proof_required === true || routeRequiresCompletionProof(route);
|
|
8
|
+
if (!proofRequired) return { ok: true, required: false, status: 'not_required', issues: [] };
|
|
9
|
+
const proof = await readRouteProof(root, missionId);
|
|
10
|
+
if (!proof) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
required: true,
|
|
14
|
+
status: 'blocked',
|
|
15
|
+
issues: ['completion_proof_missing']
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const validation = validateCompletionProof(proof);
|
|
19
|
+
const issues = [...validation.issues];
|
|
20
|
+
if (proofStatusBlocks(proof.status)) issues.push(`proof_status_${proof.status}`);
|
|
21
|
+
if (containsPlaintextSecret(proof)) issues.push('plaintext_secret');
|
|
22
|
+
if (routeRequiresImageVoxelAnchors(route || proof.route, { visualClaim })) {
|
|
23
|
+
const anchors = proof.evidence?.image_voxels?.anchors ?? proof.evidence?.image_voxels?.anchor_count ?? 0;
|
|
24
|
+
if (Number(anchors) <= 0) issues.push('image_voxel_anchors_missing');
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ok: issues.length === 0,
|
|
28
|
+
required: true,
|
|
29
|
+
status: issues.length ? 'blocked' : proof.status,
|
|
30
|
+
issues,
|
|
31
|
+
proof
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const SERIOUS_ROUTE_ALIASES = Object.freeze([
|
|
2
|
+
'$Team',
|
|
3
|
+
'$DFix',
|
|
4
|
+
'$QA-LOOP',
|
|
5
|
+
'$Research',
|
|
6
|
+
'$AutoResearch',
|
|
7
|
+
'$PPT',
|
|
8
|
+
'$Image-UX-Review',
|
|
9
|
+
'$UX-Review',
|
|
10
|
+
'$Visual-Review',
|
|
11
|
+
'$UI-UX-Review',
|
|
12
|
+
'$From-Chat-IMG',
|
|
13
|
+
'$Computer-Use',
|
|
14
|
+
'$CU',
|
|
15
|
+
'$DB',
|
|
16
|
+
'$Wiki',
|
|
17
|
+
'$GX',
|
|
18
|
+
'$Goal',
|
|
19
|
+
'$MAD-SKS',
|
|
20
|
+
'hproof',
|
|
21
|
+
'proof-field',
|
|
22
|
+
'recallpulse'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const VISUAL_ROUTE_ALIASES = Object.freeze([
|
|
26
|
+
'$Image-UX-Review',
|
|
27
|
+
'$UX-Review',
|
|
28
|
+
'$Visual-Review',
|
|
29
|
+
'$UI-UX-Review',
|
|
30
|
+
'$From-Chat-IMG',
|
|
31
|
+
'$PPT',
|
|
32
|
+
'$QA-LOOP',
|
|
33
|
+
'$Computer-Use',
|
|
34
|
+
'$CU',
|
|
35
|
+
'$GX'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const ROUTE_NORMALIZATION = Object.freeze({
|
|
39
|
+
team: '$Team',
|
|
40
|
+
dfix: '$DFix',
|
|
41
|
+
qaloop: '$QA-LOOP',
|
|
42
|
+
'qa-loop': '$QA-LOOP',
|
|
43
|
+
research: '$Research',
|
|
44
|
+
autoresearch: '$AutoResearch',
|
|
45
|
+
ppt: '$PPT',
|
|
46
|
+
imageuxreview: '$Image-UX-Review',
|
|
47
|
+
'image-ux-review': '$Image-UX-Review',
|
|
48
|
+
uxreview: '$UX-Review',
|
|
49
|
+
'ux-review': '$UX-Review',
|
|
50
|
+
visualreview: '$Visual-Review',
|
|
51
|
+
'visual-review': '$Visual-Review',
|
|
52
|
+
uiuxreview: '$UI-UX-Review',
|
|
53
|
+
'ui-ux-review': '$UI-UX-Review',
|
|
54
|
+
fromchatimg: '$From-Chat-IMG',
|
|
55
|
+
'from-chat-img': '$From-Chat-IMG',
|
|
56
|
+
computeruse: '$Computer-Use',
|
|
57
|
+
'computer-use': '$Computer-Use',
|
|
58
|
+
cu: '$CU',
|
|
59
|
+
db: '$DB',
|
|
60
|
+
wiki: '$Wiki',
|
|
61
|
+
gx: '$GX',
|
|
62
|
+
goal: '$Goal',
|
|
63
|
+
madsks: '$MAD-SKS',
|
|
64
|
+
'mad-sks': '$MAD-SKS',
|
|
65
|
+
hproof: 'hproof',
|
|
66
|
+
prooffield: 'proof-field',
|
|
67
|
+
'proof-field': 'proof-field',
|
|
68
|
+
recallpulse: 'recallpulse'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export function normalizeProofRoute(route) {
|
|
72
|
+
const raw = String(route || '').trim();
|
|
73
|
+
if (!raw) return null;
|
|
74
|
+
if (SERIOUS_ROUTE_ALIASES.includes(raw) || VISUAL_ROUTE_ALIASES.includes(raw)) return raw;
|
|
75
|
+
const stripped = raw.replace(/^\$/, '').replace(/[^A-Za-z0-9-]+/g, '').toLowerCase();
|
|
76
|
+
return ROUTE_NORMALIZATION[stripped] || raw;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function routeRequiresCompletionProof(route) {
|
|
80
|
+
const normalized = normalizeProofRoute(route);
|
|
81
|
+
return SERIOUS_ROUTE_ALIASES.includes(normalized);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function routeRequiresImageVoxelAnchors(route, opts = {}) {
|
|
85
|
+
const normalized = normalizeProofRoute(route);
|
|
86
|
+
if (opts.visualClaim === false) return false;
|
|
87
|
+
return VISUAL_ROUTE_ALIASES.includes(normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function routeFromState(state = {}) {
|
|
91
|
+
return normalizeProofRoute(state.route_command || state.route || state.mode || state.route_id || state.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function proofStatusBlocks(status) {
|
|
95
|
+
return status === 'failed' || status === 'blocked' || status === 'not_verified';
|
|
96
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,7 @@ export function validateCompletionProof(proof = {}) {
|
|
|
11
11
|
if (!Array.isArray(proof.unverified)) issues.push('unverified');
|
|
12
12
|
if (!Array.isArray(proof.blockers)) issues.push('blockers');
|
|
13
13
|
if (containsPlaintextSecret(proof)) issues.push('plaintext_secret');
|
|
14
|
+
if (proof.status === 'failed') issues.push('proof_failed');
|
|
14
15
|
return {
|
|
15
16
|
ok: issues.length === 0,
|
|
16
17
|
status: issues.length ? 'failed' : proof.status,
|
|
@@ -16,18 +16,21 @@ export async function findRustAccelerator() {
|
|
|
16
16
|
|
|
17
17
|
export async function runRustOrFallback(command, args = [], fallbackFn = async () => null) {
|
|
18
18
|
const bin = await findRustAccelerator();
|
|
19
|
-
if (!bin) return { engine: 'js', available: false, result: await fallbackFn() };
|
|
19
|
+
if (!bin) return normalizeAcceleratorResult(command, { engine: 'js', available: false, result: await fallbackFn() });
|
|
20
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 { engine: 'js', available: true, rust_error: result.stderr || result.stdout, result: await fallbackFn() };
|
|
22
|
-
return { engine: 'rust', available: true, stdout: result.stdout.trim(), result: parseRustJson(result.stdout) };
|
|
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
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function rustImageHash(file) {
|
|
26
|
-
return runRustOrFallback('image-hash', [file], async () => ({ sha256: await sha256File(file) }));
|
|
26
|
+
return runRustOrFallback('image-hash', [file], async () => ({ ok: true, engine: 'js', path: file, sha256: await sha256File(file) }));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function rustVoxelValidate(file) {
|
|
30
|
-
return runRustOrFallback('voxel-validate', [file], async () =>
|
|
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
|
+
});
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export async function rustSecretScan(file) {
|
|
@@ -39,11 +42,30 @@ export async function rustSecretScan(file) {
|
|
|
39
42
|
|
|
40
43
|
export async function rustInfo() {
|
|
41
44
|
const bin = await findRustAccelerator();
|
|
42
|
-
|
|
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.' };
|
|
43
47
|
const result = await runProcess(bin, ['--version'], { timeoutMs: 3000, maxOutputBytes: 20_000 });
|
|
44
|
-
return { available: result.code === 0, bin, version: `${result.stdout}${result.stderr}`.trim(), packaging: 'source_checkout_or_optional_path' };
|
|
48
|
+
return { available: result.code === 0, bin, version: `${result.stdout}${result.stderr}`.trim(), capabilities, packaging: 'source_checkout_or_optional_path' };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function parseRustJson(text = '') {
|
|
48
52
|
try { return JSON.parse(text); } catch { return text.trim(); }
|
|
49
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
|
+
};
|
|
71
|
+
}
|
package/src/core/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '0.9.
|
|
1
|
+
export const PACKAGE_VERSION = '0.9.13';
|
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ensureDir, exists, nowIso, packageRoot, readJson, writeJsonAtomic } fro
|
|
|
3
3
|
import { emptyImageVoxelLedger } from './image-voxel-schema.mjs';
|
|
4
4
|
import { sha256File, imageDimensions } from './image-hash.mjs';
|
|
5
5
|
import { validateImageVoxelLedger } from './validation.mjs';
|
|
6
|
+
import { createImageRelation, createVisualAnchor } from './visual-anchor.mjs';
|
|
6
7
|
|
|
7
8
|
export function wikiImageLedgerPath(root = packageRoot()) {
|
|
8
9
|
return path.join(root, '.sneakoscope', 'wiki', 'image-voxel-ledger.json');
|
|
@@ -98,7 +99,49 @@ export async function imageVoxelSummary(root = packageRoot(), ledgerFile = wikiI
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
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
|
+
|
|
101
138
|
function stableImageId(rel, sha256) {
|
|
102
139
|
const base = path.basename(rel).replace(/\.[^.]+$/, '').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-|-$/g, '') || 'image';
|
|
103
140
|
return `${base}-${sha256.slice(0, 8)}`;
|
|
104
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,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
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { IMAGE_VOXEL_LEDGER_SCHEMA } from './image-voxel-schema.mjs';
|
|
2
2
|
import { validateBbox } from './bbox.mjs';
|
|
3
3
|
|
|
4
|
-
export function validateImageVoxelLedger(ledger = {}) {
|
|
4
|
+
export function validateImageVoxelLedger(ledger = {}, opts = {}) {
|
|
5
5
|
const issues = [];
|
|
6
6
|
if (ledger.schema !== IMAGE_VOXEL_LEDGER_SCHEMA) issues.push('schema');
|
|
7
7
|
const images = Array.isArray(ledger.images) ? ledger.images : [];
|
|
8
8
|
const anchors = Array.isArray(ledger.anchors) ? ledger.anchors : [];
|
|
9
|
+
const relations = Array.isArray(ledger.relations) ? ledger.relations : [];
|
|
9
10
|
const imageById = new Map();
|
|
11
|
+
const anchorById = new Map();
|
|
10
12
|
for (const image of images) {
|
|
11
13
|
if (!image.id) issues.push('image_id');
|
|
12
14
|
if (image.id && imageById.has(image.id)) issues.push(`duplicate_image:${image.id}`);
|
|
@@ -15,28 +17,37 @@ export function validateImageVoxelLedger(ledger = {}) {
|
|
|
15
17
|
if (!image.sha256) issues.push(`image_sha256:${image.id || 'unknown'}`);
|
|
16
18
|
if (!Number.isFinite(Number(image.width)) || !Number.isFinite(Number(image.height))) issues.push(`image_dimensions:${image.id || 'unknown'}`);
|
|
17
19
|
}
|
|
20
|
+
if (opts.requireAnchors && anchors.length === 0) issues.push(`missing_anchors:${opts.route || 'visual-route'}`);
|
|
18
21
|
for (const anchor of anchors) {
|
|
19
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);
|
|
20
25
|
if (!anchor.image_id || !imageById.has(anchor.image_id)) issues.push(`anchor_image_ref:${anchor.id || 'unknown'}`);
|
|
21
26
|
if (anchor.bbox) {
|
|
22
|
-
const
|
|
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);
|
|
23
30
|
for (const issue of bbox.issues) issues.push(`${issue}:${anchor.id || 'unknown'}`);
|
|
24
31
|
} else {
|
|
25
32
|
issues.push(`anchor_bbox:${anchor.id || 'unknown'}`);
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
if (opts.requireRelations && relations.length === 0) issues.push(`missing_relations:${opts.route || 'visual-route'}`);
|
|
36
|
+
for (const relation of relations) {
|
|
29
37
|
if (relation.before_image_id && !imageById.has(relation.before_image_id)) issues.push(`relation_before:${relation.before_image_id}`);
|
|
30
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
|
+
}
|
|
31
42
|
}
|
|
32
43
|
return {
|
|
33
44
|
ok: issues.length === 0,
|
|
34
|
-
status: issues.length ? 'blocked' : 'verified_partial',
|
|
45
|
+
status: issues.length ? 'blocked' : (anchors.length ? 'verified_partial' : 'not_verified'),
|
|
35
46
|
issues,
|
|
36
47
|
summary: {
|
|
37
48
|
images: images.length,
|
|
38
49
|
anchors: anchors.length,
|
|
39
|
-
relations:
|
|
50
|
+
relations: relations.length
|
|
40
51
|
}
|
|
41
52
|
};
|
|
42
53
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rgbaKey, rgbaToWikiCoord } from '../wiki-coordinate.mjs';
|
|
2
2
|
|
|
3
|
-
export function createVisualAnchor({ id, imageId, bbox, label, source, evidencePath, trustScore = 0.5, rgba = [58, 132, 210, 240] } = {}) {
|
|
3
|
+
export function createVisualAnchor({ id, imageId, bbox, label, source, evidencePath, trustScore = 0.5, rgba = [58, 132, 210, 240], route = null, claimId = null } = {}) {
|
|
4
4
|
const key = Array.isArray(rgba) ? rgbaKey(rgba) : String(rgba || '3a84d2f0');
|
|
5
5
|
const rgbaTuple = Array.isArray(rgba)
|
|
6
6
|
? rgba
|
|
@@ -16,6 +16,8 @@ export function createVisualAnchor({ id, imageId, bbox, label, source, evidenceP
|
|
|
16
16
|
evidence_path: evidencePath || null,
|
|
17
17
|
trust_score: trustScore,
|
|
18
18
|
trust_band: source || 'visual-anchor',
|
|
19
|
+
route,
|
|
20
|
+
claim_id: claimId,
|
|
19
21
|
voxel_layers: {
|
|
20
22
|
sem: 0.7,
|
|
21
23
|
trust: trustScore,
|
|
@@ -27,3 +29,14 @@ export function createVisualAnchor({ id, imageId, bbox, label, source, evidenceP
|
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
export function createImageRelation({ type = 'before_after', beforeImageId, afterImageId, anchors = [], verification = 'changed-screen-recheck', status = 'verified_partial' } = {}) {
|
|
34
|
+
return {
|
|
35
|
+
type,
|
|
36
|
+
before_image_id: beforeImageId,
|
|
37
|
+
after_image_id: afterImageId,
|
|
38
|
+
changed_anchor_ids: Array.isArray(anchors) ? anchors : String(anchors || '').split(',').map((x) => x.trim()).filter(Boolean),
|
|
39
|
+
verification,
|
|
40
|
+
status
|
|
41
|
+
};
|
|
42
|
+
}
|