sneakoscope 4.8.7 → 5.1.2

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 (288) hide show
  1. package/README.md +51 -726
  2. package/bench/tasks/t01-off-by-one/repo/package.json +9 -0
  3. package/bench/tasks/t01-off-by-one/repo/src/pagination.js +5 -0
  4. package/bench/tasks/t01-off-by-one/repo/test.js +5 -0
  5. package/bench/tasks/t01-off-by-one/task.json +13 -0
  6. package/bench/tasks/t02-signature-cochange/repo/package.json +9 -0
  7. package/bench/tasks/t02-signature-cochange/repo/src/admin.js +5 -0
  8. package/bench/tasks/t02-signature-cochange/repo/src/audit.js +5 -0
  9. package/bench/tasks/t02-signature-cochange/repo/src/labels.js +3 -0
  10. package/bench/tasks/t02-signature-cochange/repo/src/profile.js +5 -0
  11. package/bench/tasks/t02-signature-cochange/repo/test.js +9 -0
  12. package/bench/tasks/t02-signature-cochange/task.json +12 -0
  13. package/bench/tasks/t03-type-puzzle/repo/package.json +9 -0
  14. package/bench/tasks/t03-type-puzzle/repo/src/config.js +4 -0
  15. package/bench/tasks/t03-type-puzzle/repo/test.js +6 -0
  16. package/bench/tasks/t03-type-puzzle/task.json +13 -0
  17. package/bench/tasks/t04-refactor-preserve/repo/package.json +9 -0
  18. package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +8 -0
  19. package/bench/tasks/t04-refactor-preserve/repo/test.js +4 -0
  20. package/bench/tasks/t04-refactor-preserve/task.json +12 -0
  21. package/bench/tasks/t05-performance/repo/package.json +9 -0
  22. package/bench/tasks/t05-performance/repo/src/pairs.js +9 -0
  23. package/bench/tasks/t05-performance/repo/test.js +11 -0
  24. package/bench/tasks/t05-performance/task.json +12 -0
  25. package/bench/tasks/t06-mistake-rule/repo/package.json +9 -0
  26. package/bench/tasks/t06-mistake-rule/repo/src/loader.js +6 -0
  27. package/bench/tasks/t06-mistake-rule/repo/test.js +7 -0
  28. package/bench/tasks/t06-mistake-rule/task.json +12 -0
  29. package/config/bench-baseline.json +7 -0
  30. package/crates/sks-core/Cargo.lock +1 -1
  31. package/crates/sks-core/Cargo.toml +1 -1
  32. package/crates/sks-core/src/main.rs +1 -1
  33. package/dist/bin/install.js +35 -0
  34. package/dist/bin/sks.js +1 -1
  35. package/dist/cli/cli-theme.js +51 -0
  36. package/dist/cli/command-registry.js +15 -4
  37. package/dist/cli/help-fast.js +18 -8
  38. package/dist/cli/insane-search-command.js +36 -8
  39. package/dist/cli/install-helpers.js +32 -0
  40. package/dist/commands/codex-lb.js +2 -0
  41. package/dist/commands/doctor.js +62 -6
  42. package/dist/commands/proof.js +6 -3
  43. package/dist/config/skills-manifest.json +82 -54
  44. package/dist/core/agents/agent-conflict-graph.js +5 -0
  45. package/dist/core/agents/agent-lease.js +5 -0
  46. package/dist/core/agents/agent-ledger-schemas.js +1 -0
  47. package/dist/core/agents/agent-orchestrator.js +268 -62
  48. package/dist/core/agents/agent-output-validator.js +14 -3
  49. package/dist/core/agents/agent-patch-queue-store.js +95 -2
  50. package/dist/core/agents/agent-patch-queue.js +6 -3
  51. package/dist/core/agents/agent-patch-schema.js +13 -0
  52. package/dist/core/agents/agent-proof-evidence.js +6 -1
  53. package/dist/core/agents/agent-runner-fake.js +40 -0
  54. package/dist/core/agents/agent-worker-pipeline.js +36 -2
  55. package/dist/core/agents/codex-exec-worker-adapter.js +2 -0
  56. package/dist/core/agents/native-worker-backend-router.js +58 -1
  57. package/dist/core/codex-app/sks-menubar.js +18 -5
  58. package/dist/core/codex-app.js +1 -0
  59. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  60. package/dist/core/codex-control/codex-sdk-env-policy.js +1 -1
  61. package/dist/core/codex-control/schemas/agent-worker-result.schema.js +12 -3
  62. package/dist/core/codex-hooks/codex-hook-managed-install.js +5 -0
  63. package/dist/core/codex-hooks/codex-hook-state-writer.js +70 -11
  64. package/dist/core/codex-hooks/codex-hook-trust-doctor.js +2 -2
  65. package/dist/core/codex-lb/codex-lb-env.js +20 -1
  66. package/dist/core/commands/basic-cli.js +21 -2
  67. package/dist/core/commands/check-command.js +37 -5
  68. package/dist/core/commands/computer-use-command.js +62 -7
  69. package/dist/core/commands/db-command.js +31 -5
  70. package/dist/core/commands/fast-mode-command.js +3 -0
  71. package/dist/core/commands/gate-result-contract.js +43 -0
  72. package/dist/core/commands/gates-command.js +6 -1
  73. package/dist/core/commands/gc-command.js +29 -2
  74. package/dist/core/commands/goal-command.js +9 -2
  75. package/dist/core/commands/gx-command.js +79 -7
  76. package/dist/core/commands/image-ux-review-command.js +188 -13
  77. package/dist/core/commands/mad-db-command.js +85 -176
  78. package/dist/core/commands/mad-sks-command.js +230 -29
  79. package/dist/core/commands/menubar-command.js +22 -1
  80. package/dist/core/commands/naruto-command.js +64 -10
  81. package/dist/core/commands/plan-command.js +76 -0
  82. package/dist/core/commands/ppt-command.js +159 -24
  83. package/dist/core/commands/qa-loop-command.js +6 -2
  84. package/dist/core/commands/release-command.js +55 -2
  85. package/dist/core/commands/research-command.js +1 -1
  86. package/dist/core/commands/review-command.js +217 -0
  87. package/dist/core/commands/route-success-helpers.js +59 -0
  88. package/dist/core/commands/run-command.js +19 -7
  89. package/dist/core/commands/seo-command.js +49 -1
  90. package/dist/core/commands/ui-command.js +161 -0
  91. package/dist/core/commands/uninstall-command.js +15 -1
  92. package/dist/core/db-safety.js +2 -2
  93. package/dist/core/doctor/doctor-transaction.js +8 -0
  94. package/dist/core/doctor/doctor-zellij-repair.js +1 -0
  95. package/dist/core/doctor/imagegen-repair.js +161 -0
  96. package/dist/core/feature-fixture-runner.js +2 -2
  97. package/dist/core/feature-fixtures.js +58 -37
  98. package/dist/core/feature-registry.js +102 -2
  99. package/dist/core/fsx.js +39 -6
  100. package/dist/core/image-ux-review/imagegen-adapter.js +48 -11
  101. package/dist/core/image-ux-review.js +16 -0
  102. package/dist/core/imagegen/imagegen-capability.js +11 -5
  103. package/dist/core/imagegen/require-imagegen.js +57 -0
  104. package/dist/core/init/skills.js +10 -5
  105. package/dist/core/mad-db/mad-db-coordinator.js +94 -18
  106. package/dist/core/mad-db/mad-db-policy.js +12 -10
  107. package/dist/core/mad-sks/executors/executor-base.js +8 -2
  108. package/dist/core/mad-sks/executors/index.js +4 -0
  109. package/dist/core/mad-sks/executors/sql-plane-executor.js +194 -0
  110. package/dist/core/naruto/naruto-active-pool.js +15 -4
  111. package/dist/core/naruto/naruto-real-worker-child.js +10 -0
  112. package/dist/core/naruto/naruto-role-policy.js +3 -0
  113. package/dist/core/naruto/naruto-task-hints.js +10 -0
  114. package/dist/core/naruto/naruto-work-graph.js +8 -2
  115. package/dist/core/naruto/naruto-work-item.js +6 -0
  116. package/dist/core/naruto/solution-tournament.js +101 -0
  117. package/dist/core/permission-gates.js +30 -0
  118. package/dist/core/pipeline-internals/runtime-core.js +11 -3
  119. package/dist/core/pipeline-internals/runtime-gates.js +27 -0
  120. package/dist/core/ppt-review/index.js +6 -1
  121. package/dist/core/ppt-review/slide-imagegen-review.js +75 -10
  122. package/dist/core/ppt.js +70 -2
  123. package/dist/core/proof/auto-finalize.js +70 -11
  124. package/dist/core/proof/proof-schema.js +2 -0
  125. package/dist/core/proof/route-adapter.js +5 -1
  126. package/dist/core/proof/route-finalizer.js +8 -5
  127. package/dist/core/proof/selftest-proof-fixtures.js +18 -5
  128. package/dist/core/proof/validation.js +2 -0
  129. package/dist/core/provider/model-router.js +53 -0
  130. package/dist/core/release/release-gate-node.js +3 -0
  131. package/dist/core/release/release-gate-resource-governor.js +1 -0
  132. package/dist/core/routes/constants.js +1 -1
  133. package/dist/core/routes.js +65 -18
  134. package/dist/core/safety/mutation-guard.js +5 -1
  135. package/dist/core/stop-gate/gate-evaluator.js +102 -0
  136. package/dist/core/stop-gate/stop-gate-check.js +16 -1
  137. package/dist/core/stop-gate/stop-gate-writer.js +4 -0
  138. package/dist/core/triwiki/agents-md-projector.js +184 -0
  139. package/dist/core/triwiki-wrongness/wrongness-schema.js +3 -1
  140. package/dist/core/trust-kernel/trust-kernel-schema.js +1 -0
  141. package/dist/core/trust-kernel/trust-report.js +1 -1
  142. package/dist/core/trust-kernel/trust-status.js +2 -0
  143. package/dist/core/ui/dashboard-html.js +111 -0
  144. package/dist/core/update/update-migration-state.js +366 -15
  145. package/dist/core/update-check.js +178 -81
  146. package/dist/core/verification/diff-quality.js +100 -0
  147. package/dist/core/verification/impact-scan.js +164 -0
  148. package/dist/core/verification/machine-feedback.js +146 -0
  149. package/dist/core/verification/mistake-rule-compiler.js +195 -0
  150. package/dist/core/version.js +1 -1
  151. package/dist/core/zellij/zellij-self-heal.js +9 -6
  152. package/dist/scripts/check-feature-quality.js +2 -2
  153. package/dist/scripts/cli-output-consistency-check.js +57 -0
  154. package/dist/scripts/coding-bench-check.js +136 -0
  155. package/dist/scripts/doctor-imagegen-repair-check.js +62 -0
  156. package/dist/scripts/ensure-bin-executable.js +11 -3
  157. package/dist/scripts/harness-benchmark-check.js +104 -0
  158. package/dist/scripts/legacy-update-e2e-check.js +228 -0
  159. package/dist/scripts/mad-db-command-check.js +11 -8
  160. package/dist/scripts/mad-db-route-identity-check.js +8 -6
  161. package/dist/scripts/mad-db-safety-conflict-matrix-check.js +31 -8
  162. package/dist/scripts/mad-db-skill-policy-snapshot-check.js +4 -5
  163. package/dist/scripts/ppt-real-imagegen-wiring-check.js +5 -1
  164. package/dist/scripts/proof-root-cause-policy-check.js +2 -0
  165. package/dist/scripts/release-dag-full-coverage-check.js +13 -4
  166. package/dist/scripts/release-gate-dag-runner.js +14 -0
  167. package/dist/scripts/release-gate-existence-audit.js +2 -1
  168. package/dist/scripts/release-gate-planner.js +2 -1
  169. package/dist/scripts/release-gate-script-parity-check.js +1 -1
  170. package/dist/scripts/release-metadata-1-19-check.js +2 -1
  171. package/dist/scripts/seo-geo-feature-fixture-quality-check.js +1 -1
  172. package/dist/scripts/sks-1-11-gate-lib.js +8 -6
  173. package/dist/scripts/ux-review-real-loop-fixture-check.js +9 -4
  174. package/dist/scripts/ux-review-run-wires-imagegen-check.js +4 -0
  175. package/docs/assets/sneakoscope-architecture-pipeline.jpg +0 -0
  176. package/docs/demo.tape +28 -0
  177. package/package.json +67 -3
  178. package/schemas/codex/completion-proof.schema.json +3 -2
  179. package/schemas/release/release-gate-node.schema.json +2 -1
  180. package/dist/scripts/agent-backfill-route-blackbox.js +0 -5
  181. package/dist/scripts/agent-dynamic-pool-route-blackbox.js +0 -5
  182. package/dist/scripts/agent-parallel-write-blackbox.js +0 -56
  183. package/dist/scripts/agent-patch-swarm-route-blackbox.js +0 -10
  184. package/dist/scripts/agent-route-blackbox-lib.js +0 -132
  185. package/dist/scripts/blackbox-command-import-smoke.js +0 -143
  186. package/dist/scripts/blackbox-global-shim.js +0 -77
  187. package/dist/scripts/blackbox-matrix.js +0 -70
  188. package/dist/scripts/blackbox-npx-one-shot.js +0 -69
  189. package/dist/scripts/blackbox-pack-install.js +0 -174
  190. package/dist/scripts/brand-neutrality-zero-leakage-blackbox.js +0 -4
  191. package/dist/scripts/build-once-runner-blackbox.js +0 -34
  192. package/dist/scripts/codex-0140-integration-blackbox.js +0 -13
  193. package/dist/scripts/codex-agent-type-blackbox.js +0 -4
  194. package/dist/scripts/codex-app-harness-blackbox.js +0 -4
  195. package/dist/scripts/codex-app-skill-agent-blackbox.js +0 -4
  196. package/dist/scripts/codex-hook-approval-blackbox.js +0 -4
  197. package/dist/scripts/codex-init-deep-directory-local-blackbox.js +0 -4
  198. package/dist/scripts/codex-native-feature-broker-blackbox.js +0 -4
  199. package/dist/scripts/codex-native-pattern-analysis-blackbox.js +0 -4
  200. package/dist/scripts/codex-native-read-repair-split-blackbox.js +0 -55
  201. package/dist/scripts/codex-native-reference-cache-blackbox.js +0 -41
  202. package/dist/scripts/core-skill-integrity-blackbox.js +0 -32
  203. package/dist/scripts/dfix-fast-blackbox-check.js +0 -37
  204. package/dist/scripts/dfix-parallel-write-blackbox.js +0 -48
  205. package/dist/scripts/dfix-patch-swarm-route-blackbox.js +0 -10
  206. package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +0 -16
  207. package/dist/scripts/doctor-dirty-repair-blackbox.js +0 -22
  208. package/dist/scripts/doctor-dirty-semantic-blackbox.js +0 -8
  209. package/dist/scripts/doctor-fix-production-blackbox.js +0 -26
  210. package/dist/scripts/doctor-native-capability-repair-blackbox.js +0 -39
  211. package/dist/scripts/doctor-startup-config-repair-blackbox.js +0 -13
  212. package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +0 -14
  213. package/dist/scripts/doctor-transaction-engine-blackbox.js +0 -28
  214. package/dist/scripts/doctor-zellij-fix-blackbox.js +0 -4
  215. package/dist/scripts/doctor-zellij-no-homebrew-blackbox.js +0 -4
  216. package/dist/scripts/doctor-zellij-upgrade-blackbox.js +0 -4
  217. package/dist/scripts/gate-pack-runner-blackbox.js +0 -27
  218. package/dist/scripts/gate-pack-v2-blackbox.js +0 -18
  219. package/dist/scripts/geo-cli-blackbox-check.js +0 -18
  220. package/dist/scripts/loop-collision-blackbox.js +0 -3
  221. package/dist/scripts/loop-concurrency-oversubscription-blackbox.js +0 -3
  222. package/dist/scripts/loop-fixture-production-misuse-blackbox.js +0 -3
  223. package/dist/scripts/loop-kill-interrupt-real-blackbox.js +0 -3
  224. package/dist/scripts/loop-merge-strategy-blackbox.js +0 -3
  225. package/dist/scripts/loop-mesh-production-e2e-blackbox.js +0 -3
  226. package/dist/scripts/loop-side-effect-blackbox.js +0 -3
  227. package/dist/scripts/mad-db-operation-lifecycle-blackbox.js +0 -29
  228. package/dist/scripts/mad-sks-actual-executor-blackbox.js +0 -5
  229. package/dist/scripts/mad-zellij-headless-fallback-blackbox.js +0 -4
  230. package/dist/scripts/mad-zellij-self-heal-blackbox.js +0 -4
  231. package/dist/scripts/naruto-loop-mesh-blackbox.js +0 -3
  232. package/dist/scripts/naruto-real-parallelism-blackbox.js +0 -307
  233. package/dist/scripts/naruto-worktree-coding-blackbox.js +0 -29
  234. package/dist/scripts/parallel-runtime-real-blackbox.js +0 -44
  235. package/dist/scripts/pipeline-codex-native-doctor-mad-routing-real-blackbox.js +0 -77
  236. package/dist/scripts/pipeline-codex-native-e2e-blackbox.js +0 -13
  237. package/dist/scripts/pipeline-codex-native-image-routing-real-blackbox.js +0 -50
  238. package/dist/scripts/pipeline-codex-native-loop-routing-real-blackbox.js +0 -74
  239. package/dist/scripts/pipeline-codex-native-qa-routing-real-blackbox.js +0 -51
  240. package/dist/scripts/pipeline-codex-native-research-routing-real-blackbox.js +0 -45
  241. package/dist/scripts/pipeline-execution-profile-routing-blackbox.js +0 -4
  242. package/dist/scripts/postinstall-global-doctor-blackbox.js +0 -12
  243. package/dist/scripts/ppt-full-e2e-blackbox-check.js +0 -109
  244. package/dist/scripts/ppt-imagegen-blackbox-check.js +0 -46
  245. package/dist/scripts/project-skill-dedupe-blackbox.js +0 -36
  246. package/dist/scripts/qa-backfill-route-blackbox.js +0 -5
  247. package/dist/scripts/qa-patch-swarm-route-blackbox.js +0 -10
  248. package/dist/scripts/release-full-parallelism-blackbox.js +0 -41
  249. package/dist/scripts/release-triwiki-first-runner-blackbox.js +0 -48
  250. package/dist/scripts/release-wiring-3110-blackbox.js +0 -27
  251. package/dist/scripts/release-wiring-3112-blackbox.js +0 -17
  252. package/dist/scripts/release-wiring-3113-blackbox.js +0 -17
  253. package/dist/scripts/research-backfill-route-blackbox.js +0 -5
  254. package/dist/scripts/research-final-reviewer-blackbox.js +0 -70
  255. package/dist/scripts/research-stage-cycle-runtime-blackbox.js +0 -40
  256. package/dist/scripts/research-synthesis-writer-blackbox.js +0 -24
  257. package/dist/scripts/route-blackbox-realism-check.js +0 -21
  258. package/dist/scripts/scheduler-resource-claim-blackbox.js +0 -24
  259. package/dist/scripts/seo-cli-blackbox-check.js +0 -18
  260. package/dist/scripts/sks-3110-all-feature-regression-blackbox.js +0 -116
  261. package/dist/scripts/sks-3112-all-feature-regression-blackbox.js +0 -29
  262. package/dist/scripts/sks-3113-all-feature-regression-blackbox.js +0 -17
  263. package/dist/scripts/sks-400-all-feature-regression-blackbox.js +0 -21
  264. package/dist/scripts/sks-400-extreme-parallel-blackbox.js +0 -8
  265. package/dist/scripts/sks-400-five-minute-blackbox.js +0 -9
  266. package/dist/scripts/sks-400-legacy-purge-blackbox.js +0 -8
  267. package/dist/scripts/sks-401-all-feature-regression-blackbox.js +0 -46
  268. package/dist/scripts/sks-401-five-minute-actual-blackbox.js +0 -23
  269. package/dist/scripts/sks-402-all-feature-regression-blackbox.js +0 -9
  270. package/dist/scripts/sks-402-five-minute-real-blackbox.js +0 -22
  271. package/dist/scripts/sksd-warm-cache-blackbox.js +0 -12
  272. package/dist/scripts/supabase-secret-preservation-blackbox.js +0 -29
  273. package/dist/scripts/team-backfill-route-blackbox.js +0 -5
  274. package/dist/scripts/team-parallel-write-blackbox.js +0 -55
  275. package/dist/scripts/team-patch-swarm-route-blackbox.js +0 -10
  276. package/dist/scripts/triwiki-affected-graph-blackbox.js +0 -28
  277. package/dist/scripts/triwiki-proof-bank-blackbox.js +0 -30
  278. package/dist/scripts/triwiki-proof-bank-lock-blackbox.js +0 -7
  279. package/dist/scripts/update-preserves-supabase-keys-blackbox.js +0 -27
  280. package/dist/scripts/ux-patch-swarm-route-blackbox.js +0 -10
  281. package/dist/scripts/ux-review-imagegen-blackbox-check.js +0 -67
  282. package/dist/scripts/zellij-initial-main-only-blackbox.js +0 -28
  283. package/dist/scripts/zellij-pane-lock-concurrency-blackbox.js +0 -80
  284. package/dist/scripts/zellij-pane-lock-open-worker-integration-blackbox.js +0 -137
  285. package/dist/scripts/zellij-self-heal-typed-blackbox.js +0 -4
  286. package/dist/scripts/zellij-slot-telemetry-real-blackbox.js +0 -20
  287. package/dist/scripts/zellij-stacked-fallback-integration-blackbox.js +0 -81
  288. package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +0 -202
