sneakoscope 5.1.2 → 5.5.0

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 (97) hide show
  1. package/README.md +5 -1
  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 +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/commands/doctor.js +32 -3
  7. package/dist/config/skills-manifest.json +58 -58
  8. package/dist/core/agents/agent-lifecycle.js +5 -3
  9. package/dist/core/agents/agent-orchestrator.js +14 -2
  10. package/dist/core/agents/agent-proof-evidence.js +16 -0
  11. package/dist/core/agents/native-cli-worker.js +0 -1
  12. package/dist/core/bench.js +24 -2
  13. package/dist/core/codex-app/sks-menubar.js +526 -133
  14. package/dist/core/commands/agent-command.js +35 -1
  15. package/dist/core/commands/basic-cli.js +61 -4
  16. package/dist/core/commands/command-utils.js +14 -4
  17. package/dist/core/commands/goal-command.js +3 -0
  18. package/dist/core/commands/image-ux-review-command.js +4 -1
  19. package/dist/core/commands/mad-sks-command.js +10 -1
  20. package/dist/core/commands/menubar-command.js +2 -1
  21. package/dist/core/commands/naruto-command.js +12 -7
  22. package/dist/core/commands/ppt-command.js +40 -1
  23. package/dist/core/commands/qa-loop-command.js +4 -1
  24. package/dist/core/commands/release-command.js +3 -3
  25. package/dist/core/commands/research-command.js +4 -0
  26. package/dist/core/commands/route-success-helpers.js +2 -0
  27. package/dist/core/commands/run-command.js +22 -5
  28. package/dist/core/commands/seo-command.js +31 -4
  29. package/dist/core/db-safety.js +8 -1
  30. package/dist/core/doctor/doctor-dirty-planner.js +51 -7
  31. package/dist/core/feature-fixture-executor.js +262 -0
  32. package/dist/core/feature-fixtures.js +95 -52
  33. package/dist/core/fsx.js +1 -1
  34. package/dist/core/loops/loop-decomposer.js +4 -3
  35. package/dist/core/loops/loop-planner.js +7 -3
  36. package/dist/core/loops/loop-worker-runtime.js +6 -0
  37. package/dist/core/naruto/naruto-real-worker-child.js +1 -1
  38. package/dist/core/pipeline-internals/runtime-gates.js +39 -3
  39. package/dist/core/proof/selftest-proof-fixtures.js +8 -0
  40. package/dist/core/providers/glm/naruto/glm-naruto-requirement-coverage.js +18 -1
  41. package/dist/core/providers/glm/naruto/glm-naruto-requirement-ledger.js +28 -12
  42. package/dist/core/questions.js +12 -10
  43. package/dist/core/release/release-gate-node.js +2 -0
  44. package/dist/core/routes.js +11 -0
  45. package/dist/core/stop-gate/stop-gate-check.js +4 -0
  46. package/dist/core/stop-gate/stop-gate-writer.js +4 -0
  47. package/dist/core/trust-kernel/trust-report.js +10 -1
  48. package/dist/core/update/update-migration-state.js +23 -3
  49. package/dist/core/version.js +1 -1
  50. package/dist/core/work-order-ledger.js +60 -0
  51. package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
  52. package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +16 -0
  53. package/dist/scripts/doctor-fix-production-blackbox.js +26 -0
  54. package/dist/scripts/doctor-startup-config-repair-blackbox.js +13 -0
  55. package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +14 -0
  56. package/dist/scripts/gate-policy-audit-check.js +2 -2
  57. package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
  58. package/dist/scripts/packlist-performance-check.js +7 -1
  59. package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
  60. package/dist/scripts/release-dag-full-coverage-check.js +1 -1
  61. package/dist/scripts/release-gate-existence-audit.js +1 -1
  62. package/dist/scripts/release-gate-planner.js +1 -1
  63. package/dist/scripts/release-gate-script-parity-check.js +44 -16
  64. package/dist/scripts/release-metadata-1-19-check.js +2 -2
  65. package/dist/scripts/sks-menubar-install-check.js +57 -20
  66. package/dist/scripts/supabase-secret-preservation-blackbox.js +29 -0
  67. package/dist/scripts/update-preserves-supabase-keys-blackbox.js +27 -0
  68. package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
  69. package/package.json +12 -65
  70. package/schemas/release/release-gate-node.schema.json +1 -0
  71. package/bench/tasks/t01-off-by-one/repo/package.json +0 -9
  72. package/bench/tasks/t01-off-by-one/repo/src/pagination.js +0 -5
  73. package/bench/tasks/t01-off-by-one/repo/test.js +0 -5
  74. package/bench/tasks/t01-off-by-one/task.json +0 -13
  75. package/bench/tasks/t02-signature-cochange/repo/package.json +0 -9
  76. package/bench/tasks/t02-signature-cochange/repo/src/admin.js +0 -5
  77. package/bench/tasks/t02-signature-cochange/repo/src/audit.js +0 -5
  78. package/bench/tasks/t02-signature-cochange/repo/src/labels.js +0 -3
  79. package/bench/tasks/t02-signature-cochange/repo/src/profile.js +0 -5
  80. package/bench/tasks/t02-signature-cochange/repo/test.js +0 -9
  81. package/bench/tasks/t02-signature-cochange/task.json +0 -12
  82. package/bench/tasks/t03-type-puzzle/repo/package.json +0 -9
  83. package/bench/tasks/t03-type-puzzle/repo/src/config.js +0 -4
  84. package/bench/tasks/t03-type-puzzle/repo/test.js +0 -6
  85. package/bench/tasks/t03-type-puzzle/task.json +0 -13
  86. package/bench/tasks/t04-refactor-preserve/repo/package.json +0 -9
  87. package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +0 -8
  88. package/bench/tasks/t04-refactor-preserve/repo/test.js +0 -4
  89. package/bench/tasks/t04-refactor-preserve/task.json +0 -12
  90. package/bench/tasks/t05-performance/repo/package.json +0 -9
  91. package/bench/tasks/t05-performance/repo/src/pairs.js +0 -9
  92. package/bench/tasks/t05-performance/repo/test.js +0 -11
  93. package/bench/tasks/t05-performance/task.json +0 -12
  94. package/bench/tasks/t06-mistake-rule/repo/package.json +0 -9
  95. package/bench/tasks/t06-mistake-rule/repo/src/loader.js +0 -6
  96. package/bench/tasks/t06-mistake-rule/repo/test.js +0 -7
  97. package/bench/tasks/t06-mistake-rule/task.json +0 -12
