@logictan/dsh-config-manager 0.1.60

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 (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Bundle manifest JSON Schema ↔ 实现绑定测试。
3
+ *
4
+ * 目的:把 `docs/spec/bundle-manifest.schema.json`(Bundle Format v1 的机器可校验形态)
5
+ * 焊死在 `src/schema/manifest.ts` 的实现上。规格最大的死法是漂移——文档说一套、代码做一套。
6
+ *
7
+ * 方案选择:(a) 手写仅覆盖本 schema 所用关键字的最小校验器,而不是 (b) 退化为
8
+ * 「必需字段集合 + 类型」的等价断言。理由:
9
+ * 1. 本仓库规矩是「不为小功能加依赖」(AGENTS.md §Dependency Rules),故不引入 ajv;
10
+ * 2. (b) 只能覆盖 `required`/`type`,会让 schema 里 `enum`/`additionalProperties`/`items`
11
+ * 等关键字成为「没人验证的装饰」——它们写错了测试也不会红,正是本测试要防的死法;
12
+ * 3. 手写校验器直接消费 schema 原文(readFileSync + JSON.parse),保证被验证的就是
13
+ * 发布出去的那份文件;schema 一旦用上未支持的关键字,K-03 会立刻红灯。
14
+ *
15
+ * 覆盖:K-* schema 自身健全性 / S-* 真实 buildManifest 产物契约 / R-* 拒绝路径与
16
+ * validateManifest 逐条一致 / C-* 已知兼容语义(未知分区只 warning)与已记录的语义落差。
17
+ */
18
+ import test from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import { readFileSync } from 'node:fs';
21
+ import { resolve } from 'node:path';
22
+
23
+ import {
24
+ buildManifest, parseManifest, serializeManifest, validateManifest, MANIFEST_FILE,
25
+ } from './manifest.ts';
26
+ import { SECTION_IDS } from './config.ts';
27
+ import { CURRENT_SCHEMA_VERSION } from './versions.ts';
28
+ import type { Manifest } from './types.ts';
29
+
30
+ /* ============================================================================
31
+ * (a) 最小 JSON Schema 校验器 —— 只覆盖本 schema 使用的关键字集合
32
+ * (白名单是「已实现」集合,当前 schema 未用到 items/enum/$ref,由 K-05 自检兜底)
33
+ * ==========================================================================*/
34
+
35
+ /** 支持的关键字白名单(K-03 用它钉死「schema 不得引入未支持关键字」) */
36
+ const SUPPORTED_KEYWORDS: readonly string[] = [
37
+ 'type', 'required', 'properties', 'items', 'enum', 'additionalProperties', 'oneOf', '$ref',
38
+ ];
39
+
40
+ /** 仅出现在 schema 根部、用于文档与工具提示的非校验键 */
41
+ const ANNOTATION_KEYS: readonly string[] = ['$schema', '$id', 'title', 'description'];
42
+
43
+ interface SchemaIssue { path: string; message: string }
44
+
45
+ type SchemaNode = Record<string, unknown>;
46
+
47
+ function typeOf(value: unknown): string {
48
+ if (value === null) return 'null';
49
+ if (Array.isArray(value)) return 'array';
50
+ return typeof value;
51
+ }
52
+
53
+ /** JSON Pointer 解析(只支持本 schema 用到的 `#/a/b` 形式) */
54
+ function resolveRef(root: SchemaNode, ref: string): SchemaNode {
55
+ assert.ok(ref.startsWith('#/'), `仅支持本地 $ref,收到 ${ref}`);
56
+ let node: unknown = root;
57
+ for (const rawSeg of ref.slice(2).split('/')) {
58
+ const seg = rawSeg.replace(/~1/g, '/').replace(/~0/g, '~');
59
+ assert.ok(node !== null && typeof node === 'object', `$ref 路径 ${ref} 在 "${seg}" 处断开`);
60
+ node = (node as SchemaNode)[seg];
61
+ }
62
+ assert.ok(node !== null && typeof node === 'object', `$ref ${ref} 未指向对象`);
63
+ return node as SchemaNode;
64
+ }
65
+
66
+ /** 依据 schema 节点校验数据,返回全部问题(不短路,便于与 validateManifest 做集合比对) */
67
+ function validateNode(schema: SchemaNode, data: unknown, path: string, root: SchemaNode): SchemaIssue[] {
68
+ const issues: SchemaIssue[] = [];
69
+
70
+ if (typeof schema['$ref'] === 'string') {
71
+ return validateNode(resolveRef(root, schema['$ref']), data, path, root);
72
+ }
73
+
74
+ const types = schema['type'];
75
+ if (typeof types === 'string') {
76
+ if (typeOf(data) !== types) {
77
+ issues.push({ path, message: `期望类型 ${types},收到 ${typeOf(data)}` });
78
+ return issues;
79
+ }
80
+ } else if (Array.isArray(types)) {
81
+ const allowed = types.filter((t): t is string => typeof t === 'string');
82
+ if (!allowed.includes(typeOf(data))) {
83
+ issues.push({ path, message: `期望类型 ${allowed.join(' | ')},收到 ${typeOf(data)}` });
84
+ return issues;
85
+ }
86
+ }
87
+
88
+ if (Array.isArray(schema['enum'])) {
89
+ const allowed = schema['enum'] as unknown[];
90
+ if (!allowed.some((a) => Object.is(a, data))) {
91
+ issues.push({ path, message: `取值必须是 ${allowed.map((a) => JSON.stringify(a)).join(' | ')} 之一` });
92
+ }
93
+ }
94
+
95
+ if (Array.isArray(schema['oneOf'])) {
96
+ const branches = schema['oneOf'] as SchemaNode[];
97
+ const matched = branches.filter((b) => validateNode(b, data, path, root).length === 0).length;
98
+ if (matched !== 1) {
99
+ const shapes = branches.map((b) => JSON.stringify(b['type'] ?? b['$ref'] ?? '?')).join(' | ');
100
+ issues.push({ path, message: `必须恰好匹配一种形态(${shapes}),实际匹配 ${matched} 种` });
101
+ }
102
+ }
103
+
104
+ if (typeOf(data) === 'object') {
105
+ const obj = data as Record<string, unknown>;
106
+ const required = schema['required'];
107
+ if (Array.isArray(required)) {
108
+ for (const key of required) {
109
+ if (typeof key === 'string' && !Object.prototype.hasOwnProperty.call(obj, key)) {
110
+ issues.push({ path: path === '$' ? key : `${path}.${key}`, message: `缺少必需字段 ${key}` });
111
+ }
112
+ }
113
+ }
114
+ const props = schema['properties'];
115
+ const propsObj = (props !== null && typeof props === 'object') ? props as SchemaNode : {};
116
+ for (const [key, sub] of Object.entries(propsObj)) {
117
+ if (Object.prototype.hasOwnProperty.call(obj, key) && sub !== null && typeof sub === 'object') {
118
+ issues.push(...validateNode(sub as SchemaNode, obj[key], path === '$' ? key : `${path}.${key}`, root));
119
+ }
120
+ }
121
+ const extra = schema['additionalProperties'];
122
+ if (extra !== undefined && extra !== true) {
123
+ for (const key of Object.keys(obj)) {
124
+ if (Object.prototype.hasOwnProperty.call(propsObj, key)) continue;
125
+ if (extra === false) {
126
+ issues.push({ path: path === '$' ? key : `${path}.${key}`, message: `不允许的额外字段 ${key}` });
127
+ } else if (extra !== null && typeof extra === 'object') {
128
+ issues.push(...validateNode(extra as SchemaNode, obj[key], path === '$' ? key : `${path}.${key}`, root));
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ if (Array.isArray(data) && schema['items'] !== undefined) {
135
+ const items = schema['items'];
136
+ if (items !== null && typeof items === 'object') {
137
+ data.forEach((item, idx) => {
138
+ issues.push(...validateNode(items as SchemaNode, item, `${path}[${idx}]`, root));
139
+ });
140
+ }
141
+ }
142
+
143
+ return issues;
144
+ }
145
+
146
+ /** 用 schema 校验任意数据;`$` 为根路径 */
147
+ function validateAgainstSchema(schema: SchemaNode, data: unknown): SchemaIssue[] {
148
+ return validateNode(schema, data, '$', schema);
149
+ }
150
+
151
+ /* ============================================================================
152
+ * 载入 schema 原文(被验证的就是发布出去的那份文件)
153
+ * ==========================================================================*/
154
+
155
+ const SCHEMA_PATH = resolve(import.meta.dirname, '..', '..', 'docs', 'spec', 'bundle-manifest.schema.json');
156
+ const SCHEMA = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) as SchemaNode;
157
+
158
+ /* ============================================================================
159
+ * 夹具与工具
160
+ * ==========================================================================*/
161
+
162
+ function sectionsAllFalse(): Record<string, boolean> {
163
+ const flags: Record<string, boolean> = {};
164
+ for (const id of SECTION_IDS) flags[id] = false;
165
+ return flags;
166
+ }
167
+
168
+ /** 用真实 buildManifest 构造基准 manifest(未加密、无秘密) */
169
+ function baselineManifest(): Manifest {
170
+ return buildManifest({
171
+ exporterVersion: '0.1.59',
172
+ dshVersion: '0.1.0-rc.6',
173
+ platform: 'win32',
174
+ arch: 'x64',
175
+ sections: sectionsAllFalse() as unknown as Manifest['sections'],
176
+ containsSecrets: false,
177
+ encrypted: false,
178
+ encryption: null,
179
+ exportedAt: '2026-08-14T12:00:00.000Z',
180
+ });
181
+ }
182
+
183
+ function deepClone<T>(value: T): T {
184
+ return JSON.parse(JSON.stringify(value)) as T;
185
+ }
186
+
187
+ /** 深拷贝基准 manifest 并施加改动;`undefined` 表示删除该字段 */
188
+ function mutate(overrides: Record<string, unknown>): Record<string, unknown> {
189
+ const base = deepClone(baselineManifest()) as unknown as Record<string, unknown>;
190
+ for (const [key, value] of Object.entries(overrides)) {
191
+ if (value === undefined) delete base[key];
192
+ else base[key] = value;
193
+ }
194
+ return base;
195
+ }
196
+
197
+ const errorPaths = (m: unknown): string[] =>
198
+ validateManifest(m).filter((i) => i.severity === 'error').map((i) => i.path).sort();
199
+ const warningPaths = (m: unknown): string[] =>
200
+ validateManifest(m).filter((i) => i.severity === 'warning').map((i) => i.path).sort();
201
+ const schemaPaths = (m: unknown): string[] =>
202
+ validateAgainstSchema(SCHEMA, m).map((i) => i.path).sort();
203
+
204
+ /** 递归收集 schema 节点中出现的全部关键字(跳过纯注解键;properties 的直接子键是字段名,不是关键字) */
205
+ function collectKeywords(node: unknown, found: Set<string>): void {
206
+ if (node === null || typeof node !== 'object') return;
207
+ if (Array.isArray(node)) {
208
+ for (const item of node) collectKeywords(item, found);
209
+ return;
210
+ }
211
+ for (const [key, value] of Object.entries(node as SchemaNode)) {
212
+ if (key === 'properties') {
213
+ // properties 的子键是字段名;只递归进每个字段的子 schema
214
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
215
+ for (const sub of Object.values(value as SchemaNode)) collectKeywords(sub, found);
216
+ }
217
+ continue;
218
+ }
219
+ if (!key.startsWith('$') && !ANNOTATION_KEYS.includes(key)) found.add(key);
220
+ collectKeywords(value, found);
221
+ }
222
+ }
223
+
224
+ /** 从 schema 根部收集关键字(x-* 是纯文档扩展,整体跳过) */
225
+ function collectSchemaKeywords(schema: SchemaNode): Set<string> {
226
+ const found = new Set<string>();
227
+ for (const [key, value] of Object.entries(schema)) {
228
+ if (key.startsWith('x-')) continue;
229
+ if (key === 'properties') {
230
+ for (const sub of Object.values(value as SchemaNode)) collectKeywords(sub, found);
231
+ continue;
232
+ }
233
+ if (!key.startsWith('$') && !ANNOTATION_KEYS.includes(key)) found.add(key);
234
+ collectKeywords(value, found);
235
+ }
236
+ return found;
237
+ }
238
+
239
+ /* ============================================================================
240
+ * K-*:schema 自身的健全性
241
+ * ==========================================================================*/
242
+
243
+ test('K-01 schema 是合法 JSON 且声明 draft 2020-12 与根对象形状', () => {
244
+ assert.equal(SCHEMA['$schema'], 'https://json-schema.org/draft/2020-12/schema', '必须声明 draft 2020-12');
245
+ assert.equal(typeof SCHEMA['$id'], 'string', '$id 应为字符串(可引用)');
246
+ assert.equal(SCHEMA['type'], 'object', '根类型必须是 object');
247
+ assert.deepEqual(
248
+ SCHEMA['required'],
249
+ ['schemaVersion', 'exporter', 'source', 'exportedAt', 'sections', 'security'],
250
+ '根必需字段集合即 validateManifest 要求必须存在的字段',
251
+ );
252
+ const props = SCHEMA['properties'] as Record<string, unknown>;
253
+ assert.deepEqual(
254
+ Object.keys(props).sort(),
255
+ ['exportedAt', 'exporter', 'schemaVersion', 'sections', 'security', 'source'],
256
+ 'schema 的 properties 必须与实现校验的字段集合一一对应(多一个=声明了没人校验的字段)',
257
+ );
258
+ });
259
+
260
+ test('K-02 schema 不含零宽字符(U+200B/200C/200D/2060/FEFF)', () => {
261
+ const raw = readFileSync(SCHEMA_PATH, 'utf8');
262
+ const hits = [...raw.matchAll(/[\u200b\u200c\u200d\u2060\ufeff]/gu)].map((m) => m.index);
263
+ assert.deepEqual(hits, [], `schema 含零宽字符,位置 ${hits.join(',')}`);
264
+ });
265
+
266
+ test('K-03 schema 只使用最小校验器已实现的关键字(新增关键字必须同步实现,否则本测试红灯)', () => {
267
+ // 只收集「校验位置」上的键:根级非 x- 关键字 + properties 内每个子 schema 的键。
268
+ // properties 的直接子键是字段名(schemaVersion / exporter / …)而非关键字,collectSchemaKeywords 已区分。
269
+ const unsupported = [...collectSchemaKeywords(SCHEMA)].filter((k) => !SUPPORTED_KEYWORDS.includes(k));
270
+ assert.deepEqual(
271
+ unsupported, [],
272
+ `schema 引入了最小校验器未实现的关键字:${unsupported.join(', ')}。` +
273
+ '要么改用已支持关键字,要么在 manifest-schema.test.ts 的 validateNode 中实现它。',
274
+ );
275
+ });
276
+
277
+ test('K-04 x-* 扩展键仅作为根级文档说明存在,且分区 id 表与 SECTION_IDS 一致', () => {
278
+ const xKeys = Object.keys(SCHEMA).filter((k) => k.startsWith('x-'));
279
+ assert.ok(xKeys.includes('x-section-ids'), '应记录导出端恒写的分区 id 表');
280
+ assert.ok(xKeys.includes('x-platform-values'), '应记录 platform 取值表(不设 enum,仅说明)');
281
+ assert.ok(xKeys.includes('x-semantic-constraints'), '应显式记录 JSON Schema 无法表达的语义约束');
282
+ assert.deepEqual(
283
+ SCHEMA['x-section-ids'], [...SECTION_IDS],
284
+ 'schema 记录的 15 个分区 id 必须与 SECTION_IDS 完全一致(防止分区增删后文档漂移)',
285
+ );
286
+ assert.equal(SCHEMA['x-format-version'], 'Bundle Format v1');
287
+ assert.equal(SCHEMA['x-schema-version-constant'], CURRENT_SCHEMA_VERSION);
288
+ });
289
+
290
+ test('K-05 最小校验器自检:enum / items / oneOf 分支确实生效(防止 K-03 为一个坏掉的实现背书)', () => {
291
+ // K-03 的白名单把 enum/items/oneOf 声明为「已支持」。若这些分支实际是坏的,
292
+ // 未来给 schema 加上它们就会静默失效——正是本测试要防的死法,故在此独立钉死。
293
+ const enumSchema: SchemaNode = { type: 'string', enum: ['a', 'b'] };
294
+ assert.deepEqual(validateAgainstSchema(enumSchema, 'a'), [], 'enum 命中应通过');
295
+ assert.deepEqual(validateAgainstSchema(enumSchema, 'c').map((i) => i.path), ['$'], 'enum 未命中应拒绝');
296
+
297
+ const itemsSchema: SchemaNode = { type: 'array', items: { type: 'number' } };
298
+ assert.deepEqual(validateAgainstSchema(itemsSchema, [1, 2]), [], 'items 全合法应通过');
299
+ assert.deepEqual(
300
+ validateAgainstSchema(itemsSchema, [1, 'x']).map((i) => i.path), ['$[1]'],
301
+ 'items 应按索引定位违规元素',
302
+ );
303
+
304
+ const oneOfSchema: SchemaNode = { oneOf: [{ type: 'object' }, { type: 'null' }] };
305
+ assert.deepEqual(validateAgainstSchema(oneOfSchema, {}).map((i) => i.path), [], 'oneOf 单分支命中应通过');
306
+ assert.deepEqual(validateAgainstSchema(oneOfSchema, null).map((i) => i.path), [], 'oneOf 另一分支命中应通过');
307
+ assert.deepEqual(validateAgainstSchema(oneOfSchema, 'x').map((i) => i.path), ['$'], 'oneOf 零分支命中应拒绝');
308
+
309
+ const requiredSchema: SchemaNode = { type: 'object', required: ['k'], properties: { k: { type: 'string' } } };
310
+ assert.deepEqual(validateAgainstSchema(requiredSchema, {}).map((i) => i.path), ['k'], '缺 required 字段应定位到字段名');
311
+ assert.deepEqual(validateAgainstSchema(requiredSchema, { k: 1 }).map((i) => i.path), ['k'], '类型错误应定位到字段名');
312
+ assert.deepEqual(validateAgainstSchema(requiredSchema, { k: 'v', extra: 1 }), [], '未声明 additionalProperties 时不得拒绝额外字段');
313
+ });
314
+
315
+ /* ============================================================================
316
+ * S-*:真实 buildManifest 产物满足 schema
317
+ * ==========================================================================*/
318
+
319
+ test('S-01 真实 buildManifest 产物满足 schema 声明的全部必需字段与类型', () => {
320
+ const m = baselineManifest();
321
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], 'buildManifest 产物必须零 schema 违规');
322
+ assert.deepEqual(errorPaths(m), [], '且必须零 validateManifest error');
323
+ });
324
+
325
+ test('S-02 sections 恒含全部 15 个已知分区 id(规格 §2.5),值均为布尔', () => {
326
+ const m = baselineManifest();
327
+ const keys = Object.keys(m.sections).sort();
328
+ assert.equal(SECTION_IDS.length, 15, 'SectionId 共 15 个(含无 adapter 的 secrets)');
329
+ assert.deepEqual(keys, [...SECTION_IDS].sort(), 'sections 键集合必须等于 SECTION_IDS');
330
+ for (const key of keys) {
331
+ assert.equal(typeof m.sections[key as (typeof SECTION_IDS)[number]], 'boolean', `sections.${key} 必须是布尔`);
332
+ }
333
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '全量 sections 必须满足 schema');
334
+ });
335
+
336
+ test('S-03 全部 15 个分区置 true 的 manifest 仍满足 schema(sections 是开放布尔映射)', () => {
337
+ const all = sectionsAllFalse();
338
+ for (const id of SECTION_IDS) all[id] = true;
339
+ const m = buildManifest({
340
+ exporterVersion: '0.1.59',
341
+ dshVersion: '0.1.0-rc.6',
342
+ platform: 'linux',
343
+ arch: 'arm64',
344
+ sections: all as unknown as Manifest['sections'],
345
+ containsSecrets: true,
346
+ encrypted: true,
347
+ encryption: {
348
+ algorithm: 'aes-256-gcm',
349
+ kdf: 'scrypt',
350
+ kdfParams: { N: 16384, r: 8, p: 1, keyLength: 32 },
351
+ salt: 'c2FsdA==',
352
+ iv: 'aXY=',
353
+ authTag: 'dGFn',
354
+ version: 1,
355
+ },
356
+ exportedAt: '2026-08-14T12:00:00.000Z',
357
+ });
358
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '加密 manifest 必须满足 schema');
359
+ assert.deepEqual(errorPaths(m), [], '加密 manifest 必须零 validateManifest error');
360
+ });
361
+
362
+ test('S-04 真实产物经 serializeManifest → parseManifest 往返后仍满足 schema', () => {
363
+ const m = baselineManifest();
364
+ const raw = serializeManifest(m);
365
+ assert.deepEqual(validateAgainstSchema(SCHEMA, JSON.parse(raw)), [], '序列化产物必须满足 schema');
366
+ const reparsed = parseManifest(raw);
367
+ assert.deepEqual(validateAgainstSchema(SCHEMA, reparsed), [], '解析结果必须满足 schema');
368
+ assert.equal(MANIFEST_FILE, 'manifest.json', 'schema 描述的文件名与常量一致');
369
+ });
370
+
371
+ /* ============================================================================
372
+ * R-*:拒绝路径必须与 validateManifest 逐条一致
373
+ * ==========================================================================*/
374
+
375
+ interface RejectCase { name: string; value: unknown; expect: string }
376
+
377
+ const REJECT_CASES: RejectCase[] = [
378
+ { name: '非对象(null)', value: null, expect: '$' },
379
+ { name: '非对象(字符串)', value: 'str', expect: '$' },
380
+ { name: 'schemaVersion 缺失', value: mutate({ schemaVersion: undefined }), expect: 'schemaVersion' },
381
+ { name: 'schemaVersion 类型错误', value: mutate({ schemaVersion: 'one' }), expect: 'schemaVersion' },
382
+ { name: 'exporter 缺失', value: mutate({ exporter: undefined }), expect: 'exporter' },
383
+ { name: 'exporter.name 类型错误', value: mutate({ exporter: { name: 1, version: 'x' } }), expect: 'exporter.name' },
384
+ { name: 'exporter.version 缺失', value: mutate({ exporter: { name: 'n' } }), expect: 'exporter.version' },
385
+ { name: 'source 缺失', value: mutate({ source: undefined }), expect: 'source' },
386
+ { name: 'source.arch 缺失', value: mutate({ source: { dshVersion: 'v', platform: 'win32' } }), expect: 'source.arch' },
387
+ { name: 'source.platform 类型错误', value: mutate({ source: { dshVersion: 'v', platform: 1, arch: 'x64' } }), expect: 'source.platform' },
388
+ { name: 'exportedAt 缺失', value: mutate({ exportedAt: undefined }), expect: 'exportedAt' },
389
+ { name: 'sections 缺失', value: mutate({ sections: undefined }), expect: 'sections' },
390
+ { name: 'sections 值非布尔', value: mutate({ sections: { ...sectionsAllFalse(), settings: 'yes' } }), expect: 'sections.settings' },
391
+ { name: 'security 缺失', value: mutate({ security: undefined }), expect: 'security' },
392
+ {
393
+ name: 'security.containsSecrets 类型错误',
394
+ value: mutate({ security: { containsSecrets: 'yes', encrypted: false, encryption: null } }),
395
+ expect: 'security.containsSecrets',
396
+ },
397
+ {
398
+ name: 'security.encrypted 缺失',
399
+ value: mutate({ security: { containsSecrets: false, encryption: null } }),
400
+ expect: 'security.encrypted',
401
+ },
402
+ {
403
+ name: 'security.encryption 缺失',
404
+ value: mutate({ security: { containsSecrets: false, encrypted: false } }),
405
+ expect: 'security.encryption',
406
+ },
407
+ {
408
+ name: 'security.encryption 类型错误(字符串)',
409
+ value: mutate({ security: { containsSecrets: false, encrypted: false, encryption: 'nope' } }),
410
+ expect: 'security.encryption',
411
+ },
412
+ ];
413
+
414
+ test('R-01 每个非法输入都被 validateManifest 拒绝,且 schema 在完全相同的路径上拒绝', () => {
415
+ for (const c of REJECT_CASES) {
416
+ const implErrors = errorPaths(c.value);
417
+ assert.ok(
418
+ implErrors.includes(c.expect),
419
+ `[${c.name}] validateManifest 应在 ${c.expect} 报 error,实际 error 路径 = [${implErrors.join(', ')}]`,
420
+ );
421
+ const sch = schemaPaths(c.value);
422
+ assert.ok(
423
+ sch.includes(c.expect),
424
+ `[${c.name}] schema 应在 ${c.expect} 拒绝,实际 schema 路径 = [${sch.join(', ')}]`,
425
+ );
426
+ }
427
+ });
428
+
429
+ test('R-02 schema 拒绝路径集合 = validateManifest error 路径集合(无单侧漂移)', () => {
430
+ for (const c of REJECT_CASES) {
431
+ assert.deepEqual(
432
+ schemaPaths(c.value), errorPaths(c.value),
433
+ `[${c.name}] schema 与 validateManifest 的拒绝路径集合必须完全一致`,
434
+ );
435
+ }
436
+ });
437
+
438
+ test('R-03 非法 manifest 经 parseManifest 抛错,且错误文案列出被拒字段(导入第一道闸)', () => {
439
+ for (const c of REJECT_CASES) {
440
+ if (typeof c.value === 'string') continue; // parseManifest 的入参是 JSON 文本
441
+ assert.throws(
442
+ () => parseManifest(JSON.stringify(c.value)),
443
+ /manifest\.json 无效/,
444
+ `[${c.name}] parseManifest 必须拒绝`,
445
+ );
446
+ }
447
+ assert.throws(() => parseManifest('{not json'), /manifest\.json 无效|JSON/);
448
+ });
449
+
450
+ /* ============================================================================
451
+ * C-*:已知兼容语义(不得被 schema 收紧)
452
+ * ==========================================================================*/
453
+
454
+ test('C-01 未知分区键:schema 接受,validateManifest 仅 warning(对齐 M-04,不得设计成拒绝)', () => {
455
+ const m = mutate({
456
+ sections: { ...sectionsAllFalse(), settings: true, keybindings: true, workflows: false },
457
+ });
458
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '未知分区键不得被 schema 拒绝');
459
+ assert.deepEqual(errorPaths(m), [], '未知分区键不得产生 error');
460
+ assert.deepEqual(warningPaths(m), ['sections.keybindings', 'sections.workflows'], '未知分区键应为 warning');
461
+ assert.doesNotThrow(() => parseManifest(JSON.stringify(m)), '未知分区键不得阻断解析(规格 §2.3)');
462
+ });
463
+
464
+ test('C-02 未知顶层字段与已知对象内未知子字段:保留、不报错(规格 §2.3 / SC-07)', () => {
465
+ const m = mutate({ 'x-future-manifest-key': { anything: [1, 2, 3] } });
466
+ (m['exporter'] as Record<string, unknown>)['futureExporterField'] = 'kept';
467
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '未知字段不得被 schema 拒绝');
468
+ assert.deepEqual(errorPaths(m), [], '未知字段不得产生 error');
469
+ const parsed = parseManifest(JSON.stringify(m)) as unknown as Record<string, unknown>;
470
+ assert.deepEqual(parsed['x-future-manifest-key'], { anything: [1, 2, 3] }, '顶层未知字段应被保留');
471
+ assert.equal((parsed['exporter'] as Record<string, unknown>)['futureExporterField'], 'kept', '子字段未知字段应被保留');
472
+ });
473
+
474
+ test('C-03 source.platform 不做枚举校验(实现只校验 typeof string,schema 不得更严)', () => {
475
+ const m = mutate({ source: { dshVersion: '0.1.0-rc.6', platform: 'plan9', arch: 'x64' } });
476
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '未列出的 platform 不得被 schema 拒绝');
477
+ assert.deepEqual(errorPaths(m), [], '实现同样接受任意字符串 platform(规格 §2.2 取证)');
478
+ // 而枚举本身作为文档事实保留在 x-platform-values
479
+ assert.ok((SCHEMA['x-platform-values'] as string[]).includes('win32'));
480
+ assert.ok((SCHEMA['x-platform-values'] as string[]).includes('other'));
481
+ });
482
+
483
+ test('C-04 security.encryption:对象或 null 均合法,其它标量两边都拒绝(不得单侧收紧)', () => {
484
+ for (const enc of [null, {}, { algorithm: 'aes-256-gcm' }]) {
485
+ const m = mutate({ security: { containsSecrets: false, encrypted: enc !== null, encryption: enc } });
486
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], `encryption=${JSON.stringify(enc)} 应合法`);
487
+ assert.deepEqual(errorPaths(m), [], `encryption=${JSON.stringify(enc)} 实现也应接受`);
488
+ }
489
+ for (const bad of [0, false, 'x']) {
490
+ const m = mutate({ security: { containsSecrets: false, encrypted: false, encryption: bad } });
491
+ assert.deepEqual(errorPaths(m), ['security.encryption'], `encryption=${JSON.stringify(bad)} 实现应拒绝`);
492
+ assert.deepEqual(schemaPaths(m), ['security.encryption'], `encryption=${JSON.stringify(bad)} schema 应拒绝`);
493
+ }
494
+ });
495
+
496
+ test('C-05 已记录的实现缺口:数组被 validateManifest 当作合法 encryption(typeof [] === "object")', () => {
497
+ const m = mutate({ security: { containsSecrets: false, encrypted: false, encryption: [] } });
498
+ assert.deepEqual(errorPaths(m), [], '实现接受数组——这是 src/schema/manifest.ts:118 的 typeof 判定缺口,不是本 schema 的契约');
499
+ assert.deepEqual(schemaPaths(m), ['security.encryption'], 'schema 按规格 §2.2/§4.4 拒绝数组(对象或 null)');
500
+ // 该缺口必须显式记录在 schema 的语义约束里,否则就是「文档说一套、代码做一套」的静默漂移
501
+ const notes = SCHEMA['x-semantic-constraints'] as string[];
502
+ assert.ok(notes.some((n) => n.includes('数组')), 'schema 必须显式记录该实现缺口');
503
+ });
504
+
505
+ test('C-06 已记录的语义落差:exportedAt 的 ISO-8601 可解析性无法用 JSON Schema 关键字表达', () => {
506
+ const m = mutate({ exportedAt: 'not-a-date' });
507
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], 'schema 只能校验 type=string(已知能力边界)');
508
+ assert.deepEqual(errorPaths(m), ['exportedAt'], '实现必须拒绝不可解析的时间');
509
+ // 该落差已写入 schema 的 x-semantic-constraints,属于显式记录而非静默漂移
510
+ const notes = SCHEMA['x-semantic-constraints'] as string[];
511
+ assert.ok(notes.some((n) => n.includes('exportedAt')), 'schema 必须显式记录该语义约束');
512
+ });
513
+
514
+ test('C-07 schemaVersion 的「是否受支持」不属结构校验:schemaVersion=2 两边都放行', () => {
515
+ const m = mutate({ schemaVersion: 2 });
516
+ assert.deepEqual(validateAgainstSchema(SCHEMA, m), [], '结构校验不判定版本支持性');
517
+ assert.deepEqual(errorPaths(m), [], '实现同样只在版本协商阶段拒绝(规格 §6.2)');
518
+ assert.doesNotThrow(() => parseManifest(JSON.stringify(m)), 'parseManifest 不做版本协商');
519
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * manifest.json 的类型、解析、序列化与结构校验。
3
+ * 类型本体在 types.ts(§13.1);本文件负责 manifest 的读写与结构完整性。
4
+ */
5
+ import { parseJsonSafe } from '../utils/json.ts';
6
+ import { CURRENT_SCHEMA_VERSION } from './versions.ts';
7
+ import type { Manifest, Platform, SectionId } from './types.ts';
8
+ import { SECTION_IDS } from './config.ts';
9
+
10
+ export const MANIFEST_FILE = 'manifest.json';
11
+ export const CHECKSUMS_FILE = 'integrity/checksums.json';
12
+ export const EXPORTER_NAME = 'DSH Config Manager';
13
+
14
+ export interface BuildManifestInput {
15
+ exporterVersion: string;
16
+ dshVersion: string;
17
+ platform: Platform;
18
+ arch: string;
19
+ sections: Record<SectionId, boolean>;
20
+ containsSecrets: boolean;
21
+ encrypted: boolean;
22
+ encryption: Manifest['security']['encryption'];
23
+ exportedAt?: string; // 测试可注入固定时间
24
+ }
25
+
26
+ /** 构造 manifest(schemaVersion 恒为当前版本,集中于此) */
27
+ export function buildManifest(input: BuildManifestInput): Manifest {
28
+ return {
29
+ schemaVersion: CURRENT_SCHEMA_VERSION,
30
+ exporter: { name: EXPORTER_NAME, version: input.exporterVersion },
31
+ source: {
32
+ dshVersion: input.dshVersion,
33
+ platform: input.platform,
34
+ arch: input.arch,
35
+ },
36
+ exportedAt: input.exportedAt ?? new Date().toISOString(),
37
+ sections: input.sections,
38
+ security: {
39
+ containsSecrets: input.containsSecrets,
40
+ encrypted: input.encrypted,
41
+ encryption: input.encryption,
42
+ },
43
+ };
44
+ }
45
+
46
+ /** 序列化(pretty JSON,供 ZIP 内 manifest.json) */
47
+ export function serializeManifest(manifest: Manifest): string {
48
+ return JSON.stringify(manifest, null, 2);
49
+ }
50
+
51
+ /** 解析 + 结构校验;非法输入抛错(导入第一道闸之一) */
52
+ export function parseManifest(raw: string): Manifest {
53
+ const parsed = parseJsonSafe(raw);
54
+ const issues = validateManifest(parsed);
55
+ const errors = issues.filter((i) => i.severity === 'error');
56
+ if (errors.length > 0) {
57
+ throw new Error(`manifest.json 无效: ${errors.map((e) => e.message).join('; ')}`);
58
+ }
59
+ return parsed as Manifest;
60
+ }
61
+
62
+ export interface ManifestIssue { path: string; message: string; severity: 'error' | 'warning'; }
63
+
64
+ /** 结构校验:字段类型 / sections 键集合 / security 形状 */
65
+ export function validateManifest(m: unknown): ManifestIssue[] {
66
+ const issues: ManifestIssue[] = [];
67
+ if (m === null || typeof m !== 'object') {
68
+ return [{ path: '$', message: 'manifest 必须是对象', severity: 'error' }];
69
+ }
70
+ const obj = m as Record<string, unknown>;
71
+
72
+ if (typeof obj['schemaVersion'] !== 'number') {
73
+ issues.push({ path: 'schemaVersion', message: 'schemaVersion 必须是数字', severity: 'error' });
74
+ }
75
+
76
+ const exporter = obj['exporter'] as Record<string, unknown> | undefined;
77
+ if (!exporter || typeof exporter !== 'object') {
78
+ issues.push({ path: 'exporter', message: 'exporter 缺失', severity: 'error' });
79
+ } else {
80
+ if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: 'exporter.name 必须是字符串', severity: 'error' });
81
+ if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: 'exporter.version 必须是字符串', severity: 'error' });
82
+ }
83
+
84
+ const source = obj['source'] as Record<string, unknown> | undefined;
85
+ if (!source || typeof source !== 'object') {
86
+ issues.push({ path: 'source', message: 'source 缺失', severity: 'error' });
87
+ } else {
88
+ if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: 'source.dshVersion 必须是字符串', severity: 'error' });
89
+ if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: 'source.platform 必须是字符串', severity: 'error' });
90
+ if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: 'source.arch 必须是字符串', severity: 'error' });
91
+ }
92
+
93
+ if (typeof obj['exportedAt'] !== 'string' || Number.isNaN(Date.parse(obj['exportedAt']))) {
94
+ issues.push({ path: 'exportedAt', message: 'exportedAt 必须是合法 ISO-8601 时间', severity: 'error' });
95
+ }
96
+
97
+ const sections = obj['sections'] as Record<string, unknown> | undefined;
98
+ if (!sections || typeof sections !== 'object') {
99
+ issues.push({ path: 'sections', message: 'sections 缺失', severity: 'error' });
100
+ } else {
101
+ for (const key of Object.keys(sections)) {
102
+ if (!(SECTION_IDS as readonly string[]).includes(key)) {
103
+ issues.push({ path: `sections.${key}`, message: `未知分区 "${key}"`, severity: 'warning' });
104
+ }
105
+ if (typeof sections[key] !== 'boolean') {
106
+ issues.push({ path: `sections.${key}`, message: `sections.${key} 必须是布尔`, severity: 'error' });
107
+ }
108
+ }
109
+ }
110
+
111
+ const security = obj['security'] as Record<string, unknown> | undefined;
112
+ if (!security || typeof security !== 'object') {
113
+ issues.push({ path: 'security', message: 'security 缺失', severity: 'error' });
114
+ } else {
115
+ if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: 'containsSecrets 必须是布尔', severity: 'error' });
116
+ if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: 'encrypted 必须是布尔', severity: 'error' });
117
+ const enc = security['encryption'];
118
+ if (enc !== null && (typeof enc !== 'object' || enc === undefined)) {
119
+ issues.push({ path: 'security.encryption', message: 'encryption 必须是对象或 null', severity: 'error' });
120
+ }
121
+ }
122
+
123
+ return issues;
124
+ }