@@ -1,11 +1,14 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { PACKAGE_VERSION, packageRoot, readJson, runProcess, which } from './fsx.js';
3
+ import { PACKAGE_VERSION, packageRoot, readJson, runProcess, throttleLines, which } from './fsx.js';
4
4
  import { createRequestedScopeContract } from './safety/requested-scope-contract.js';
5
5
  import { guardedPackageInstall, guardContextForRoute } from './safety/mutation-guard.js';
6
6
  import { isUpdateMigrationReceiptCurrent, resolveInstalledSksEntrypoint, runPackageLocalDoctor, writeProjectUpdateMigrationReceipt } from './update/update-migration-state.js';
7
7
  import { installSksMenuBar } from './codex-app/sks-menubar.js';
8
8
  import { reconcileSkills } from './init/skills.js';
9
+ import { codexHookTrustDoctor } from './codex-hooks/codex-hook-trust-doctor.js';
10
+ import { readCodexHookActualState } from './codex-hooks/codex-hook-actual-discovery.js';
11
+ import { ui as cliUi, withHeartbeat } from '../cli/cli-theme.js';
9
12
  const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
10
13
  export async function runSksUpdateCheck(options = {}) {
11
14
  const packageName = options.packageName || 'sneakoscope';
@@ -102,8 +105,16 @@ export async function runSksUpdateNow(options = {}) {
102
105
  const globalRoot = npmBin ? await detectNpmGlobalRoot(npmBin, env, options).catch(() => null) : null;
103
106
  const projectReceiptRoot = path.resolve(options.projectRoot || env.SKS_MUTATION_LEDGER_ROOT || process.cwd());
104
107
  const stages = [];
108
+ const quiet = options.quiet === true || /^(1|true)$/i.test(String(env.SKS_UPDATE_QUIET || ''));
109
+ const machineOutput = quiet || options.json === true;
110
+ const stageStart = (id, status) => {
111
+ if (!machineOutput)
112
+ cliUi.step(`▸ ${id} - ${status}`);
113
+ };
105
114
  const stage = (id, ok, status, detail = {}) => {
106
115
  stages.push({ id, ok, status, detail });
116
+ if (!machineOutput)
117
+ cliUi.step(`${ok ? '✔' : '✖'} ${id} - ${status}`);
107
118
  };
108
119
  if (!npmBin) {
109
120
  return buildUpdateNowResult({
@@ -157,45 +168,6 @@ export async function runSksUpdateNow(options = {}) {
157
168
  error: check.error || 'latest version unavailable'
158
169
  });
159
170
  }
160
- if (!requestedVersion && check.latest && !check.update_available) {
161
- const receipt = await writeProjectUpdateMigrationReceipt({
162
- root: projectReceiptRoot,
163
- source: 'update-now-current',
164
- blockers: [],
165
- warnings: ['package_already_current']
166
- }).catch(() => null);
167
- const migrationCurrent = isUpdateMigrationReceiptCurrent(receipt);
168
- stage('project_receipt', migrationCurrent, migrationCurrent ? 'current' : 'failed', { root: projectReceiptRoot });
169
- const sksMenuBar = migrationCurrent
170
- ? await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage })
171
- : null;
172
- await runUpdateGlobalSkillsReconcile(stage);
173
- return buildUpdateNowResult({
174
- packageName,
175
- from: check.current,
176
- latest: check.latest,
177
- requestedVersion,
178
- installVersion,
179
- npmBin,
180
- npmArgs,
181
- command,
182
- cwd,
183
- registry,
184
- globalRoot,
185
- status: 'current',
186
- ok: migrationCurrent,
187
- installCode: null,
188
- oldVersionDoctor: null,
189
- newBinary: null,
190
- newVersion: check.current,
191
- newVersionDoctor: null,
192
- projectReceipt: receipt,
193
- migrationCurrent,
194
- sksMenuBar,
195
- stages,
196
- error: null
197
- });
198
- }
199
171
  if (options.dryRun) {
200
172
  stage('old_version_doctor_preflight', true, 'skipped_dry_run', { reason: 'dry_run_does_not_run_doctor_fix' });
201
173
  stage('npm_install', true, 'dry_run', { command });
@@ -224,19 +196,20 @@ export async function runSksUpdateNow(options = {}) {
224
196
  error: null
225
197
  });
226
198
  }
