sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -0,0 +1,207 @@
1
+ import { COMMAND_ALIASES_LITE, COMMAND_MANIFEST_LITE, LEGACY_COMMAND_ALIASES_LITE } from '../../../../../cli/command-manifest-lite.js';
2
+ import { contextGraphNodeId } from '../../ids.js';
3
+ import { addEdge, addNode, asRecord, ensureFileNode, estimateTokenCost, isCanonicalId, mapIdentifierLines, readStringField, readWorkspaceText, recordSkip, topologyLintError } from './shared.js';
4
+ export const TOPOLOGY_COMMAND_MANIFEST_PATH = 'src/cli/command-manifest-lite.ts';
5
+ export const TOPOLOGY_RUNTIME_SCRIPTS_PATH = 'runtime-required-scripts.json';
6
+ const RUNTIME_SCRIPTS_SCHEMA = 'sks.runtime-required-scripts.v1';
7
+ const COMMAND_NAME_LINE = /\{\s*name:\s*'([^']+)'/;
8
+ const EMPTY_RUNTIME_MANIFEST = {
9
+ present: false,
10
+ path: TOPOLOGY_RUNTIME_SCRIPTS_PATH,
11
+ hash: '',
12
+ sources: new Set()
13
+ };
14
+ export function collectRuntimeManifest(ctx) {
15
+ const read = readWorkspaceText(ctx, TOPOLOGY_RUNTIME_SCRIPTS_PATH);
16
+ if (!read)
17
+ return EMPTY_RUNTIME_MANIFEST;
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(read.text);
21
+ }
22
+ catch {
23
+ topologyLintError(ctx, 'invalid_node_field', `${TOPOLOGY_RUNTIME_SCRIPTS_PATH} is not valid JSON`, {
24
+ path: TOPOLOGY_RUNTIME_SCRIPTS_PATH
25
+ });
26
+ return EMPTY_RUNTIME_MANIFEST;
27
+ }
28
+ const root = asRecord(parsed);
29
+ if (!root || readStringField(root, 'schema') !== RUNTIME_SCRIPTS_SCHEMA) {
30
+ topologyLintError(ctx, 'invalid_node_field', `${TOPOLOGY_RUNTIME_SCRIPTS_PATH} does not declare ${RUNTIME_SCRIPTS_SCHEMA}`, { path: TOPOLOGY_RUNTIME_SCRIPTS_PATH });
31
+ return EMPTY_RUNTIME_MANIFEST;
32
+ }
33
+ const sources = new Set();
34
+ const scripts = Array.isArray(root.scripts) ? root.scripts : [];
35
+ for (const raw of scripts) {
36
+ const entry = asRecord(raw);
37
+ if (!entry)
38
+ continue;
39
+ const mapped = mapDistPathToSource(readStringField(entry, 'path'));
40
+ if (!mapped || !ctx.files.set.has(mapped))
41
+ continue;
42
+ ctx.fileClassification.set(mapped, readStringField(entry, 'category') || 'installed_runtime');
43
+ sources.add(mapped);
44
+ }
45
+ const policies = Array.isArray(root.reference_source_policies) ? root.reference_source_policies : [];
46
+ for (const raw of policies) {
47
+ const entry = asRecord(raw);
48
+ if (!entry)
49
+ continue;
50
+ const source = readStringField(entry, 'source').split('#')[0] ?? '';
51
+ if (!source || source.includes('*') || source.startsWith('dist/'))
52
+ continue;
53
+ if (!ctx.files.set.has(source))
54
+ continue;
55
+ ctx.fileClassification.set(source, readStringField(entry, 'classification') || 'unclassified');
56
+ sources.add(source);
57
+ }
58
+ return { present: true, path: TOPOLOGY_RUNTIME_SCRIPTS_PATH, hash: read.hash, sources };
59
+ }
60
+ function mapDistPathToSource(value) {
61
+ if (!value.startsWith('dist/') || value.includes('..'))
62
+ return null;
63
+ return `src/${value.slice('dist/'.length).replace(/\.(js|mjs|cjs)$/, '.ts')}`;
64
+ }
65
+ export function commandHandlerCandidates(name) {
66
+ return [
67
+ `src/commands/${name}.ts`,
68
+ `src/core/commands/${name}-command.ts`,
69
+ `src/cli/${name}-command.ts`,
70
+ `src/cli/${name}.ts`
71
+ ];
72
+ }
73
+ const RISK_BY_LEVEL = { R0: 'low', R1: 'medium', R2: 'medium', R3: 'high' };
74
+ export function commandNodeIdFor(name) {
75
+ return contextGraphNodeId({ kind: 'command', name });
76
+ }
77
+ function commandAliasMap() {
78
+ return {
79
+ ...LEGACY_COMMAND_ALIASES_LITE,
80
+ ...COMMAND_ALIASES_LITE
81
+ };
82
+ }
83
+ export function canonicalCommandName(candidate) {
84
+ const raw = String(candidate ?? '').trim();
85
+ if (!raw)
86
+ return null;
87
+ const resolved = commandAliasMap()[raw] ?? raw;
88
+ return COMMAND_MANIFEST_LITE.some((entry) => entry.name === resolved) ? resolved : null;
89
+ }
90
+ export function buildCommandGraph(ctx, runtime, presets) {
91
+ const read = readWorkspaceText(ctx, TOPOLOGY_COMMAND_MANIFEST_PATH);
92
+ if (!read)
93
+ return { present: false, path: TOPOLOGY_COMMAND_MANIFEST_PATH, hash: '' };
94
+ const lines = mapIdentifierLines(read.text, COMMAND_NAME_LINE);
95
+ const aliasesByCommand = groupAliases();
96
+ for (const entry of COMMAND_MANIFEST_LITE) {
97
+ const nodeId = commandNodeIdFor(entry.name);
98
+ if (!isCanonicalId('command', entry.name, nodeId)) {
99
+ topologyLintError(ctx, 'invalid_node_field', `command "${entry.name}" is not a canonical command id`, {
100
+ path: TOPOLOGY_COMMAND_MANIFEST_PATH
101
+ });
102
+ continue;
103
+ }
104
+ const line = lines.get(entry.name);
105
+ const metadata = {
106
+ source: 'command-manifest-lite',
107
+ commandName: entry.name,
108
+ summary: entry.summary,
109
+ maturity: entry.maturity,
110
+ riskLevel: entry.risk,
111
+ latency: entry.latency,
112
+ inputProfile: entry.inputProfile,
113
+ supportsJson: entry.supportsJson,
114
+ remoteAllowed: entry.remoteAllowed,
115
+ telegramAllowed: entry.telegramAllowed,
116
+ readonlyCommand: entry.readonly === true,
117
+ diagnostic: entry.diagnostic === true,
118
+ deprecated: entry.deprecated === true,
119
+ hidden: entry.hidden === true,
120
+ mutatesRouteState: entry.mutatesRouteState === true,
121
+ requiredCapabilities: [...entry.requiredCapabilities],
122
+ aliases: aliasesByCommand.get(entry.name) ?? []
123
+ };
124
+ addNode(ctx, {
125
+ id: nodeId,
126
+ kind: 'command',
127
+ label: entry.name,
128
+ path: TOPOLOGY_COMMAND_MANIFEST_PATH,
129
+ line,
130
+ contentHash: read.hash,
131
+ trust: 0.95,
132
+ risk: RISK_BY_LEVEL[entry.risk] ?? 'medium',
133
+ tokenCost: estimateTokenCost([entry.name, entry.summary]),
134
+ metadata,
135
+ sourcePath: TOPOLOGY_COMMAND_MANIFEST_PATH
136
+ });
137
+ let handlers = 0;
138
+ for (const candidate of commandHandlerCandidates(entry.name)) {
139
+ if (!ctx.files.set.has(candidate))
140
+ continue;
141
+ const fileId = ensureFileNode(ctx, candidate, 'command_handler', TOPOLOGY_COMMAND_MANIFEST_PATH);
142
+ if (!fileId)
143
+ continue;
144
+ const declared = runtime.present && runtime.sources.has(candidate);
145
+ const confidence = declared ? 'manifest' : 'derived';
146
+ const added = addEdge(ctx, {
147
+ from: nodeId,
148
+ to: fileId,
149
+ type: 'routes_to',
150
+ confidence,
151
+ path: declared ? runtime.path : TOPOLOGY_COMMAND_MANIFEST_PATH,
152
+ hash: declared ? runtime.hash : read.hash,
153
+ ...(declared ? {} : { line })
154
+ });
155
+ if (added)
156
+ handlers += 1;
157
+ if (declared) {
158
+ addEdge(ctx, {
159
+ from: nodeId,
160
+ to: fileId,
161
+ type: 'owns',
162
+ confidence: 'manifest',
163
+ path: runtime.path,
164
+ hash: runtime.hash
165
+ });
166
+ }
167
+ }
168
+ if (!handlers) {
169
+ recordSkip(ctx, TOPOLOGY_COMMAND_MANIFEST_PATH, 'excluded', `command ${entry.name} has no conventional handler file in this workspace`);
170
+ }
171
+ }
172
+ linkCommandsToPresetPipelines(ctx, presets);
173
+ return { present: true, path: TOPOLOGY_COMMAND_MANIFEST_PATH, hash: read.hash };
174
+ }
175
+ function groupAliases() {
176
+ const out = new Map();
177
+ const aliases = commandAliasMap();
178
+ for (const alias of Object.keys(aliases).sort()) {
179
+ const target = aliases[alias];
180
+ if (!target)
181
+ continue;
182
+ const bucket = out.get(target);
183
+ if (bucket)
184
+ bucket.push(alias);
185
+ else
186
+ out.set(target, [alias]);
187
+ }
188
+ return out;
189
+ }
190
+ function linkCommandsToPresetPipelines(ctx, presets) {
191
+ for (const preset of presets) {
192
+ const name = canonicalCommandName(preset.preset);
193
+ if (!name)
194
+ continue;
195
+ const commandId = commandNodeIdFor(name);
196
+ if (!ctx.nodes.has(commandId))
197
+ continue;
198
+ addEdge(ctx, {
199
+ from: commandId,
200
+ to: preset.pipelineId,
201
+ type: 'routes_to',
202
+ confidence: 'derived',
203
+ path: preset.manifestPath,
204
+ hash: preset.manifestHash
205
+ });
206
+ }
207
+ }
@@ -0,0 +1,201 @@
1
+ import { computeStronglyConnectedComponents } from '../../graph-index.js';
2
+ import { contextGraphNodeId } from '../../ids.js';
3
+ import { gateCheckCandidates, gateDependentIds, isProtectedGate } from './gates.js';
4
+ import { expandGlob, isGlobPattern } from './globs.js';
5
+ import { TOPOLOGY_GATE_AFFECTED_CAP, TOPOLOGY_GATE_VERIFIED_CAP, TOPOLOGY_GLOB_MATCH_CAP, addEdge, addNode, ensureFileNode, estimateTokenCost, recordSkip, topologyLintError } from './shared.js';
6
+ export function buildGateDependencyEdges(ctx, gates) {
7
+ const byId = new Map(gates.map((gate) => [gate.id, gate]));
8
+ const adjacency = new Map();
9
+ for (const gate of gates) {
10
+ const resolved = [];
11
+ for (const dep of [...gate.deps].sort()) {
12
+ if (dep === gate.id) {
13
+ topologyLintError(ctx, 'manifest_dag_cycle', `gate ${gate.id} depends on itself`, {
14
+ nodeId: gate.nodeId,
15
+ path: gate.manifestPath
16
+ });
17
+ continue;
18
+ }
19
+ const target = byId.get(dep);
20
+ if (!target) {
21
+ topologyLintError(ctx, 'dangling_edge', `gate ${gate.id} depends on unknown gate ${dep}`, {
22
+ nodeId: gate.nodeId,
23
+ path: gate.manifestPath
24
+ });
25
+ continue;
26
+ }
27
+ const added = addEdge(ctx, {
28
+ from: gate.nodeId,
29
+ to: target.nodeId,
30
+ type: 'depends_on',
31
+ confidence: 'manifest',
32
+ path: gate.manifestPath,
33
+ hash: gate.manifestHash,
34
+ line: gate.line
35
+ });
36
+ if (added)
37
+ resolved.push({ to: target.nodeId });
38
+ }
39
+ adjacency.set(gate.nodeId, resolved);
40
+ }
41
+ const components = computeStronglyConnectedComponents(gates.map((gate) => gate.nodeId), (nodeId) => adjacency.get(nodeId) ?? []);
42
+ for (const component of components) {
43
+ topologyLintError(ctx, 'manifest_dag_cycle', `gate dependency cycle: ${component.join(' -> ')}`, {
44
+ nodeId: component[0] ?? '',
45
+ path: gates[0]?.manifestPath ?? ''
46
+ });
47
+ }
48
+ }
49
+ export function buildGateVerificationEdges(ctx, gates) {
50
+ const verified = new Set();
51
+ for (const gate of gates) {
52
+ let emitted = 0;
53
+ for (const candidate of gateCheckCandidates(gate.command)) {
54
+ if (emitted >= TOPOLOGY_GATE_VERIFIED_CAP)
55
+ break;
56
+ const expansion = expandGlob(ctx.files, candidate, TOPOLOGY_GLOB_MATCH_CAP);
57
+ if (expansion.capped) {
58
+ recordSkip(ctx, gate.manifestPath, 'cap_reached', `check pattern ${candidate} matched ${expansion.total} files`);
59
+ continue;
60
+ }
61
+ if (!expansion.matches.length) {
62
+ recordSkip(ctx, gate.manifestPath, 'excluded', `check implementation ${candidate} is not in this workspace`);
63
+ continue;
64
+ }
65
+ const confidence = isGlobPattern(candidate) ? 'derived' : 'manifest';
66
+ for (const match of expansion.matches) {
67
+ if (emitted >= TOPOLOGY_GATE_VERIFIED_CAP)
68
+ break;
69
+ const fileId = ensureFileNode(ctx, match, 'gate_check', gate.manifestPath);
70
+ if (!fileId)
71
+ continue;
72
+ const added = addEdge(ctx, {
73
+ from: gate.nodeId,
74
+ to: fileId,
75
+ type: 'verified_by',
76
+ confidence,
77
+ path: gate.manifestPath,
78
+ hash: gate.manifestHash,
79
+ line: gate.line
80
+ });
81
+ if (added) {
82
+ emitted += 1;
83
+ verified.add(gate.id);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return verified;
89
+ }
90
+ export function buildGateAffectedEdges(ctx, gates) {
91
+ const affected = new Set();
92
+ for (const gate of gates) {
93
+ let emitted = 0;
94
+ const seen = new Set();
95
+ for (const input of gate.cacheInputs) {
96
+ if (emitted >= TOPOLOGY_GATE_AFFECTED_CAP)
97
+ break;
98
+ const expansion = expandGlob(ctx.files, input, TOPOLOGY_GLOB_MATCH_CAP);
99
+ if (expansion.capped) {
100
+ recordSkip(ctx, gate.manifestPath, 'cap_reached', `cache input ${input} matched ${expansion.total} files`);
101
+ continue;
102
+ }
103
+ for (const match of expansion.matches) {
104
+ if (emitted >= TOPOLOGY_GATE_AFFECTED_CAP)
105
+ break;
106
+ if (seen.has(match))
107
+ continue;
108
+ seen.add(match);
109
+ const fileId = ensureFileNode(ctx, match, 'gate_cache_input', gate.manifestPath);
110
+ if (!fileId)
111
+ continue;
112
+ const added = addEdge(ctx, {
113
+ from: gate.nodeId,
114
+ to: fileId,
115
+ type: 'affected_by',
116
+ confidence: 'derived',
117
+ path: gate.manifestPath,
118
+ hash: gate.manifestHash,
119
+ line: gate.line
120
+ });
121
+ if (added) {
122
+ emitted += 1;
123
+ affected.add(gate.id);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return affected;
129
+ }
130
+ export function reportUnbackedProtectedGates(ctx, gates, verified, affected) {
131
+ const dependedOn = gateDependentIds(gates);
132
+ for (const gate of gates) {
133
+ if (!isProtectedGate(gate.id, dependedOn))
134
+ continue;
135
+ if (verified.has(gate.id) || affected.has(gate.id))
136
+ continue;
137
+ topologyLintError(ctx, 'protected_gate_without_source_relation', `protected gate ${gate.id} has no check implementation and no cache input inside this workspace`, { nodeId: gate.nodeId, path: gate.manifestPath });
138
+ }
139
+ }
140
+ export function buildGatePresetPipelines(ctx, gates) {
141
+ const byPreset = new Map();
142
+ for (const gate of gates) {
143
+ for (const preset of gate.presets) {
144
+ const bucket = byPreset.get(preset);
145
+ if (bucket)
146
+ bucket.push(gate);
147
+ else
148
+ byPreset.set(preset, [gate]);
149
+ }
150
+ }
151
+ const dependedOn = gateDependentIds(gates);
152
+ const pipelines = [];
153
+ for (const preset of [...byPreset.keys()].sort()) {
154
+ const members = byPreset.get(preset) ?? [];
155
+ const first = members[0];
156
+ if (!first)
157
+ continue;
158
+ const pipelineId = contextGraphNodeId({ kind: 'pipeline', pipelineId: `gates:${preset}` });
159
+ const metadata = {
160
+ source: 'gate-manifest',
161
+ pipelineKind: 'gate_preset',
162
+ preset,
163
+ manifest: first.manifestPath,
164
+ gateCount: members.length,
165
+ protectedGateCount: members.filter((gate) => isProtectedGate(gate.id, dependedOn)).length
166
+ };
167
+ const added = addNode(ctx, {
168
+ id: pipelineId,
169
+ kind: 'pipeline',
170
+ label: `gates:${preset}`,
171
+ path: first.manifestPath,
172
+ contentHash: first.manifestHash,
173
+ trust: 0.95,
174
+ risk: preset === 'release' ? 'protected' : 'high',
175
+ tokenCost: estimateTokenCost([preset, String(members.length)]),
176
+ metadata,
177
+ sourcePath: first.manifestPath
178
+ });
179
+ if (!added)
180
+ continue;
181
+ for (const gate of members) {
182
+ addEdge(ctx, {
183
+ from: pipelineId,
184
+ to: gate.nodeId,
185
+ type: 'gated_by',
186
+ confidence: 'manifest',
187
+ path: gate.manifestPath,
188
+ hash: gate.manifestHash,
189
+ line: gate.line
190
+ });
191
+ }
192
+ pipelines.push({
193
+ preset,
194
+ pipelineId,
195
+ manifestPath: first.manifestPath,
196
+ manifestHash: first.manifestHash,
197
+ gateCount: members.length
198
+ });
199
+ }
200
+ return pipelines;
201
+ }
@@ -0,0 +1,182 @@
1
+ import { ALWAYS_ON_GATES, FORBIDDEN_RECURSIVE_GATES, REQUIRED_FOR_PUBLISH } from '../../../../release/gate-manifest.js';
2
+ import { contextGraphNodeId } from '../../ids.js';
3
+ import { addNode, asRecord, estimateTokenCost, isCanonicalId, mapIdentifierLines, readNumberField, readStringArrayField, readStringField, readWorkspaceText, recordSkip, topologyLintError } from './shared.js';
4
+ export const TOPOLOGY_GATE_MANIFESTS = [
5
+ { path: 'release-gates.v2.json', schemas: ['sks.release-gates.v2'] },
6
+ { path: 'infra-harness-gates.json', schemas: ['sks.infra-harness-gates.v1'] }
7
+ ];
8
+ export const TOPOLOGY_PROTECTED_NAMESPACES = new Set([
9
+ 'release',
10
+ 'publish',
11
+ 'security',
12
+ 'secret',
13
+ 'prepublish'
14
+ ]);
15
+ const GATE_ID_LINE = /"id"\s*:\s*"([^"]+)"/;
16
+ export function collectTopologyGates(ctx) {
17
+ const gates = [];
18
+ const seen = new Map();
19
+ for (const manifest of TOPOLOGY_GATE_MANIFESTS) {
20
+ const read = readWorkspaceText(ctx, manifest.path);
21
+ if (!read)
22
+ continue;
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(read.text);
26
+ }
27
+ catch {
28
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} is not valid JSON`, { path: manifest.path });
29
+ continue;
30
+ }
31
+ const root = asRecord(parsed);
32
+ if (!root) {
33
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} is not a gate manifest object`, { path: manifest.path });
34
+ continue;
35
+ }
36
+ const schema = readStringField(root, 'schema');
37
+ if (!manifest.schemas.includes(schema)) {
38
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} declares schema "${schema}" instead of ${manifest.schemas.join(' | ')}`, { path: manifest.path });
39
+ continue;
40
+ }
41
+ const rawGates = root.gates;
42
+ if (!Array.isArray(rawGates)) {
43
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} has no gates array`, { path: manifest.path });
44
+ continue;
45
+ }
46
+ const lines = mapIdentifierLines(read.text, GATE_ID_LINE);
47
+ for (const [index, rawGate] of rawGates.entries()) {
48
+ const entry = asRecord(rawGate);
49
+ if (!entry) {
50
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} gate[${index}] is not an object`, { path: manifest.path });
51
+ continue;
52
+ }
53
+ const id = readStringField(entry, 'id').trim();
54
+ if (!id) {
55
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} gate[${index}] has no id`, { path: manifest.path });
56
+ continue;
57
+ }
58
+ const nodeId = contextGraphNodeId({ kind: 'gate', gateId: id });
59
+ if (!isCanonicalId('gate', id, nodeId)) {
60
+ topologyLintError(ctx, 'invalid_node_field', `${manifest.path} gate id "${id}" is not a canonical gate id`, {
61
+ path: manifest.path
62
+ });
63
+ continue;
64
+ }
65
+ const previous = seen.get(id);
66
+ if (previous) {
67
+ topologyLintError(ctx, 'duplicate_node_conflict', `gate ${id} is declared in both ${previous} and ${manifest.path}`, {
68
+ nodeId,
69
+ path: manifest.path
70
+ });
71
+ continue;
72
+ }
73
+ seen.set(id, manifest.path);
74
+ const cache = asRecord(entry.cache);
75
+ gates.push({
76
+ id,
77
+ nodeId,
78
+ manifestPath: manifest.path,
79
+ manifestHash: read.hash,
80
+ line: lines.get(id),
81
+ command: readStringField(entry, 'command'),
82
+ deps: readStringArrayField(entry, 'deps'),
83
+ cacheInputs: cache ? readStringArrayField(cache, 'inputs') : [],
84
+ cacheEnabled: cache ? cache.enabled === true : false,
85
+ presets: readStringArrayField(entry, 'preset'),
86
+ sideEffect: readStringField(entry, 'side_effect'),
87
+ resource: readStringArrayField(entry, 'resource'),
88
+ timeoutMs: readNumberField(entry, 'timeout_ms'),
89
+ outputContract: readStringField(entry, 'output_contract'),
90
+ declaredProtected: entry.protected === true
91
+ });
92
+ }
93
+ }
94
+ gates.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
95
+ return gates;
96
+ }
97
+ export function gateDependentIds(gates) {
98
+ const dependedOn = new Set();
99
+ for (const gate of gates)
100
+ for (const dep of gate.deps)
101
+ dependedOn.add(dep);
102
+ return dependedOn;
103
+ }
104
+ export function isProtectedGate(id, dependedOn, declaredProtected = false) {
105
+ if (declaredProtected)
106
+ return true;
107
+ if (REQUIRED_FOR_PUBLISH.has(id) || ALWAYS_ON_GATES.has(id) || FORBIDDEN_RECURSIVE_GATES.has(id))
108
+ return true;
109
+ if (dependedOn.has(id))
110
+ return true;
111
+ return TOPOLOGY_PROTECTED_NAMESPACES.has(id.split(':')[0] ?? '');
112
+ }
113
+ function gateRisk(gate, dependedOn) {
114
+ if (isProtectedGate(gate.id, dependedOn, gate.declaredProtected))
115
+ return 'protected';
116
+ return gate.sideEffect === 'hermetic' ? 'medium' : 'high';
117
+ }
118
+ export function gateCheckCandidates(command) {
119
+ const out = [];
120
+ for (const token of String(command ?? '').split(/\s+/)) {
121
+ const cleaned = token.replace(/^['"]|['"]$/g, '').replace(/^\.\//, '');
122
+ if (!/\.(js|mjs|cjs)$/.test(cleaned))
123
+ continue;
124
+ if (cleaned.startsWith('node_modules/'))
125
+ continue;
126
+ if (cleaned.startsWith('/') || cleaned.startsWith('~') || /^[A-Za-z]:/.test(cleaned))
127
+ continue;
128
+ if (cleaned.includes('..'))
129
+ continue;
130
+ const mapped = cleaned.startsWith('dist/')
131
+ ? `src/${cleaned.slice('dist/'.length).replace(/\.(js|mjs|cjs)$/, '.ts')}`
132
+ : cleaned;
133
+ if (!out.includes(mapped))
134
+ out.push(mapped);
135
+ }
136
+ return out;
137
+ }
138
+ export function buildGateNodes(ctx, gates) {
139
+ const dependedOn = gateDependentIds(gates);
140
+ for (const gate of gates) {
141
+ const metadata = {
142
+ source: 'gate-manifest',
143
+ manifest: gate.manifestPath,
144
+ gateId: gate.id,
145
+ namespace: gate.id.split(':')[0] ?? '',
146
+ deps: [...gate.deps],
147
+ presets: [...gate.presets],
148
+ resource: [...gate.resource],
149
+ sideEffect: gate.sideEffect,
150
+ timeoutMs: gate.timeoutMs,
151
+ cacheEnabled: gate.cacheEnabled,
152
+ cacheInputs: [...gate.cacheInputs],
153
+ checkScripts: gateCheckCandidates(gate.command),
154
+ outputContract: gate.outputContract,
155
+ requiredForPublish: REQUIRED_FOR_PUBLISH.has(gate.id),
156
+ alwaysOnRelease: ALWAYS_ON_GATES.has(gate.id),
157
+ nonRecursive: FORBIDDEN_RECURSIVE_GATES.has(gate.id),
158
+ hasDependents: dependedOn.has(gate.id),
159
+ declaredProtected: gate.declaredProtected
160
+ };
161
+ addNode(ctx, {
162
+ id: gate.nodeId,
163
+ kind: 'gate',
164
+ label: gate.id,
165
+ path: gate.manifestPath,
166
+ line: gate.line,
167
+ contentHash: gate.manifestHash,
168
+ trust: 0.95,
169
+ risk: gateRisk(gate, dependedOn),
170
+ tokenCost: estimateTokenCost([gate.id, gate.sideEffect, gate.cacheInputs.join(' ')]),
171
+ metadata,
172
+ sourcePath: gate.manifestPath
173
+ });
174
+ }
175
+ if (!gates.length) {
176
+ for (const manifest of TOPOLOGY_GATE_MANIFESTS) {
177
+ if (ctx.inputHashes[manifest.path] === undefined)
178
+ continue;
179
+ recordSkip(ctx, manifest.path, 'excluded', 'manifest declared no usable gates');
180
+ }
181
+ }
182
+ }