@@ -645,7 +645,14 @@ async function scanSupabaseMcpConfigs(root) {
645
645
  }
646
646
  function extractSupabaseMcpUrls(text) {
647
647
  const out = new Set();
648
- const re = /https:\/\/mcp\.supabase\.com\/mcp[^"'\s)>,]*/gi;
648
+ // Config files (TOML/JSON) always quote URL values, so a closing quote or
649
+ // whitespace is the only reliable terminator. A comma is a legitimate query-string
650
+ // character (e.g. "...&features=database,docs") - excluding it here silently
651
+ // truncated multi-value features lists, which happened to be benign only because
652
+ // the truncated remainder still contained an allowed feature. Stopping at ')'/'>' is
653
+ // unnecessary for structured config scanning and risks the same truncation for any
654
+ // URL whose query string legitimately contains those characters (URL-encoded or not).
655
+ const re = /https:\/\/mcp\.supabase\.com\/mcp[^"'\s]*/gi;
649
656
  let m;
650
657
  while ((m = re.exec(text)))
651
658
  out.add(m[0]);
@@ -1,8 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
4
5
  import { PACKAGE_VERSION, packageRoot, sha256 } from '../fsx.js';
5
6
  import { hashJson } from '../triwiki/triwiki-cache-key.js';
7
+ import { SKS_MENUBAR_LABEL, sksMenuBarPaths } from '../codex-app/sks-menubar.js';
6
8
  export const DOCTOR_DIRTY_PLAN_SCHEMA = 'sks.doctor-dirty-plan.v2';
7
9
  export function planDoctorDirtyRepair(root, phaseIds) {
8
10
  const phases = phaseIds.map((id) => {
@@ -10,25 +12,30 @@ export function planDoctorDirtyRepair(root, phaseIds) {
10
12
  const inputHash = phaseInputHash(root, id);
11
13
  const postcheckRequired = phaseRequiresPostcheck(id);
12
14
  const markerState = readMarker(marker);
15
+ const runtimeProbeFailed = runtimeProbeFailures(root, id);
13
16
  if (!markerState)
14
- return { id, status: 'dirty', reason: 'no_clean_marker', input_hash: inputHash, last_clean_proof_id: null, postcheck_required: postcheckRequired, postcheck_pending: false };
17
+ return { id, status: 'dirty', reason: 'no_clean_marker', input_hash: inputHash, last_clean_proof_id: null, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
15
18
  if (markerState.input_hash !== inputHash) {
16
- return { id, status: 'dirty', reason: 'input_hash_changed', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
19
+ return { id, status: 'dirty', reason: 'input_hash_changed', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
17
20
  }
18
21
  if (markerState.proof_id && !proofExists(root, markerState.proof_id)) {
19
- return { id, status: 'dirty', reason: 'clean_proof_missing', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
22
+ return { id, status: 'dirty', reason: 'clean_proof_missing', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
20
23
  }
21
24
  if (!markerState.repair_applied) {
22
- return { id, status: 'dirty', reason: 'repair_not_applied', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false };
25
+ return { id, status: 'dirty', reason: 'repair_not_applied', input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
26
+ }
27
+ if (runtimeProbeFailed.length) {
28
+ return { id, status: 'dirty', reason: `runtime_probe_failed:${runtimeProbeFailed.join(',')}`, input_hash: inputHash, last_clean_proof_id: markerState.proof_id, postcheck_required: postcheckRequired, postcheck_pending: false, runtime_probe_failed: runtimeProbeFailed };
23
29
  }
24
30
  return {
25
31
  id,
26
- status: 'clean',
32
+ status: markerState.postcheck_pending ? 'dirty' : 'clean',
27
33
  reason: postcheckRequired && markerState.postcheck_pending ? 'matching_repair_marker_postcheck_pending' : 'matching_clean_proof',
28
34
  input_hash: inputHash,
29
35
  last_clean_proof_id: markerState.proof_id,
30
36
  postcheck_required: postcheckRequired,
31
- postcheck_pending: postcheckRequired && markerState.postcheck_pending
37
+ postcheck_pending: postcheckRequired && markerState.postcheck_pending,
38
+ runtime_probe_failed: []
32
39
  };
33
40
  });
34
41
  const plan = {
@@ -37,6 +44,7 @@ export function planDoctorDirtyRepair(root, phaseIds) {
37
44
  phases,
38
45
  dirty_count: phases.filter((phase) => phase.status === 'dirty').length,
39
46
  clean_count: phases.filter((phase) => phase.status === 'clean').length,
47
+ runtime_probe_failed: phases.flatMap((phase) => phase.runtime_probe_failed.map((failure) => `${phase.id}:${failure}`)),
40
48
  semantic_dirty_plan_path: dirtyPlanPath(root)
41
49
  };
42
50
  writeDirtyPlan(root, plan);
@@ -236,7 +244,43 @@ function proofExists(root, proofId) {
236
244
  }
237
245
  }
238
246
  function phaseRequiresPostcheck(id) {
239
- return /zellij|context7|startup|supabase|native|secret/i.test(id);
247
+ return /zellij|context7|startup|supabase|native|secret|menubar/i.test(id);
248
+ }
249
+ function runtimeProbeFailures(root, id) {
250
+ if (!id.includes('menubar') || process.platform !== 'darwin')
251
+ return [];
252
+ const failures = [];
253
+ const paths = sksMenuBarPaths(process.env.HOME, root);
254
+ if (!fs.existsSync(paths.executable_path))
255
+ failures.push('menubar_app_missing');
256
+ if (!fs.existsSync(paths.action_script_path))
257
+ failures.push('action_script_missing');
258
+ else {
259
+ try {
260
+ fs.accessSync(paths.action_script_path, fs.constants.X_OK);
261
+ }
262
+ catch {
263
+ failures.push('action_script_not_executable');
264
+ }
265
+ }
266
+ const launchctl = process.env.SKS_MENUBAR_LAUNCHCTL || '/bin/launchctl';
267
+ if (!fs.existsSync(launchctl)) {
268
+ failures.push('launchctl_missing');
269
+ return failures;
270
+ }
271
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
272
+ const service = `${uid === null ? 'gui' : `gui/${uid}`}/${SKS_MENUBAR_LABEL}`;
273
+ const result = spawnSync(launchctl, ['print', service], {
274
+ encoding: 'utf8',
275
+ timeout: 500,
276
+ maxBuffer: 32 * 1024
277
+ });
278
+ const text = `${result.stdout || ''}\n${result.stderr || ''}`;
279
+ if (result.error)
280
+ failures.push(`launchctl_probe_error:${result.error.message}`);
281
+ else if (result.status !== 0 || !(/\bstate = running\b|\bpid = \d+\b/.test(text)))
282
+ failures.push('launchd_not_running');
283
+ return failures;
240
284
  }
241
285
  function writeDirtyPlan(root, plan) {
242
286
  const file = dirtyPlanPath(root);
@@ -0,0 +1,262 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { resolveExpectedArtifactPath } from './feature-fixture-runner.js';
5
+ import { findLatestMission } from './mission.js';
6
+ const FIXTURE_COMMAND_TIMEOUT_MS = 60_000;
7
+ /** Per-fixture override, set via the fixture()'s `extra.timeout_ms` for commands
8
+ * that are legitimately slower than the 60s default (real agent/swarm orchestration,
9
+ * multi-step pipelines) rather than actually hung. */
10
+ function fixtureTimeoutMs(fixture) {
11
+ const override = Number(fixture?.timeout_ms);
12
+ return Number.isFinite(override) && override > 0 ? override : FIXTURE_COMMAND_TIMEOUT_MS;
13
+ }
14
+ /**
15
+ * Actually spawns a feature fixture's declared `command` string (safely tokenized,
16
+ * no shell interpolation) for `execute` / `execute_and_validate_artifacts` fixtures,
17
+ * checks the real exit code, validates any expected_artifacts that were declared
18
+ * (string path or { path, schema } object form), and derives a real pass/fail status
19
+ * from what actually happened rather than trusting fixture.status.
20
+ *
21
+ * This is distinct from runFeatureFixture() in feature-fixture-runner.ts, which only
22
+ * executes fixtures present in an explicit safe-args allowlist. This function executes
23
+ * whatever the fixture itself declares as its command, so it must tokenize defensively
24
+ * and never hand a raw string to a shell.
25
+ */
26
+ export async function runFeatureFixture(feature, { root = process.cwd() } = {}) {
27
+ const id = feature?.id || feature?.featureId || 'unknown';
28
+ const fixture = feature?.fixture || feature || {};
29
+ const kind = fixture.kind;
30
+ const claimedStatus = fixture.status;
31
+ const expected = normalizeExpectedArtifacts(fixture.expected_artifacts);
32
+ if (kind !== 'execute' && kind !== 'execute_and_validate_artifacts') {
33
+ return {
34
+ id,
35
+ kind,
36
+ executed: false,
37
+ skipped: true,
38
+ skip_reason: kind === 'mock' || kind === 'wiring_only' ? 'mock_or_wiring_only' : `kind_not_executable:${kind}`,
39
+ claimed_status: claimedStatus,
40
+ actual_status: claimedStatus,
41
+ ok: true,
42
+ command: fixture.command || null,
43
+ artifacts: [],
44
+ blockers: []
45
+ };
46
+ }
47
+ if (!fixture.command || !String(fixture.command).trim()) {
48
+ return {
49
+ id,
50
+ kind,
51
+ executed: false,
52
+ skipped: false,
53
+ claimed_status: claimedStatus,
54
+ actual_status: 'missing',
55
+ ok: false,
56
+ command: null,
57
+ artifacts: [],
58
+ blockers: [`${id}:fixture_command_missing`]
59
+ };
60
+ }
61
+ const tokens = tokenizeCommand(fixture.command);
62
+ const spawnEnv = { ...process.env, CI: 'true', SKS_SKIP_NPM_FRESHNESS_CHECK: '1', SKS_ENSURE_DIST_NO_REBUILD: '1' };
63
+ const isSksCommand = tokens[0] === 'sks';
64
+ const [spawnCommand, spawnArgs] = isSksCommand
65
+ ? [process.execPath, [resolveSksEntrypoint(root), ...tokens.slice(1)]]
66
+ : [tokens[0] ?? '', tokens.slice(1)];
67
+ const timeoutMs = fixtureTimeoutMs(fixture);
68
+ const spawnResult = spawnSync(spawnCommand, spawnArgs, {
69
+ cwd: root,
70
+ encoding: 'utf8',
71
+ timeout: timeoutMs,
72
+ maxBuffer: 1024 * 1024 * 20,
73
+ env: spawnEnv
74
+ });
75
+ const timedOut = spawnResult.error?.code === 'ETIMEDOUT';
76
+ const exitOk = !timedOut && spawnResult.status === 0;
77
+ // Most fixture commands create a fresh, uniquely-IDed mission and write their
78
+ // expected artifacts (completion-proof.json, <route>-gate.json, ...) inside that
79
+ // mission's own directory, not at the project root. Without a mission id,
80
+ // resolveExpectedArtifactPath() falls back to root-relative resolution and every
81
+ // such artifact is reported "missing" even when the command succeeded. Prefer the
82
+ // mission id the command itself printed in its --json stdout (unambiguous, immune
83
+ // to races with unrelated concurrent sessions on a shared machine); only fall back
84
+ // to a filesystem mtime scan if the command's own output didn't carry one.
85
+ const missionIdFromOutput = extractMissionId(spawnResult.stdout);
86
+ const latestMissionId = missionIdFromOutput
87
+ ?? (kind === 'execute_and_validate_artifacts' ? await findLatestMission(root).catch(() => null) : null);
88
+ const artifacts = kind === 'execute_and_validate_artifacts'
89
+ ? expected.map((artifact) => inspectArtifact(root, artifact, latestMissionId))
90
+ : [];
91
+ const artifactFailures = artifacts.filter((artifact) => !artifact.ok).map((artifact) => `${id}:${artifact.path}:${artifact.failure || 'artifact_invalid'}`);
92
+ const blockers = [];
93
+ if (!exitOk)
94
+ blockers.push(timedOut ? `${id}:command_timeout_${timeoutMs}` : `${id}:command_exit_${spawnResult.status}`);
95
+ blockers.push(...artifactFailures);
96
+ const ok = blockers.length === 0;
97
+ const actualStatus = ok ? 'pass' : 'blocked';
98
+ // "ok" (the ultimate pass/fail signal selftest --real counts) must mean "the
99
+ // registry's claimed status matches reality" - NOT "the underlying command exited
100
+ // 0" - those are different questions. Several fixtures intentionally run a
101
+ // command that always exits non-zero by design (an honest mock/blocked
102
+ // demonstration, per the execution_class:'mock_fixture' hardening elsewhere in
103
+ // this codebase); such a fixture correctly declaring claimed_status:'blocked' is
104
+ // a PASSING self-consistency check, not a failure, even though blockers still
105
+ // faithfully records the real command_exit_N/artifact issues for anyone auditing
106
+ // what actually happened. Only an actual mismatch between claim and reality is a
107
+ // real fixture-registry defect worth failing selftest --real over.
108
+ const statusMatches = !claimedStatus || claimedStatus === actualStatus;
109
+ if (!statusMatches) {
110
+ blockers.push(`${id}:fixture_status_claim_mismatch:claimed=${claimedStatus}:actual=${actualStatus}`);
111
+ }
112
+ return {
113
+ id,
114
+ kind,
115
+ executed: true,
116
+ skipped: false,
117
+ command: fixture.command,
118
+ args: tokens.slice(1),
119
+ exit_code: spawnResult.status,
120
+ signal: spawnResult.signal || null,
121
+ timed_out: timedOut,
122
+ claimed_status: claimedStatus,
123
+ actual_status: actualStatus,
124
+ ok: statusMatches,
125
+ stdout_bytes: Buffer.byteLength(spawnResult.stdout || ''),
126
+ stderr_bytes: Buffer.byteLength(spawnResult.stderr || ''),
127
+ stderr_tail: String(spawnResult.stderr || '').slice(-800),
128
+ artifacts,
129
+ blockers,
130
+ resolved_mission_id: latestMissionId
131
+ };
132
+ }
133
+ /**
134
+ * Pulls a mission id out of a command's JSON stdout. Handles the common field
135
+ * names/shapes used across sks commands (`mission_id`, `missionId`, a top-level
136
+ * `id` that matches the `M-<timestamp>-<suffix>` pattern, or a nested `mission.id`),
137
+ * and tolerates stdout that has trailing non-JSON log lines by scanning for the
138
+ * last parseable JSON object.
139
+ */
140
+ function extractMissionId(stdout) {
141
+ if (!stdout)
142
+ return null;
143
+ const candidates = [];
144
+ for (const line of String(stdout).split('\n')) {
145
+ const trimmed = line.trim();
146
+ if (!trimmed.startsWith('{'))
147
+ continue;
148
+ try {
149
+ candidates.push(JSON.parse(trimmed));
150
+ }
151
+ catch {
152
+ // not a standalone JSON line; the payload may still be a multi-line pretty-printed
153
+ // object, handled by the whole-stdout attempt below.
154
+ }
155
+ }
156
+ try {
157
+ candidates.push(JSON.parse(stdout));
158
+ }
159
+ catch {
160
+ // pretty-printed JSON mixed with other stdout; ignore.
161
+ }
162
+ const missionIdPattern = /^M-\d{8}-\d{6}-[a-z0-9]+$/i;
163
+ for (const parsed of candidates.reverse()) {
164
+ if (!parsed || typeof parsed !== 'object')
165
+ continue;
166
+ const direct = parsed.mission_id || parsed.missionId || parsed.mission?.id || parsed.completion_proof?.mission_id || parsed.decision?.mission_id;
167
+ if (typeof direct === 'string' && direct)
168
+ return direct;
169
+ if (typeof parsed.id === 'string' && missionIdPattern.test(parsed.id))
170
+ return parsed.id;
171
+ }
172
+ return null;
173
+ }
174
+ function inspectArtifact(root, artifact, latestMissionId) {
175
+ const file = resolveExpectedArtifactPath(root, artifact.path, { latestMissionId });
176
+ const exists = fs.existsSync(file);
177
+ const relPath = path.isAbsolute(artifact.path) ? artifact.path : artifact.path;
178
+ if (!exists) {
179
+ return { path: relPath, schema: artifact.schema, exists: false, ok: Boolean(artifact.optional), failure: artifact.optional ? undefined : 'missing' };
180
+ }
181
+ if (!file.endsWith('.json')) {
182
+ const content = fs.readFileSync(file, 'utf8');
183
+ return { path: relPath, schema: artifact.schema, exists: true, ok: content.trim().length > 0, failure: content.trim().length ? undefined : 'empty' };
184
+ }
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
188
+ }
189
+ catch {
190
+ return { path: relPath, schema: artifact.schema, exists: true, ok: false, failure: 'json_parse' };
191
+ }
192
+ if (!artifact.schema)
193
+ return { path: relPath, schema: null, exists: true, ok: true };
194
+ const schemaOk = parsed.schema === artifact.schema || parsed.schema_version != null;
195
+ return { path: relPath, schema: artifact.schema, exists: true, ok: schemaOk, failure: schemaOk ? undefined : 'schema_mismatch', actual_schema: parsed.schema || null };
196
+ }
197
+ function normalizeExpectedArtifacts(items = []) {
198
+ return (items || []).map((artifact) => {
199
+ if (typeof artifact === 'string')
200
+ return { path: artifact, schema: inferSchema(artifact) };
201
+ return { path: artifact.path, schema: artifact.schema || inferSchema(artifact.path), optional: Boolean(artifact.optional) };
202
+ });
203
+ }
204
+ function inferSchema(file = '') {
205
+ if (file.includes('completion-proof'))
206
+ return 'sks.completion-proof.v1';
207
+ if (file.includes('image-voxel-ledger'))
208
+ return 'sks.image-voxel-ledger.v1';
209
+ if (file.includes('visual-anchors'))
210
+ return 'sks.visual-anchors.v1';
211
+ return null;
212
+ }
213
+ function resolveSksEntrypoint(root) {
214
+ const candidates = [
215
+ path.join(root, 'dist', 'bin', 'sks.js'),
216
+ path.resolve('dist', 'bin', 'sks.js')
217
+ ];
218
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
219
+ return found ?? candidates[0] ?? path.resolve('dist', 'bin', 'sks.js');
220
+ }
221
+ /**
222
+ * Minimal, defensive command-line tokenizer: splits on whitespace while respecting
223
+ * single and double quotes. Never passed to a shell — used purely to build a spawnSync
224
+ * argv array, so shell metacharacters inside tokens are inert.
225
+ */
226
+ export function tokenizeCommand(command) {
227
+ const tokens = [];
228
+ let current = '';
229
+ let quote = null;
230
+ let hasToken = false;
231
+ for (let i = 0; i < command.length; i += 1) {
232
+ const ch = command[i] ?? '';
233
+ if (quote) {
234
+ if (ch === quote) {
235
+ quote = null;
236
+ }
237
+ else {
238
+ current += ch;
239
+ }
240
+ continue;
241
+ }
242
+ if (ch === '"' || ch === "'") {
243
+ quote = ch;
244
+ hasToken = true;
245
+ continue;
246
+ }
247
+ if (/\s/.test(ch)) {
248
+ if (hasToken) {
249
+ tokens.push(current);
250
+ current = '';
251
+ hasToken = false;
252
+ }
253
+ continue;
254
+ }
255
+ current += ch;
256
+ hasToken = true;
257
+ }
258
+ if (hasToken)
259
+ tokens.push(current);
260
+ return tokens;
261
+ }
262
+ //# sourceMappingURL=feature-fixture-executor.js.map