227
- const oldDoctorTimeoutOverride = Number.parseInt(env.SKS_UPDATE_OLD_DOCTOR_TIMEOUT_MS || '', 10);
228
- const oldDoctorTimeoutMs = Number.isFinite(oldDoctorTimeoutOverride) && oldDoctorTimeoutOverride > 0
229
- ? oldDoctorTimeoutOverride
230
- : 60_000;
231
- const oldVersionDoctor = await runPackageLocalDoctor({
232
- root: projectReceiptRoot,
233
- args: ['doctor', '--fix', '--yes', '--profile', 'migration', '--machine-only', '--report-file', path.join(projectReceiptRoot, '.sneakoscope', 'update', 'old-version-doctor.json')],
234
- env,
235
- timeoutMs: oldDoctorTimeoutMs,
236
- maxOutputBytes: 32 * 1024
237
- });
238
- stage('old_version_doctor_preflight', oldVersionDoctor.ok, oldVersionDoctor.status, { entrypoint: oldVersionDoctor.entrypoint, exit_code: oldVersionDoctor.exit_code, timeout_ms: oldDoctorTimeoutMs });
239
- if (!oldVersionDoctor.ok && env.SKS_UPDATE_SKIP_OLD_DOCTOR_PREFLIGHT !== '1') {
199
+ if (!requestedVersion && check.latest && !check.update_available) {
200
+ const receipt = await writeProjectUpdateMigrationReceipt({
201
+ root: projectReceiptRoot,
202
+ source: 'update-now-current',
203
+ fromVersion: check.current,
204
+ blockers: [],
205
+ warnings: ['package_already_current']
206
+ }).catch(() => null);
207
+ const migrationCurrent = isUpdateMigrationReceiptCurrent(receipt);
208
+ stage('project_receipt', migrationCurrent, migrationCurrent ? 'current' : 'failed', { root: projectReceiptRoot });
209
+ const sksMenuBar = migrationCurrent
210
+ ? await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage, quiet: machineOutput })
211
+ : null;
212
+ await runUpdateGlobalSkillsReconcile(stage, { quiet: machineOutput });
240
213
  return buildUpdateNowResult({
241
214
  packageName,
242
215
  from: check.current,
@@ -249,17 +222,46 @@ export async function runSksUpdateNow(options = {}) {
249
222
  cwd,
250
223
  registry,
251
224
  globalRoot,
252
- status: 'failed',
253
- ok: false,
225
+ status: 'current',
226
+ ok: migrationCurrent,
254
227
  installCode: null,
255
- oldVersionDoctor,
228
+ oldVersionDoctor: null,
256
229
  newBinary: null,
257
- newVersion: null,
230
+ newVersion: check.current,
258
231
  newVersionDoctor: null,
259
- projectReceipt: null,
260
- migrationCurrent: false,
232
+ projectReceipt: receipt,
233
+ migrationCurrent,
234
+ sksMenuBar,
261
235
  stages,
262
- error: oldVersionDoctor.error || 'old-version Doctor preflight failed'
236
+ error: null
237
+ });
238
+ }
239
+ const oldDoctorTimeoutOverride = Number.parseInt(env.SKS_UPDATE_OLD_DOCTOR_TIMEOUT_MS || '', 10);
240
+ const oldDoctorTimeoutMs = Number.isFinite(oldDoctorTimeoutOverride) && oldDoctorTimeoutOverride > 0
241
+ ? oldDoctorTimeoutOverride
242
+ : 60_000;
243
+ let oldVersionDoctor = null;
244
+ if (env.SKS_UPDATE_SKIP_OLD_DOCTOR_PREFLIGHT === '1') {
245
+ stage('old_version_doctor_preflight', true, 'skipped', { reason: 'SKS_UPDATE_SKIP_OLD_DOCTOR_PREFLIGHT=1' });
246
+ }
247
+ else {
248
+ stageStart('old_version_doctor_preflight', 'running migration doctor on current install');
249
+ oldVersionDoctor = await updateHeartbeat(machineOutput, 'old-version doctor', runPackageLocalDoctor({
250
+ root: projectReceiptRoot,
251
+ args: ['doctor', '--fix', '--yes', '--profile', 'migration', '--machine-only', '--report-file', path.join(projectReceiptRoot, '.sneakoscope', 'update', 'old-version-doctor.json')],
252
+ env: {
253
+ ...env,
254
+ ...(env.SKS_TEST_OLD_DOCTOR_FAIL === '1' ? { SKS_TEST_DOCTOR_FAIL: '1' } : {})
255
+ },
256
+ timeoutMs: oldDoctorTimeoutMs,
257
+ maxOutputBytes: 32 * 1024
258
+ }), 60_000);
259
+ stage('old_version_doctor_preflight', oldVersionDoctor.ok, oldVersionDoctor.ok ? oldVersionDoctor.status : 'failed_continuing', {
260
+ entrypoint: oldVersionDoctor.entrypoint,
261
+ exit_code: oldVersionDoctor.exit_code,
262
+ timeout_ms: oldDoctorTimeoutMs,
263
+ timed_out: oldVersionDoctor.timedOut,
264
+ note: oldVersionDoctor.ok ? null : 'legacy doctor unreliable; new-version doctor will repair after install'
263
265
  });
264
266
  }
