akm-cli 0.9.0-rc.8 → 0.9.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 (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +73 -88
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +141 -87
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +105 -57
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +37 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +101 -6
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1806
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1234
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -223
  424. package/dist/migrate/legacy/content-migration.js +0 -305
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -72
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -841
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -0,0 +1,566 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { parse } from "node-html-parser";
5
+ import TurndownService from "turndown";
6
+ import { escapeMarkdownStructure } from "./fetcher-util.js";
7
+ /**
8
+ * Main-content extraction and HTML -> Markdown conversion for website snapshots.
9
+ *
10
+ * Snapshots are read back by agents as trusted knowledge, so anything a hostile
11
+ * page can smuggle into the markdown is a prompt-injection vector. Three
12
+ * invariants are load-bearing and each is enforced defensively:
13
+ *
14
+ * 1. `<script>` / `<style>` / `<noscript>` / `<template>` bodies never reach
15
+ * the markdown.
16
+ * 2. Only `http:` / `https:` URLs are emitted as links or images; anything
17
+ * else degrades to plain text.
18
+ * 3. A page cannot forge document structure — code fences are sized so their
19
+ * content cannot close them early.
20
+ */
21
+ const DANGEROUS_TAGS = ["script", "style", "noscript", "template"];
22
+ /**
23
+ * Scrub patterns for {@link scrubDangerousMarkup}, compiled once at module
24
+ * load rather than per call — this runs on every crawled page.
25
+ */
26
+ const DANGEROUS_TAG_PATTERNS = DANGEROUS_TAGS.map((tag) => ({
27
+ // A full block with any end-tag spelling a browser would accept.
28
+ full: new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/\\s*${tag}\\b[^>]*>`, "gi"),
29
+ // Unterminated open tag: everything to EOF is inside the raw-text element.
30
+ unterminated: new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*$`, "i"),
31
+ // Any stray leftover tag of this kind.
32
+ stray: new RegExp(`<\\/?\\s*${tag}\\b[^>]*>`, "gi"),
33
+ }));
34
+ const PAGE_CHROME_SELECTORS = ["nav", "header", "footer", "aside"];
35
+ /** Inform-parity classed chrome removed from whichever content region wins. */
36
+ const UNWANTED_CLASS_SELECTORS = [
37
+ ".nav",
38
+ ".navigation",
39
+ ".menu",
40
+ ".sidebar",
41
+ ".advertisement",
42
+ ".ad",
43
+ ".social",
44
+ ".share",
45
+ ".comments",
46
+ ".related",
47
+ ".breadcrumb",
48
+ ".breadcrumbs",
49
+ ".cookie-notice",
50
+ ".popup",
51
+ ".modal",
52
+ ".overlay",
53
+ ];
54
+ /** Content-region selectors in priority order; narrow regions beat app shells. */
55
+ const CONTENT_SELECTORS = [
56
+ ".markdown-body",
57
+ ".article-content",
58
+ ".entry-content",
59
+ ".post-content",
60
+ ".docs-content",
61
+ ".main-content",
62
+ "#docs-content",
63
+ "#main-content",
64
+ '[role="main"]',
65
+ "main",
66
+ "article",
67
+ "#content",
68
+ ".content",
69
+ ];
70
+ const ARTICLE_METADATA_SELECTORS = new Set([".article-content", ".entry-content", ".post-content"]);
71
+ /**
72
+ * Remove raw-text blocks textually, BEFORE the DOM parse.
73
+ *
74
+ * This is the security boundary, not `querySelectorAll("script").remove()`.
75
+ * `node-html-parser` only closes a raw-text element on an exactly-matching
76
+ * lowercase `</script>`; browsers additionally accept `</SCRIPT>`,
77
+ * `</script >`, `</script/>`, `</script foo>` and `</ script>`. On any of
78
+ * those the parser never creates a `script` element at all and serializes the
79
+ * script *body* as ordinary text — so DOM-level removal silently does nothing
80
+ * and the payload lands in the snapshot while the page renders normally in a
81
+ * browser. An unterminated `<script>` at EOF has the same effect.
82
+ */
83
+ function scrubDangerousMarkup(html) {
84
+ let out = html;
85
+ for (const pattern of DANGEROUS_TAG_PATTERNS) {
86
+ out = out.replace(pattern.full, " ").replace(pattern.unterminated, " ").replace(pattern.stray, " ");
87
+ }
88
+ return out;
89
+ }
90
+ function stripDangerousNodes(root) {
91
+ for (const tag of DANGEROUS_TAGS) {
92
+ for (const node of root.querySelectorAll(tag))
93
+ node.remove();
94
+ }
95
+ }
96
+ function isExplicitlyHidden(node) {
97
+ if (node.hasAttribute("hidden") || node.hasAttribute("inert"))
98
+ return true;
99
+ if (node.getAttribute("aria-hidden")?.toLowerCase() === "true")
100
+ return true;
101
+ const style = node.getAttribute("style")?.toLowerCase();
102
+ if (!style)
103
+ return false;
104
+ return style.split(";").some((declaration) => {
105
+ const [property, value] = declaration.split(":", 2).map((part) => part?.trim());
106
+ return ((property === "display" && value?.startsWith("none")) ||
107
+ (property === "visibility" && value?.startsWith("hidden")));
108
+ });
109
+ }
110
+ function isHiddenRegion(node) {
111
+ let current = node;
112
+ while (current) {
113
+ if (isExplicitlyHidden(current))
114
+ return true;
115
+ const parentNode = current.parentNode;
116
+ current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
117
+ }
118
+ return false;
119
+ }
120
+ function isInUnwantedRegion(node) {
121
+ let current = node;
122
+ while (current) {
123
+ const tagName = current.tagName.toLowerCase();
124
+ if (PAGE_CHROME_SELECTORS.some((selector) => selector === tagName))
125
+ return true;
126
+ const classes = new Set((current.getAttribute("class") ?? "").split(/\s+/).filter(Boolean));
127
+ if (UNWANTED_CLASS_SELECTORS.some((selector) => classes.has(selector.slice(1))))
128
+ return true;
129
+ const parentNode = current.parentNode;
130
+ current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
131
+ }
132
+ return false;
133
+ }
134
+ function stripUnwantedNodes(root, preserveSemanticChrome = false) {
135
+ for (const node of root.querySelectorAll("*")) {
136
+ if (isExplicitlyHidden(node))
137
+ node.remove();
138
+ }
139
+ for (const selector of PAGE_CHROME_SELECTORS) {
140
+ if (preserveSemanticChrome && selector !== "nav")
141
+ continue;
142
+ for (const node of root.querySelectorAll(selector)) {
143
+ node.remove();
144
+ }
145
+ }
146
+ for (const selector of UNWANTED_CLASS_SELECTORS) {
147
+ for (const node of root.querySelectorAll(selector))
148
+ node.remove();
149
+ }
150
+ }
151
+ function enclosingArticle(node) {
152
+ let current = node;
153
+ while (current) {
154
+ if (typeof current.tagName === "string" && current.tagName.toLowerCase() === "article")
155
+ return current;
156
+ const parentNode = current.parentNode;
157
+ current = parentNode && typeof parentNode.tagName === "string" ? parentNode : null;
158
+ }
159
+ return null;
160
+ }
161
+ function hasMatchingAncestor(candidate, matches) {
162
+ let current = candidate.parentNode;
163
+ while (current && typeof current.tagName === "string") {
164
+ if (matches.has(current))
165
+ return true;
166
+ current = current.parentNode;
167
+ }
168
+ return false;
169
+ }
170
+ /** True for the only link schemes we will emit into agent-facing markdown. */
171
+ export function isSafeLinkUrl(url) {
172
+ return url.protocol === "http:" || url.protocol === "https:";
173
+ }
174
+ /**
175
+ * Resolve an href/src for emission: absolute, http(s) only, credentials
176
+ * stripped. Returns null when the URL must not be emitted.
177
+ */
178
+ function resolveEmittableUrl(raw, pageUrl) {
179
+ try {
180
+ const resolved = new URL(raw, pageUrl);
181
+ if (!isSafeLinkUrl(resolved))
182
+ return null;
183
+ // `validateWebsiteUrl` rejects credentials on input URLs; do not let them
184
+ // re-enter through a link inside page content.
185
+ resolved.username = "";
186
+ resolved.password = "";
187
+ return resolved.toString();
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ /**
194
+ * Escape characters that would let attacker-controlled text break out of a
195
+ * markdown link/image and start a new one. `alt` and link labels are raw page
196
+ * strings; an alt of `x](javascript:alert(1))` otherwise emits a second,
197
+ * unvalidated destination ahead of the validated `src`.
198
+ */
199
+ function escapeMarkdownLabel(value) {
200
+ return value
201
+ .replace(/([\\[\]])/g, "\\$1")
202
+ .replace(/\s+/g, " ")
203
+ .trim();
204
+ }
205
+ /**
206
+ * Percent-encode parens in a link destination. Unescaped parens truncate the
207
+ * link and spill the remainder into the document as live markup. Encoding
208
+ * (rather than the `<...>` destination form) keeps the result free of `<`, so
209
+ * {@link escapeResidualMarkup} cannot corrupt it afterwards.
210
+ */
211
+ function markdownDestination(url) {
212
+ return url.replaceAll("(", "%28").replaceAll(")", "%29");
213
+ }
214
+ /**
215
+ * Guard against pathological nesting. `node-html-parser` and Turndown both
216
+ * recurse, and ~20k nested elements (well under the page byte cap) overflows
217
+ * the stack — which would otherwise abort an entire crawl.
218
+ */
219
+ const MAX_NESTING_DEPTH = 2_000;
220
+ function exceedsNestingBudget(html) {
221
+ let depth = 0;
222
+ let max = 0;
223
+ const tagPattern = /<(\/?)([a-zA-Z][\w:-]*)[^>]*?(\/?)>/g;
224
+ for (const match of html.matchAll(tagPattern)) {
225
+ const closing = match[1] === "/";
226
+ const selfClosing = match[3] === "/";
227
+ if (selfClosing)
228
+ continue;
229
+ if (closing)
230
+ depth = Math.max(0, depth - 1);
231
+ else {
232
+ depth += 1;
233
+ if (depth > max)
234
+ max = depth;
235
+ if (max > MAX_NESTING_DEPTH)
236
+ return true;
237
+ }
238
+ }
239
+ return false;
240
+ }
241
+ /** Last-resort conversion when the document is too deep to parse safely. */
242
+ function plainTextFallback(html) {
243
+ const text = scrubDangerousMarkup(html)
244
+ .replace(/<[^>]+>/g, " ")
245
+ .replace(/\s+/g, " ")
246
+ .trim();
247
+ return escapeMarkdownStructure(text.replace(/([\\[\]`])/g, "\\$1"));
248
+ }
249
+ /**
250
+ * Select the main-content region of a document and return its HTML.
251
+ *
252
+ * Falls back to `<body>` minus page chrome, then to the whole document, so a
253
+ * page with no semantic markup still yields content rather than nothing.
254
+ */
255
+ /**
256
+ * Select the content region from an already-parsed, already-scrubbed root
257
+ * (dangerous nodes removed). Mutates the root when it falls back to `<body>`.
258
+ */
259
+ function selectMainContentFromRoot(root) {
260
+ for (const selector of CONTENT_SELECTORS) {
261
+ const candidates = root
262
+ .querySelectorAll(selector)
263
+ .filter((match) => match.textContent.trim() && !isHiddenRegion(match) && !isInUnwantedRegion(match));
264
+ const candidateSet = new Set(candidates);
265
+ const matches = candidates.filter((match) => !hasMatchingAncestor(match, candidateSet));
266
+ // Repeated cards/articles usually form a listing. Let a broader primary
267
+ // container (or the body fallback) retain all of them instead of silently
268
+ // truncating the page to the first match.
269
+ if (matches.length > 1)
270
+ continue;
271
+ const match = matches[0];
272
+ // Ignore a region that matched structurally but carries no prose — a
273
+ // decorative wrapper should not shadow the real content below it.
274
+ if (match) {
275
+ const region = ARTICLE_METADATA_SELECTORS.has(selector) ? (enclosingArticle(match) ?? match) : match;
276
+ stripUnwantedNodes(region, true);
277
+ if (region.textContent.trim())
278
+ return region.toString();
279
+ }
280
+ }
281
+ const body = root.querySelector("body");
282
+ if (body) {
283
+ stripUnwantedNodes(body);
284
+ if (body.textContent.trim())
285
+ return body.toString();
286
+ }
287
+ stripUnwantedNodes(root);
288
+ return root.toString();
289
+ }
290
+ export function extractMainContentHtml(html) {
291
+ const root = parse(scrubDangerousMarkup(html), { comment: false });
292
+ stripDangerousNodes(root);
293
+ return selectMainContentFromRoot(root);
294
+ }
295
+ /** Read a fence language off `class="language-ts"` / `class="lang-ts"`. */
296
+ function fenceLanguage(className) {
297
+ return /(?:language|lang)-([\w+#-]+)/.exec(className)?.[1] ?? "";
298
+ }
299
+ /**
300
+ * Size a fence so its own content cannot close it. Turndown's default rule
301
+ * does this; the custom rule below must not regress it, or a page can end the
302
+ * fence early and inject forged headings and prose into the snapshot.
303
+ */
304
+ function fenceFor(code) {
305
+ let longest = 0;
306
+ for (const run of code.matchAll(/`+/g))
307
+ longest = Math.max(longest, run[0].length);
308
+ return "`".repeat(Math.max(3, longest + 1));
309
+ }
310
+ function createTurndown(pageUrl) {
311
+ const service = new TurndownService({
312
+ headingStyle: "atx",
313
+ codeBlockStyle: "fenced",
314
+ emDelimiter: "_",
315
+ bulletListMarker: "-",
316
+ });
317
+ // Defense in depth: these are already scrubbed textually and removed from
318
+ // the DOM. A caller converting raw HTML must not be able to leak them either.
319
+ service.remove([...DANGEROUS_TAGS]);
320
+ service.addRule("fencedCodeBlock", {
321
+ filter: (node) => node.nodeName === "PRE" && Boolean(node.firstChild) && node.firstChild?.nodeName === "CODE",
322
+ replacement: (_content, node) => {
323
+ const code = node.firstChild;
324
+ const language = fenceLanguage(code?.getAttribute?.("class") ?? "");
325
+ const text = (code?.textContent ?? "").replace(/\n+$/, "");
326
+ if (!text.trim())
327
+ return "\n\n";
328
+ const fence = fenceFor(text);
329
+ return `\n\n${fence}${language}\n${text}\n${fence}\n\n`;
330
+ },
331
+ });
332
+ service.addRule("safeLink", {
333
+ filter: (node) => node.nodeName === "A",
334
+ replacement: (content, node) => {
335
+ const label = content.trim();
336
+ if (!label)
337
+ return "";
338
+ const href = node.getAttribute("href");
339
+ if (!href)
340
+ return label;
341
+ const resolved = resolveEmittableUrl(href, pageUrl);
342
+ if (!resolved)
343
+ return label;
344
+ return `[${escapeMarkdownLabel(label)}](${markdownDestination(resolved)})`;
345
+ },
346
+ });
347
+ // Turndown's built-in image rule emits `src` verbatim — no scheme check and
348
+ // no base resolution — so `javascript:` / `data:` srcs would slip past the
349
+ // anchor policy entirely.
350
+ service.addRule("safeImage", {
351
+ filter: (node) => node.nodeName === "IMG",
352
+ replacement: (_content, node) => {
353
+ const element = node;
354
+ const alt = escapeMarkdownLabel(element.getAttribute("alt") ?? "");
355
+ const src = element.getAttribute("src");
356
+ if (!src)
357
+ return alt;
358
+ const resolved = resolveEmittableUrl(src, pageUrl);
359
+ return resolved ? `![${alt}](${markdownDestination(resolved)})` : alt;
360
+ },
361
+ });
362
+ const tableSupport = new WeakMap();
363
+ const tableFor = (node) => {
364
+ let current = node;
365
+ while (current) {
366
+ if (current.nodeName === "TABLE")
367
+ return current;
368
+ current = current.parentNode;
369
+ }
370
+ return null;
371
+ };
372
+ const cellsFor = (row) => Array.from(row.childNodes).filter((child) => child.nodeName === "TH" || child.nodeName === "TD");
373
+ const hasUnsupportedSpan = (cell) => {
374
+ const colspan = cell.getAttribute("colspan");
375
+ const rowspan = cell.getAttribute("rowspan");
376
+ return (colspan !== null && colspan !== "1") || (rowspan !== null && rowspan !== "1");
377
+ };
378
+ const isSupportedTable = (table) => {
379
+ const cached = tableSupport.get(table);
380
+ if (cached !== undefined)
381
+ return cached;
382
+ const hasAncestorTable = table.parentNode !== null && tableFor(table.parentNode) !== null;
383
+ let ancestor = table.parentNode;
384
+ while (ancestor) {
385
+ if (ancestor.nodeName === "TABLE") {
386
+ tableSupport.set(table, false);
387
+ return false;
388
+ }
389
+ ancestor = ancestor.parentNode;
390
+ }
391
+ const rows = [];
392
+ let hasCaption = false;
393
+ let hasNestedTable = false;
394
+ const visit = (node) => {
395
+ for (const child of Array.from(node.childNodes)) {
396
+ if (child.nodeName === "TABLE") {
397
+ hasNestedTable = true;
398
+ continue;
399
+ }
400
+ if (child.nodeName === "CAPTION")
401
+ hasCaption = true;
402
+ if (child.nodeName === "TR")
403
+ rows.push(child);
404
+ visit(child);
405
+ }
406
+ };
407
+ visit(table);
408
+ const firstCells = rows[0] ? cellsFor(rows[0]) : [];
409
+ const supported = rows.length > 0 &&
410
+ !hasAncestorTable &&
411
+ !hasCaption &&
412
+ !hasNestedTable &&
413
+ firstCells.length > 0 &&
414
+ firstCells.every((cell) => cell.nodeName === "TH" && !hasUnsupportedSpan(cell)) &&
415
+ rows.slice(1).every((row) => {
416
+ const cells = cellsFor(row);
417
+ return (cells.length === firstCells.length &&
418
+ cells.every((cell) => cell.nodeName === "TD" && !hasUnsupportedSpan(cell)));
419
+ });
420
+ tableSupport.set(table, supported);
421
+ return supported;
422
+ };
423
+ const isInSupportedTable = (node) => {
424
+ const table = tableFor(node);
425
+ return table !== null && isSupportedTable(table);
426
+ };
427
+ service.addRule("tableCell", {
428
+ filter: (node) => (node.nodeName === "TH" || node.nodeName === "TD") && isInSupportedTable(node),
429
+ replacement: (content) => {
430
+ const cell = content
431
+ .replace(/\|/g, "\\|")
432
+ .replace(/\r?\n+/g, " ")
433
+ .trim();
434
+ return ` ${cell} |`;
435
+ },
436
+ });
437
+ service.addRule("tableRow", {
438
+ filter: (node) => node.nodeName === "TR" && isInSupportedTable(node),
439
+ replacement: (content, node) => {
440
+ const cells = Array.from(node.childNodes).filter((child) => child.nodeName === "TH" || child.nodeName === "TD");
441
+ if (cells.length === 0)
442
+ return "";
443
+ const row = `|${content.trimEnd()}\n`;
444
+ const isHeader = cells.every((cell) => cell.nodeName === "TH");
445
+ return isHeader ? `${row}|${cells.map(() => " --- |").join("")}\n` : row;
446
+ },
447
+ });
448
+ service.addRule("table", {
449
+ filter: (node) => node.nodeName === "TABLE" && isInSupportedTable(node),
450
+ replacement: (content) => `\n\n${content.trim().replace(/\n\s*\n/g, "\n")}\n\n`,
451
+ });
452
+ return service;
453
+ }
454
+ /**
455
+ * Escape markup that survives conversion as literal text.
456
+ *
457
+ * Turndown does not escape `<`, so page text written as `&lt;script&gt;`
458
+ * (harmless, visible text on the page) would otherwise become a live
459
+ * `<script>` tag in the markdown and execute in any renderer that passes raw
460
+ * HTML through.
461
+ */
462
+ function escapeResidualMarkup(markdown) {
463
+ // Only `<` that begins a tag-like construct; a bare `a < b` stays readable.
464
+ return markdown.replace(/<(?=[a-zA-Z/!?])/g, "&lt;");
465
+ }
466
+ /** Escape residual markup, then normalize whitespace, into the final snapshot. */
467
+ function finalizeMarkdown(markdown) {
468
+ return escapeResidualMarkup(markdown)
469
+ .replace(/\r/g, "")
470
+ .replace(/[ \t]+\n/g, "\n")
471
+ .replace(/\n{3,}/g, "\n\n")
472
+ .trim();
473
+ }
474
+ /** Turn an already-scrubbed root into snapshot Markdown (mutates the root). */
475
+ function markdownFromRoot(root, html, pageUrl) {
476
+ if (exceedsNestingBudget(html))
477
+ return plainTextFallback(html);
478
+ try {
479
+ stripDangerousNodes(root);
480
+ return createTurndown(pageUrl).turndown(selectMainContentFromRoot(root));
481
+ }
482
+ catch {
483
+ // Depth budget is a heuristic; a parser or converter blow-up must
484
+ // degrade this page, never abort the surrounding crawl.
485
+ return plainTextFallback(html);
486
+ }
487
+ }
488
+ /**
489
+ * Collect http(s) links from an already-parsed, already-scrubbed root. Read
490
+ * BEFORE any content-region mutation so nav/header/footer links survive.
491
+ */
492
+ function collectLinksFromRoot(root, pageUrl) {
493
+ const links = [];
494
+ for (const anchor of root.querySelectorAll("a")) {
495
+ const href = anchor.getAttribute("href")?.trim();
496
+ if (!href || href.startsWith("#"))
497
+ continue;
498
+ try {
499
+ const resolved = new URL(href, pageUrl);
500
+ if (isSafeLinkUrl(resolved))
501
+ links.push(resolved);
502
+ }
503
+ catch {
504
+ /* ignore malformed hrefs */
505
+ }
506
+ }
507
+ return links;
508
+ }
509
+ /**
510
+ * Convert a page to Markdown, scoped to its main-content region.
511
+ *
512
+ * `pageUrl` resolves relative hrefs; it is not fetched.
513
+ */
514
+ export function htmlToMarkdown(html, pageUrl) {
515
+ let markdown;
516
+ if (exceedsNestingBudget(html)) {
517
+ markdown = plainTextFallback(html);
518
+ }
519
+ else {
520
+ try {
521
+ markdown = createTurndown(pageUrl).turndown(extractMainContentHtml(html));
522
+ }
523
+ catch {
524
+ markdown = plainTextFallback(html);
525
+ }
526
+ }
527
+ return finalizeMarkdown(markdown);
528
+ }
529
+ /**
530
+ * Crawl-path entry point: convert a page AND collect its links from a SINGLE
531
+ * parse. The content region and the whole-document link set both derive from
532
+ * the same DOM, so a crawled page is scrubbed and parsed once, not twice.
533
+ *
534
+ * Links are collected from the whole document (nav/header/footer included —
535
+ * that is how a crawl discovers pages) and read before the content-region
536
+ * selection mutates the tree.
537
+ */
538
+ export function htmlToMarkdownAndLinks(html, pageUrl) {
539
+ let root = null;
540
+ try {
541
+ root = parse(scrubDangerousMarkup(html), { comment: false });
542
+ }
543
+ catch {
544
+ root = null;
545
+ }
546
+ const links = root ? collectLinksFromRoot(root, pageUrl) : [];
547
+ const markdown = root ? markdownFromRoot(root, html, pageUrl) : plainTextFallback(html);
548
+ return { markdown: finalizeMarkdown(markdown), links };
549
+ }
550
+ /**
551
+ * Collect links from the WHOLE document, not the extracted content region.
552
+ *
553
+ * Deliberate and load-bearing: nav/header/footer links are how a crawl
554
+ * discovers pages. Narrowing this to the content region would silently shrink
555
+ * every crawl to whatever the first page happens to link inline.
556
+ */
557
+ export function extractDocumentLinks(html, pageUrl) {
558
+ let root;
559
+ try {
560
+ root = parse(scrubDangerousMarkup(html), { comment: false });
561
+ }
562
+ catch {
563
+ return [];
564
+ }
565
+ return collectLinksFromRoot(root, pageUrl);
566
+ }
@@ -0,0 +1,41 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Small shared helpers for the snapshot fetchers.
6
+ *
7
+ * These live in a leaf module (no fetcher or `website-ingest` imports) so the
8
+ * individual fetchers can share them without cycling through the fetcher
9
+ * registry — the same reason the SSRF guards were pulled into `host-guard.ts`.
10
+ */
11
+ /**
12
+ * Neutralize markdown structure in attacker-controlled prose. Without this a
13
+ * post/tweet body containing a line starting with `##` forges a section
14
+ * boundary in the snapshot, letting it impersonate content the fetcher
15
+ * vouched for.
16
+ */
17
+ export function escapeMarkdownStructure(value) {
18
+ return value
19
+ .replace(/\r\n?/g, "\n")
20
+ .split("\n")
21
+ .map((line) => line.replace(/^(\s*)([#>\-*+=~_`]|\d+[.)])/, "$1\\$2"))
22
+ .join("\n");
23
+ }
24
+ /** Coerce an unknown JSON value to a string, defaulting to "". */
25
+ export function coerceString(value) {
26
+ return typeof value === "string" ? value : "";
27
+ }
28
+ /**
29
+ * `index` and `log` are OKF reserved structural basenames at every depth — an
30
+ * adapter never indexes them, so an asset written there imports but can never
31
+ * be found by search or show. Remap the trailing segment (`x/index` ->
32
+ * `x/index-content`). Idempotent: a `-content`-suffixed name is left alone.
33
+ */
34
+ export function avoidReservedBasename(relPath) {
35
+ const segments = relPath.split("/");
36
+ const last = segments[segments.length - 1] ?? "";
37
+ if (last === "index" || last === "log") {
38
+ segments[segments.length - 1] = `${last}-content`;
39
+ }
40
+ return segments.join("/");
41
+ }