265
267
  const mutationLedgerRoot = env.SKS_MUTATION_LEDGER_ROOT || packageRoot();
@@ -269,7 +271,10 @@ export async function runSksUpdateNow(options = {}) {
269
271
  projectRoot: mutationLedgerRoot,
270
272
  overrides: { package_install: true }
271
273
  });
272
- const install = await guardedPackageInstall(guardContextForRoute(mutationLedgerRoot, installContract, command || `npm global install ${packageName}`), `${packageName}@${installVersion}`, {
274
+ const npmStdout = machineOutput ? undefined : throttleLines((line) => process.stderr.write(` npm | ${line}\n`), 500);
275
+ const npmStderr = machineOutput ? undefined : throttleLines((line) => process.stderr.write(` npm ! ${line}\n`), 500);
276
+ stageStart('npm_global_install', command || `npm global install ${packageName}`);
277
+ const installOptions = {
273
278
  confirmed: true,
274
279
  command: npmBin,
275
280
  args: npmArgs,
@@ -277,21 +282,32 @@ export async function runSksUpdateNow(options = {}) {
277
282
  env,
278
283
  timeoutMs: options.timeoutMs ?? 10 * 60 * 1000,
279
284
  maxOutputBytes: options.maxOutputBytes ?? 128 * 1024
280
- }).catch((err) => ({
281
- code: 1,
282
- stdout: '',
283
- stderr: err instanceof Error ? err.message : String(err)
284
- }));
285
+ };
286
+ if (npmStdout)
287
+ installOptions.onStdout = npmStdout;
288
+ if (npmStderr)
289
+ installOptions.onStderr = npmStderr;
290
+ const install = env.SKS_UPDATE_FAKE_INSTALL === '1'
291
+ ? { code: 0, stdout: 'fake install ok', stderr: '', timedOut: false }
292
+ : await updateHeartbeat(machineOutput, `npm install -g ${packageName}`, guardedPackageInstall(guardContextForRoute(mutationLedgerRoot, installContract, command || `npm global install ${packageName}`), `${packageName}@${installVersion}`, installOptions), 60_000).catch((err) => ({
293
+ code: 1,
294
+ stdout: '',
295
+ stderr: err instanceof Error ? err.message : String(err),
296
+ timedOut: false
297
+ }));
285
298
  const installOk = install.code === 0;
286
- stage('npm_global_install', installOk, installOk ? 'installed' : 'failed', { command, code: install.code });
299
+ stage('npm_global_install', installOk, installOk ? env.SKS_UPDATE_FAKE_INSTALL === '1' ? 'fake_installed' : 'installed' : 'failed', { command, code: install.code });
287
300
  let newBinary = null;
288
301
  let newVersion = null;
289
302
  let newVersionDoctor = null;
290
303
  let projectReceipt = null;
291
304
  let migrationCurrent = false;
292
305
  let sksMenuBar = null;
306
+ let hookTrust = null;
293
307
  if (installOk) {
294
- newBinary = await resolveInstalledSksEntrypoint({ packageName, globalRoot, env });
308
+ newBinary = env.SKS_UPDATE_FAKE_INSTALL === '1'
309
+ ? path.join(packageRoot(), 'dist', 'bin', 'sks.js')
310
+ : await resolveInstalledSksEntrypoint({ packageName, globalRoot, env });
295
311
  stage('resolve_new_package_local_binary', Boolean(newBinary), newBinary ? 'resolved' : 'missing', { new_binary: newBinary });
296
312
  if (newBinary) {
297
313
  const versionProbe = await runProcess(process.execPath, [newBinary, '--version'], {
@@ -302,33 +318,52 @@ export async function runSksUpdateNow(options = {}) {
302
318
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
303
319
  newVersion = parseVersionText(versionProbe.stdout || versionProbe.stderr || '') || null;
304
320
  stage('new_version_probe', Boolean(newVersion), newVersion ? 'version_detected' : 'failed', { new_version: newVersion, code: versionProbe.code });
305
- newVersionDoctor = await runPackageLocalDoctor({
321
+ stageStart('new_version_global_doctor', 'running migration doctor on updated install');
322
+ newVersionDoctor = await updateHeartbeat(machineOutput, 'new-version doctor', runPackageLocalDoctor({
306
323
  root: globalSksRootPath(),
307
324
  entrypoint: newBinary,
308
325
  args: ['doctor', '--fix', '--yes', '--profile', 'migration', '--machine-only', '--report-file', path.join(globalSksRootPath(), 'update', 'new-version-doctor.json')],
309
326
  env,
310
- timeoutMs: 15_000,
327
+ timeoutMs: updateDoctorTimeoutMs(env),
311
328
  maxOutputBytes: 32 * 1024
312
- });
313
- stage('new_version_global_doctor', newVersionDoctor.ok, newVersionDoctor.status, { entrypoint: newBinary, exit_code: newVersionDoctor.exit_code });
329
+ }), 60_000);
330
+ stage('new_version_global_doctor', newVersionDoctor.ok, newVersionDoctor.status, { entrypoint: newBinary, exit_code: newVersionDoctor.exit_code, timeout_ms: updateDoctorTimeoutMs(env), timed_out: newVersionDoctor.timedOut });
314
331
  }
315
332
  if (newVersionDoctor?.ok) {
333
+ hookTrust = await codexHookTrustDoctor(projectReceiptRoot, { fix: true, managed: true, actual: true })
334
+ .catch((err) => ({ ok: false, blockers: [`hook_trust_repair_failed:${err?.message || err}`] }));
335
+ stage('hook_trust_repair', hookTrust?.ok !== false, hookTrust?.ok !== false ? 'repaired' : 'failed', {
336
+ entries: hookTrust?.current_hash_count ?? null,
337
+ blockers: hookTrust?.blockers || []
338
+ });
339
+ }
340
+ if (newVersionDoctor?.ok && hookTrust?.ok !== false) {
316
341
  projectReceipt = await writeProjectUpdateMigrationReceipt({
317
342
  root: projectReceiptRoot,
318
343
  source: 'update-now',
319
344
  doctor: newVersionDoctor,
320
345
  updateStages: stages,
346
+ fromVersion: check.current,
321
347
  blockers: [],
322
348
  warnings: []
323
349
  }).catch(() => null);
324
350
  migrationCurrent = isUpdateMigrationReceiptCurrent(projectReceipt);
325
351
  stage('project_receipt', migrationCurrent, migrationCurrent ? 'current' : 'failed', { root: projectReceiptRoot });
326
352
  if (migrationCurrent)
327
- sksMenuBar = await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage });
328
- await runUpdateGlobalSkillsReconcile(stage);
353
+ sksMenuBar = await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage, quiet: machineOutput });
354
+ await runUpdateGlobalSkillsReconcile(stage, { quiet: machineOutput });
329
355
  }
330
356
  }
331
- const ok = installOk && Boolean(newBinary) && newVersionDoctor?.ok === true && migrationCurrent;
357
+ const verification = await runFinalUpdateVerification({ installOk, newBinary, installVersion, env, projectReceiptRoot });
358
+ const verifyOk = verification.length > 0 && verification.every((item) => item.ok);
359
+ if (verification.length) {
360
+ stage('final_self_verification', verifyOk, verifyOk ? 'verified' : 'issues', {
361
+ failed: verification.filter((item) => !item.ok).map((item) => item.id)
362
+ });
363
+ }
364
+ const baseOk = installOk && Boolean(newBinary) && newVersionDoctor?.ok === true && hookTrust?.ok !== false && migrationCurrent;
365
+ const ok = baseOk && verifyOk;
366
+ const status = ok ? 'updated' : baseOk ? 'updated_with_issues' : 'failed';
332
367
  return buildUpdateNowResult({
333
368
  packageName,
334
369
  from: check.current,
@@ -341,7 +376,7 @@ export async function runSksUpdateNow(options = {}) {
341
376
  cwd,
342
377
  registry,
343
378
  globalRoot,
344
- status: ok ? 'updated' : 'failed',
379
+ status,
345
380
  ok,
346
381
  installCode: install.code,
347
382
  oldVersionDoctor,
@@ -352,15 +387,17 @@ export async function runSksUpdateNow(options = {}) {
352
387
  migrationCurrent,
353
388
  sksMenuBar,
354
389
  stages,
355
- error: ok ? null : updateNowError(install, newBinary, newVersionDoctor, migrationCurrent)
390
+ verification,
391
+ error: ok ? null : status === 'updated_with_issues' ? verificationError(verification) : updateNowError(install, newBinary, newVersionDoctor, migrationCurrent)
356
392
  });
357
393
  }
358
- async function runUpdateGlobalSkillsReconcile(stage) {
359
- const result = await reconcileSkills({
394
+ async function runUpdateGlobalSkillsReconcile(stage, opts = {}) {
395
+ const work = reconcileSkills({
360
396
  targetDir: path.join(os.homedir(), '.agents', 'skills'),
361
397
  scope: 'global',
362
398
  fix: true
363
399
  }).catch((err) => ({ schema: 'sks.skill-reconcile.v1', ok: false, error: err?.message || String(err) }));
400
+ const result = opts.quiet ? await work : await withHeartbeat('skills reconcile', work, { warnAfterMs: 30_000 });
364
401
  const ok = result.ok !== false && !result.error;
365
402
  stage('global_skills_reconcile', ok, ok ? 'reconciled' : 'failed', {
366
403
  installed: Array.isArray(result.installed) ? result.installed.length : null,
@@ -534,6 +571,7 @@ function buildUpdateNowResult(input) {
534
571
  migration_current: input.migrationCurrent,
535
572
  sks_menubar: input.sksMenuBar || null,
536
573
  stages: input.stages,
574
+ verification: input.verification || [],
537
575
  error: input.error
538
576
  };
539
577
  }
@@ -542,11 +580,12 @@ async function installUpdateSksMenuBar(input) {
542
580
  input.stage('sks_menubar', true, 'skipped', { reason: 'SKS_UPDATE_SKIP_SKS_MENUBAR=1' });
543
581
  return null;
544
582
  }
545
- const result = await installSksMenuBar({
583
+ const work = installSksMenuBar({
546
584
  root: input.root,
547
585
  apply: true,
548
586
  launch: true,
549
- env: input.env
587
+ env: input.env,
588
+ quiet: input.quiet === true
550
589
  }).catch((err) => ({
551
590
  schema: 'sks.codex-app-sks-menubar.v1',
552
591
  ok: false,
@@ -572,6 +611,7 @@ async function installUpdateSksMenuBar(input) {
572
611
  blockers: [err?.message || String(err)],
573
612
  warnings: []
574
613
  }));
614
+ const result = input.quiet ? await work : await withHeartbeat('SKS menu bar install', work, { warnAfterMs: 30_000 });
575
615
  input.stage('sks_menubar', result.ok !== false, result.status, {
576
616
  app_path: result.app_path,
577
617
  launch_agent_path: result.launch_agent_path,
@@ -597,6 +637,63 @@ function parseVersionText(text) {
597
637
  function globalSksRootPath() {
598
638
  return path.join(process.env.HOME || os.homedir(), '.sneakoscope-global');
599
639
  }
640
+ function updateDoctorTimeoutMs(env) {
641
+ const override = Number.parseInt(env.SKS_UPDATE_NEW_DOCTOR_TIMEOUT_MS || env.SKS_MIGRATION_DOCTOR_TIMEOUT_MS || '', 10);
642
+ return Number.isFinite(override) && override > 0 ? override : 180_000;
643
+ }
644
+ async function updateHeartbeat(quiet, label, work, warnAfterMs = 60_000) {
645
+ return quiet ? work : withHeartbeat(label, work, { warnAfterMs });
646
+ }
647
+ async function runFinalUpdateVerification(input) {
648
+ if (!input.installOk || !input.newBinary || !input.installVersion)
649
+ return [];
650
+ const verification = [];
651
+ const versionProbe = await runProcess(process.execPath, [input.newBinary, '--version'], {
652
+ timeoutMs: 5000,
653
+ maxOutputBytes: 4096,
654
+ env: { ...input.env, SKS_UPDATE_MIGRATION_GATE_DISABLED: '1', SKS_DISABLE_UPDATE_CHECK: '1' }
655
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
656
+ const got = parseVersionText(`${versionProbe.stdout || ''}\n${versionProbe.stderr || ''}`);
657
+ verification.push({
658
+ id: 'version_match',
659
+ ok: got === input.installVersion,
660
+ detail: `expected ${input.installVersion}, got ${got || 'missing'}`,
661
+ remediation: 'Run: sks update now --version <expected>'
662
+ });
663
+ const hookState = await readCodexHookActualState(input.projectReceiptRoot).catch(() => null);
664
+ const managedEntries = (hookState?.entries || []).filter((entry) => entry.managed === true);
665
+ const untrusted = managedEntries.filter((entry) => entry.trust_status !== 'Trusted' && entry.trust_status !== 'Managed');
666
+ verification.push({
667
+ id: 'hooks_trusted',
668
+ ok: Boolean(hookState && hookState.ok !== false && managedEntries.length > 0 && untrusted.length === 0),
669
+ detail: untrusted.length ? untrusted.map((entry) => entry.key).slice(0, 3).join(', ') : `managed ${managedEntries.length}`,
670
+ remediation: 'Run: sks codex trust-doctor --fix --managed --actual'
671
+ });
672
+ const stampPath = path.join(path.dirname(input.newBinary), '..', '.sks-build-stamp.json');
673
+ const stamp = await readJson(stampPath, null).catch(() => null);
674
+ const stampVersion = stamp?.package_version || stamp?.version || null;
675
+ verification.push({
676
+ id: 'dist_stamp',
677
+ ok: stampVersion === input.installVersion,
678
+ detail: `expected ${input.installVersion}, got ${stampVersion || 'missing'}`,
679
+ remediation: 'Run: npm run build:incremental'
680
+ });
681
+ const home = input.env.HOME || os.homedir();
682
+ const skillsManifest = await readJson(path.join(home, '.agents', 'skills', '.sks-generated.json'), null).catch(() => null);
683
+ verification.push({
684
+ id: 'skills_manifest',
685
+ ok: skillsManifest?.version === input.installVersion,
686
+ detail: `expected ${input.installVersion}, got ${skillsManifest?.version || 'missing'}`,
687
+ remediation: 'Run: sks doctor --fix --yes'
688
+ });
689
+ return verification;
690
+ }
691
+ function verificationError(verification) {
692
+ const failed = verification.filter((item) => !item.ok);
693
+ return failed.length
694
+ ? `update self-verification failed: ${failed.map((item) => item.id).join(', ')}`
695
+ : 'update self-verification did not run';
696
+ }
600
697
  function updateNowError(install, newBinary, newVersionDoctor, migrationCurrent) {
601
698
  if (install.code !== 0)
602
699
  return `${install.stderr || install.stdout || 'npm global install failed'}`.trim();
@@ -0,0 +1,100 @@
1
+ import path from 'node:path';
2
+ import { findReferences, pickScanTool } from './impact-scan.js';
3
+ const ADDED_EXPORT_RE = /^\+\s*export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+([A-Za-z_$][\w$]*)\b/;
4
+ export async function analyzeDiffQuality(input) {
5
+ const planFiles = new Set((input.plannedFiles || []).map(normalizePath).filter(Boolean));
6
+ const touchedFiles = new Set(input.changedFiles.map(normalizePath).filter(Boolean));
7
+ const ratio = touchedFiles.size / Math.max(1, planFiles.size || touchedFiles.size || 1);
8
+ const deadAdditions = await detectDeadAdditions(input.root, input.patchText, input.changedFiles);
9
+ const commentNoise = detectCommentNoise(input.patchText);
10
+ const guardBloat = detectGuardBloat(input.patchText);
11
+ const warnings = [
12
+ ...(ratio > 2 ? [`minimality_ratio:${ratio.toFixed(2)}`] : []),
13
+ ...(commentNoise > 0 ? [`comment_noise:${commentNoise}`] : []),
14
+ ...(guardBloat > 0 ? [`guard_bloat:${guardBloat}`] : [])
15
+ ];
16
+ const errors = deadAdditions.map((name) => `dead_addition:${name}`);
17
+ return {
18
+ schema: 'sks.diff-quality.v1',
19
+ minimality: {
20
+ plan_files: planFiles.size,
21
+ touched_files: touchedFiles.size,
22
+ ratio: Number(ratio.toFixed(3))
23
+ },
24
+ dead_additions: deadAdditions,
25
+ comment_noise: commentNoise,
26
+ guard_bloat: guardBloat,
27
+ warnings,
28
+ errors
29
+ };
30
+ }
31
+ function addedExports(patchText, changedFiles) {
32
+ const out = [];
33
+ let currentFile = normalizePath(changedFiles[0] || '');
34
+ for (const line of String(patchText || '').split(/\r?\n/)) {
35
+ if (line.startsWith('+++ b/')) {
36
+ currentFile = normalizePath(line.slice('+++ b/'.length));
37
+ continue;
38
+ }
39
+ const match = line.match(ADDED_EXPORT_RE);
40
+ if (match?.[1])
41
+ out.push({ name: match[1], file: currentFile });
42
+ }
43
+ const seen = new Set();
44
+ return out.filter((row) => {
45
+ const key = `${row.file}:${row.name}`;
46
+ if (seen.has(key))
47
+ return false;
48
+ seen.add(key);
49
+ return true;
50
+ });
51
+ }
52
+ async function detectDeadAdditions(root, patchText, changedFiles) {
53
+ const additions = addedExports(patchText, changedFiles);
54
+ if (!additions.length)
55
+ return [];
56
+ const tool = await pickScanTool();
57
+ const dead = [];
58
+ for (const addition of additions) {
59
+ const refs = await findReferences(root, addition.name, tool, { excludeFile: addition.file });
60
+ if (!refs.length)
61
+ dead.push(addition.name);
62
+ }
63
+ return dead;
64
+ }
65
+ function detectCommentNoise(patchText) {
66
+ const added = String(patchText || '').split(/\r?\n/).filter((line) => line.startsWith('+') && !line.startsWith('+++'));
67
+ let count = 0;
68
+ for (let i = 0; i < added.length - 1; i += 1) {
69
+ const comment = added[i]?.replace(/^\+\s*/, '') || '';
70
+ const code = added[i + 1]?.replace(/^\+\s*/, '') || '';
71
+ if (!/^(\/\/|\/\*|\*)/.test(comment.trim()))
72
+ continue;
73
+ if (tokenOverlap(comment, code) >= 0.7)
74
+ count += 1;
75
+ }
76
+ return count;
77
+ }
78
+ function detectGuardBloat(patchText) {
79
+ const text = String(patchText || '').split(/\r?\n/).filter((line) => line.startsWith('+') && !line.startsWith('+++')).map((line) => line.slice(1)).join('\n');
80
+ const matches = text.match(/catch\s*\([^)]*\)\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}/g);
81
+ return matches?.length || 0;
82
+ }
83
+ function tokenOverlap(a, b) {
84
+ const left = tokens(a);
85
+ const right = tokens(b);
86
+ if (!left.size || !right.size)
87
+ return 0;
88
+ let overlap = 0;
89
+ for (const token of left)
90
+ if (right.has(token))
91
+ overlap += 1;
92
+ return overlap / Math.max(1, Math.min(left.size, right.size));
93
+ }
94
+ function tokens(value) {
95
+ return new Set(String(value || '').toLowerCase().match(/[a-z0-9_]{3,}/g) || []);
96
+ }
97
+ function normalizePath(value) {
98
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
99
+ }
100
+ //# sourceMappingURL=diff-quality.js.map
@@ -0,0 +1,164 @@
1
+ import path from 'node:path';
2
+ import { listFilesRecursive, readText, runProcess, which } from '../fsx.js';
3
+ const DECL_RE = /^\s*export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+([A-Za-z_$][\w$]*)\b/;
4
+ const TEXT_EXT_RE = /\.(?:[cm]?[jt]sx?|json|md|css|scss|html|yml|yaml|toml)$/i;
5
+ export async function scanImpact(root, changedFiles, patchText) {
6
+ const symbols = extractChangedExportedSymbols(patchText, changedFiles);
7
+ const tool = await pickScanTool();
8
+ const references = [];
9
+ for (const sym of symbols) {
10
+ references.push(...await findReferences(root, sym.name, tool, { excludeFile: sym.file }));
11
+ }
12
+ const patchFiles = new Set(changedFiles.map(normalizePath));
13
+ const cochange = [...new Set(references.map((ref) => ref.file))]
14
+ .filter((file) => !patchFiles.has(normalizePath(file)));
15
+ return {
16
+ schema: 'sks.impact-scan.v1',
17
+ changed_symbols: symbols,
18
+ references,
19
+ cochange_required: cochange,
20
+ tool
21
+ };
22
+ }
23
+ export function extractChangedExportedSymbols(patchText, changedFiles) {
24
+ const symbols = [];
25
+ let currentFile = normalizePath(changedFiles[0] || '');
26
+ for (const rawLine of String(patchText || '').split(/\r?\n/)) {
27
+ if (rawLine.startsWith('+++ b/')) {
28
+ currentFile = normalizePath(rawLine.slice('+++ b/'.length));
29
+ continue;
30
+ }
31
+ if (rawLine.startsWith('--- ') || !rawLine.startsWith('-'))
32
+ continue;
33
+ const line = rawLine.slice(1);
34
+ const match = line.match(DECL_RE);
35
+ if (!match?.[1])
36
+ continue;
37
+ symbols.push({ name: match[1], file: currentFile, kind: 'export' });
38
+ }
39
+ return dedupeSymbols(symbols);
40
+ }
41
+ export async function pickScanTool() {
42
+ if (await which('ast-grep'))
43
+ return 'ast-grep';
44
+ if (await which('rg'))
45
+ return 'ripgrep';
46
+ return 'builtin';
47
+ }
48
+ export async function findReferences(root, symbol, tool, opts = {}) {
49
+ const normalizedExclude = normalizePath(opts.excludeFile || '');
50
+ if (!symbol || !/^[A-Za-z_$][\w$]*$/.test(symbol))
51
+ return [];
52
+ if (tool === 'ast-grep') {
53
+ const refs = await astGrepReferences(root, symbol, normalizedExclude);
54
+ if (refs.length)
55
+ return refs;
56
+ }
57
+ if (tool === 'ast-grep' || tool === 'ripgrep') {
58
+ const refs = await ripgrepReferences(root, symbol, normalizedExclude);
59
+ if (refs.length || tool === 'ripgrep')
60
+ return refs;
61
+ }
62
+ return builtinReferences(root, symbol, normalizedExclude);
63
+ }
64
+ async function astGrepReferences(root, symbol, excludeFile) {
65
+ const result = await runProcess('ast-grep', ['run', '-p', symbol, '--json', root], {
66
+ cwd: root,
67
+ timeoutMs: 15_000,
68
+ maxOutputBytes: 512 * 1024
69
+ }).catch(() => null);
70
+ if (!result || result.code !== 0 || !result.stdout.trim())
71
+ return [];
72
+ const refs = [];
73
+ try {
74
+ const rows = JSON.parse(result.stdout);
75
+ for (const row of Array.isArray(rows) ? rows : []) {
76
+ const file = normalizePath(path.relative(root, String(row.file || row.path || '')));
77
+ if (!file || file === excludeFile || shouldIgnorePath(file))
78
+ continue;
79
+ refs.push({
80
+ symbol,
81
+ file,
82
+ line: Number(row.range?.start?.line || row.line || 1),
83
+ text: String(row.text || row.lines || '').trim().slice(0, 240)
84
+ });
85
+ }
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ return capReferences(refs);
91
+ }
92
+ async function ripgrepReferences(root, symbol, excludeFile) {
93
+ const result = await runProcess('rg', ['-n', '--glob', '!node_modules', '--glob', '!.git', '--glob', '!dist', `\\b${escapeRegex(symbol)}\\b`, '.'], {
94
+ cwd: root,
95
+ timeoutMs: 15_000,
96
+ maxOutputBytes: 512 * 1024
97
+ }).catch(() => null);
98
+ if (!result || (result.code !== 0 && !result.stdout))
99
+ return [];
100
+ return capReferences(String(result.stdout || '').split(/\r?\n/).flatMap((line) => {
101
+ const match = line.match(/^(.+?):(\d+):(.*)$/);
102
+ if (!match)
103
+ return [];
104
+ const file = normalizePath(match[1] || '');
105
+ if (!file || file === excludeFile || shouldIgnorePath(file))
106
+ return [];
107
+ return [{ symbol, file, line: Number(match[2] || 1), text: String(match[3] || '').trim().slice(0, 240) }];
108
+ }));
109
+ }
110
+ async function builtinReferences(root, symbol, excludeFile) {
111
+ const files = await listFilesRecursive(root, { ignore: ['.git', 'node_modules', 'dist', '.sneakoscope/tmp', '.sneakoscope/arenas'], maxFiles: 30_000 });
112
+ const word = new RegExp(`\\b${escapeRegex(symbol)}\\b`);
113
+ const refs = [];
114
+ for (const abs of files) {
115
+ const file = normalizePath(path.relative(root, abs));
116
+ if (!TEXT_EXT_RE.test(file) || file === excludeFile || shouldIgnorePath(file))
117
+ continue;
118
+ const text = await readText(abs, '').catch(() => '');
119
+ if (!word.test(String(text)))
120
+ continue;
121
+ const lines = String(text).split(/\r?\n/);
122
+ let perFile = 0;
123
+ for (let index = 0; index < lines.length && perFile < 50; index += 1) {
124
+ if (!word.test(lines[index] || ''))
125
+ continue;
126
+ refs.push({ symbol, file, line: index + 1, text: String(lines[index] || '').trim().slice(0, 240) });
127
+ perFile += 1;
128
+ }
129
+ }
130
+ return capReferences(refs);
131
+ }
132
+ function capReferences(refs) {
133
+ const perFile = new Map();
134
+ const out = [];
135
+ for (const ref of refs) {
136
+ const key = `${ref.symbol}:${ref.file}`;
137
+ const count = perFile.get(key) || 0;
138
+ if (count >= 50)
139
+ continue;
140
+ perFile.set(key, count + 1);
141
+ out.push(ref);
142
+ }
143
+ return out.slice(0, 500);
144
+ }
145
+ function dedupeSymbols(symbols) {
146
+ const seen = new Set();
147
+ return symbols.filter((sym) => {
148
+ const key = `${sym.file}:${sym.name}:${sym.kind}`;
149
+ if (seen.has(key))
150
+ return false;
151
+ seen.add(key);
152
+ return true;
153
+ });
154
+ }
155
+ function shouldIgnorePath(file) {
156
+ return file.startsWith('node_modules/') || file.startsWith('.git/') || file.startsWith('dist/');
157
+ }
158
+ function normalizePath(value) {
159
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
160
+ }
161
+ function escapeRegex(value) {
162
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
163
+ }
164
+ //# sourceMappingURL=impact-scan.js.map