@inneranimalmedia/agentsam-sdk 2.6.5 → 2.6.8

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 (400) hide show
  1. package/AGENTSAM.md +29 -0
  2. package/README.md +37 -0
  3. package/apps/README.md +5 -0
  4. package/apps/cad-creator/README.md +4 -4
  5. package/apps/cad-creator/shared/cad/package.json +1 -1
  6. package/apps/client-cms-editor/AGENTS.md +12 -7
  7. package/apps/client-cms-editor/README.md +76 -48
  8. package/apps/client-cms-editor/agentsam.app.json +26 -9
  9. package/apps/client-cms-editor/backend/package.json +20 -3
  10. package/apps/client-cms-editor/backend/src/api/client.ts +134 -101
  11. package/apps/client-cms-editor/backend/src/index.ts +1 -1
  12. package/apps/client-cms-editor/backend/src/model.ts +11 -115
  13. package/apps/client-cms-editor/backend/src/preview/bridge.ts +1 -1
  14. package/apps/client-cms-editor/backend/tsconfig.json +6 -1
  15. package/apps/client-cms-editor/bin/agentsam-cms.js +262 -0
  16. package/apps/client-cms-editor/bin/agentsam-cms.mjs +2 -228
  17. package/apps/client-cms-editor/frontend/package.json +16 -7
  18. package/apps/client-cms-editor/frontend/src/AgentSamDrawer.tsx +9 -41
  19. package/apps/client-cms-editor/frontend/src/CmsAgentSurface.tsx +19 -174
  20. package/apps/client-cms-editor/frontend/src/adapters/heuristic.ts +29 -0
  21. package/apps/client-cms-editor/frontend/src/adapters/http.ts +628 -0
  22. package/apps/client-cms-editor/frontend/src/adapters/memory.ts +510 -0
  23. package/apps/client-cms-editor/frontend/src/editor/CmsEditor.tsx +213 -0
  24. package/apps/client-cms-editor/frontend/src/editor/CmsEditorProvider.tsx +20 -0
  25. package/apps/client-cms-editor/frontend/src/editor/canvas/PreviewCanvas.tsx +53 -0
  26. package/apps/client-cms-editor/frontend/src/{CmsEditor.tsx → editor/legacy/CmsEditorPrototype.tsx} +52 -23
  27. package/apps/client-cms-editor/frontend/src/editor/useCmsEditorController.ts +218 -0
  28. package/apps/client-cms-editor/frontend/src/index.ts +19 -5
  29. package/apps/client-cms-editor/frontend/src/lib/agent-host.ts +24 -0
  30. package/apps/client-cms-editor/frontend/src/main.tsx +82 -8
  31. package/apps/client-cms-editor/frontend/src/mount.tsx +92 -30
  32. package/apps/client-cms-editor/frontend/src/styles/agentsam-drawer.css +1 -1
  33. package/apps/client-cms-editor/frontend/src/styles/cmsShell.css +8 -8
  34. package/apps/client-cms-editor/frontend/src/styles/studio.css +55 -3
  35. package/apps/client-cms-editor/frontend/tsconfig.json +12 -1
  36. package/apps/client-cms-editor/frontend/vite.config.ts +12 -0
  37. package/apps/client-cms-editor/package-lock.json +973 -134
  38. package/apps/client-cms-editor/package.json +118 -6
  39. package/apps/client-cms-editor/reference/harvest/HARVEST_RECEIPT.json +201 -0
  40. package/apps/client-cms-editor/reference/harvest/PROVENANCE_HISTORY.txt +10 -0
  41. package/apps/client-cms-editor/reference/harvest/README.md +8 -0
  42. package/apps/client-cms-editor/reference/harvest/plans/ARCHIVE-INTAKE.md +135 -0
  43. package/apps/client-cms-editor/reference/harvest/plans/CHATGPT-IDENTITY-HARVEST.md +83 -0
  44. package/apps/client-cms-editor/reference/harvest/plans/CMS-EDITOR-HARVEST.md +166 -0
  45. package/apps/client-cms-editor/reference/harvest/plans/HARVEST-INDEX.md +196 -0
  46. package/apps/client-cms-editor/reference/harvest/plans/INNERANIMALS-CONTENT-001.md +137 -0
  47. package/apps/client-cms-editor/reference/harvest/plans/NO-IMPLICIT-SIDE-EFFECTS.md +87 -0
  48. package/apps/client-cms-editor/reference/harvest/plans/STOREFRONT-SURFACE-MAP.md +140 -0
  49. package/apps/client-cms-editor/reference/harvest/plans/THEME-INNERANIMALS-HARVEST.md +137 -0
  50. package/apps/client-cms-editor/reference/harvest/plans/TOOLING-LAB.md +63 -0
  51. package/apps/client-cms-editor/reference/harvest/plans/harvest-manifest.json +97 -0
  52. package/apps/client-cms-editor/reference/harvest/plans/local-cms-harvest-schema.sql +328 -0
  53. package/apps/client-cms-editor/shared/cms/package.json +17 -7
  54. package/apps/client-cms-editor/shared/cms/src/adapter.ts +204 -0
  55. package/apps/client-cms-editor/shared/cms/src/agent-context.ts +7 -5
  56. package/apps/client-cms-editor/shared/cms/src/editor-types.ts +32 -0
  57. package/apps/client-cms-editor/shared/cms/src/host.ts +82 -0
  58. package/apps/client-cms-editor/shared/cms/src/index.ts +4 -0
  59. package/apps/client-cms-editor/shared/cms/src/map.ts +115 -0
  60. package/apps/client-cms-editor/shared/cms/src/starter-pack.ts +158 -0
  61. package/contracts/source-types.v1.json +353 -0
  62. package/docs/CMS_CONTEXTUAL_COMPOSER.md +1 -1
  63. package/docs/PRODUCT_LIFECYCLE.md +566 -0
  64. package/docs/REPO_FILE_MAP.txt +132 -0
  65. package/docs/THEME_REFINERY.md +2 -2
  66. package/docs/architecture/AGENTSAM_RUNTIME_PROTOCOL.md +40 -20
  67. package/docs/content-studio/REVISION_GATE_2026-09-27.md +1 -1
  68. package/docs/plans/LOCAL_STUDIO_DESKTOP_RELEASE_2026-09-29.md +150 -0
  69. package/migrations/d1/0017_identity_native_oauth.sql +21 -0
  70. package/migrations/d1/0018_goap_control_plane_remaster.sql +40 -0
  71. package/migrations/d1/0019_agentsam_agent_run_plan_todo.sql +14 -0
  72. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +20 -9
  73. package/migrations/runtime/0002_goap_blackboard_v1.sql +15 -0
  74. package/migrations/runtime/0004_agent_run_plan_linkage.sql +8 -0
  75. package/native/agentsam-machine/Cargo.lock +426 -0
  76. package/native/agentsam-machine/Cargo.toml +9 -0
  77. package/native/agentsam-machine/cli/Cargo.toml +21 -0
  78. package/native/agentsam-machine/cli/README.md +21 -0
  79. package/native/agentsam-machine/cli/src/main.rs +77 -0
  80. package/native/agentsam-machine/core/Cargo.toml +22 -0
  81. package/native/agentsam-machine/core/README.md +14 -0
  82. package/native/agentsam-machine/core/build.rs +33 -0
  83. package/native/agentsam-machine/core/contracts/source-types.v1.json +353 -0
  84. package/native/agentsam-machine/core/src/assets.rs +790 -0
  85. package/native/agentsam-machine/core/src/bound.rs +298 -0
  86. package/native/agentsam-machine/core/src/enrich.rs +706 -0
  87. package/native/agentsam-machine/core/src/frontend.rs +442 -0
  88. package/native/agentsam-machine/core/src/lib.rs +1092 -0
  89. package/native/agentsam-machine/core/src/liquid.rs +528 -0
  90. package/native/agentsam-machine/core/src/source_types.rs +78 -0
  91. package/package.json +64 -24
  92. package/packages/agentsam-brand/README.md +1 -1
  93. package/packages/agentsam-brand/package.json +4 -4
  94. package/packages/agentsam-brand/src/index.js +1 -1
  95. package/packages/agentsam-contracts/package.json +13 -3
  96. package/packages/agentsam-contracts/src/tools.ts +22 -0
  97. package/packages/agentsam-errors/package.json +13 -3
  98. package/packages/agentsam-errors/src/index.js +1 -0
  99. package/packages/agentsam-errors/src/tool.js +56 -0
  100. package/packages/agentsam-errors/test/tool.test.mjs +58 -0
  101. package/packages/agentsam-goap/README.md +52 -0
  102. package/packages/agentsam-goap/package.json +37 -0
  103. package/packages/agentsam-goap/src/adapters/d1-sqlite.js +570 -0
  104. package/packages/agentsam-goap/src/adapters/memory.js +199 -0
  105. package/packages/agentsam-goap/src/contracts.js +226 -0
  106. package/packages/agentsam-goap/src/control-plane.js +112 -0
  107. package/packages/agentsam-goap/src/errors.js +157 -0
  108. package/packages/agentsam-goap/src/index.js +5 -0
  109. package/packages/agentsam-goap/src/world.js +202 -0
  110. package/packages/agentsam-goap/test/goap-control-plane.test.mjs +245 -0
  111. package/packages/agentsam-goap/test/world.test.mjs +79 -0
  112. package/packages/agentsam-knowledge/package.json +18 -4
  113. package/packages/agentsam-knowledge/src/autorag/index.js +10 -3
  114. package/packages/agentsam-knowledge/src/backends/index.js +33 -1
  115. package/packages/agentsam-knowledge/test/autorag.test.mjs +60 -1
  116. package/packages/agentsam-nav/dist/browser.global.js +21 -18
  117. package/packages/agentsam-nav/dist/browser.js +6807 -130
  118. package/packages/agentsam-nav/dist/index.js +6799 -122
  119. package/packages/agentsam-nav/package.json +24 -6
  120. package/packages/agentsam-queue-control/README.md +1 -1
  121. package/packages/agentsam-queue-control/package.json +7 -2
  122. package/packages/agentsam-rapid-rust/src/main.rs +1 -1
  123. package/packages/agentsam-repository/package.json +13 -3
  124. package/packages/agentsam-repository/src/goap.js +13 -14
  125. package/packages/agentsam-repository/src/source-types.js +8 -0
  126. package/packages/agentsam-repository/src/tickets.js +15 -22
  127. package/packages/agentsam-repository/test/goap-tickets-workspace-state.test.mjs +19 -8
  128. package/packages/agentsam-vault/package.json +14 -4
  129. package/packages/agentsam-workbench/package.json +15 -4
  130. package/packages/agentsam-workbench/src/agent/ToolPermissionRequest.tsx +94 -0
  131. package/packages/agentsam-workbench/src/agent/index.ts +2 -0
  132. package/packages/agentsam-workbench/src/agent/tool-permission.css +134 -0
  133. package/packages/connectors/cloudflare/package.json +15 -3
  134. package/packages/connectors/cloudflare/src/api-client.js +3 -2
  135. package/packages/connectors/cloudflare/src/families/vectorize.js +127 -0
  136. package/packages/connectors/cloudflare/src/index.js +1 -0
  137. package/packages/heuristic-theme/README.md +2 -2
  138. package/packages/heuristic-theme/package.json +8 -3
  139. package/packages/heuristic-theme/src/registry.js +2 -2
  140. package/packages/heuristic-theme/src/theme.js +1 -1
  141. package/packages/heuristic-theme/test/theme.test.mjs +1 -1
  142. package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
  143. package/packages/identity/.agentsam/features/oauth-login-portal/providers.json +27 -7
  144. package/packages/identity/README.md +2 -2
  145. package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +18 -15
  146. package/packages/identity/migrations/apply-portable.js +109 -0
  147. package/packages/identity/migrations/sqlite/004_identity_oauth_consent_catalog.sql +1 -1
  148. package/packages/identity/migrations/sqlite/005_identity_company_native.sql +52 -0
  149. package/packages/identity/package.json +27 -6
  150. package/packages/identity/schema/agentsam.identity/manifest.json +101 -0
  151. package/packages/identity/scripts/local-identity-bridge.mjs +13 -7
  152. package/packages/identity/scripts/preview-auth-portal.mjs +6 -6
  153. package/packages/identity/src/adapters/cloudflare-d1/index.js +8 -401
  154. package/packages/identity/src/adapters/iam-compat/index.js +540 -0
  155. package/packages/identity/src/adapters/portable-d1/index.js +18 -0
  156. package/packages/identity/src/adapters/portable-store.js +479 -0
  157. package/packages/identity/src/adapters/sqlite/index.js +13 -312
  158. package/packages/identity/src/contracts/auth-config.js +0 -1
  159. package/packages/identity/src/contracts/identity-store.js +56 -5
  160. package/packages/identity/src/contracts/provider.js +1 -0
  161. package/packages/identity/src/contracts/sign-in-options.js +104 -0
  162. package/packages/identity/src/core/constants.js +1 -1
  163. package/packages/identity/src/core/session-policy.js +31 -0
  164. package/packages/identity/src/frontend/auth-portal/pages/login.html +6 -6
  165. package/packages/identity/src/frontend/auth-portal/pages/reset.html +5 -5
  166. package/packages/identity/src/frontend/auth-portal/pages/signup.html +5 -5
  167. package/packages/identity/src/frontend/auth-portal/shared/company-branding.js +1 -1
  168. package/packages/identity/src/frontend/auth-portal/shared/desktop-identity-bridge.js +6 -37
  169. package/packages/identity/src/index.js +28 -1
  170. package/packages/identity/src/migrations/README.md +23 -23
  171. package/packages/identity/src/oauth/README.md +1 -1
  172. package/packages/identity/src/oauth/credentials.js +10 -11
  173. package/packages/identity/src/oauth/google-desktop-exchange.js +195 -27
  174. package/packages/identity/src/oauth/iam-platform.js +21 -9
  175. package/packages/identity/src/oauth/native-handoff.js +136 -0
  176. package/packages/identity/src/providers/chatgpt/headers.js +65 -0
  177. package/packages/identity/src/providers/chatgpt/index.js +86 -0
  178. package/packages/identity/src/providers/chatgpt/mapper.js +43 -0
  179. package/packages/identity/src/providers/chatgpt/paths.js +45 -0
  180. package/packages/identity/src/providers/github/oauth.js +1 -2
  181. package/packages/identity/src/providers/github/profile.js +16 -2
  182. package/packages/identity/src/providers/index.js +11 -3
  183. package/packages/identity/src/server/identity-service.js +2 -1
  184. package/packages/identity/src/server/worker-router.js +71 -18
  185. package/packages/identity/tests/chatgpt-provider.test.mjs +75 -0
  186. package/packages/identity/tests/google-desktop-identity-login.test.mjs +210 -0
  187. package/packages/identity/tests/identity-init-scaffold.test.mjs +15 -4
  188. package/packages/identity/tests/lane1-identity-store-parity.test.mjs +527 -0
  189. package/packages/identity/tests/lane1-packed-consumer.test.mjs +175 -0
  190. package/packages/identity/tests/native-oauth-handoff.test.mjs +292 -0
  191. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  192. package/packages/identity/tests/preview-auth-portal.test.mjs +4 -2
  193. package/packages/identity/tests/session-routes-policy.test.mjs +13 -1
  194. package/packages/identity/tests/worker-runtime-boundary.test.mjs +11 -0
  195. package/packages/providers/completeful/package.json +7 -2
  196. package/packages/theme-church-site/package.json +17 -6
  197. package/packages/theme-church-site/src/harvest.json +4 -2
  198. package/packages/theme-church-site/src/index.js +7 -3
  199. package/packages/theme-church-site/test/theme.test.mjs +4 -2
  200. package/packages/theme-companions-site/package.json +17 -6
  201. package/packages/theme-companions-site/src/harvest.json +4 -2
  202. package/packages/theme-companions-site/src/index.js +7 -3
  203. package/packages/theme-companions-site/test/theme.test.mjs +4 -2
  204. package/packages/theme-floors-site/package.json +17 -6
  205. package/packages/theme-floors-site/src/harvest.json +4 -2
  206. package/packages/theme-floors-site/src/index.js +7 -3
  207. package/packages/theme-floors-site/test/theme.test.mjs +4 -2
  208. package/packages/theme-fuelnfree-site/package.json +17 -6
  209. package/packages/theme-fuelnfree-site/src/harvest.json +4 -2
  210. package/packages/theme-fuelnfree-site/src/index.js +7 -3
  211. package/packages/theme-fuelnfree-site/test/theme.test.mjs +4 -2
  212. package/packages/theme-handyman-site/package.json +17 -6
  213. package/packages/theme-handyman-site/src/harvest.json +4 -2
  214. package/packages/theme-handyman-site/src/index.js +7 -3
  215. package/packages/theme-handyman-site/test/theme.test.mjs +4 -2
  216. package/packages/theme-insurance-site/package.json +17 -6
  217. package/packages/theme-insurance-site/src/harvest.json +4 -2
  218. package/packages/theme-insurance-site/src/index.js +7 -3
  219. package/packages/theme-insurance-site/test/theme.test.mjs +4 -2
  220. package/packages/theme-scenes/README.md +1 -1
  221. package/packages/theme-scenes/package.json +4 -3
  222. package/packages/theme-scenes/src/registry.js +187 -0
  223. package/packages/theme-shinshu-site/package.json +17 -6
  224. package/packages/theme-shinshu-site/src/harvest.json +4 -2
  225. package/packages/theme-shinshu-site/src/index.js +7 -3
  226. package/packages/theme-shinshu-site/test/theme.test.mjs +4 -2
  227. package/packages/work-graph/package.json +14 -4
  228. package/protocol/FILEMETA_V1.md +2 -2
  229. package/protocol/capabilities/manifest.json +1 -1
  230. package/protocol/database/agentsam.schema-pack.v1.schema.json +72 -0
  231. package/protocol/presets/catalog.json +2 -2
  232. package/protocol/theme-refinery/lineage-registry.json +105 -0
  233. package/protocol/theme-refinery/product-metadata.schema.json +21 -1
  234. package/python/agentsam_sdk/repository/intelligence/discovery.py +27 -2
  235. package/python/agentsam_sdk/repository/intelligence/git.py +4 -0
  236. package/python/tests/test_repository_intelligence.py +30 -0
  237. package/src/cli/command-catalog.js +48 -5
  238. package/src/cli/dispatch.js +1 -0
  239. package/src/cli.js +20 -1
  240. package/src/cloudflare/runtime-status.js +2 -1
  241. package/src/commands/autorag.js +67 -13
  242. package/src/commands/cheat-sheet.js +29 -4
  243. package/src/commands/codebaseindex.js +45 -2
  244. package/src/commands/goap-world.js +134 -0
  245. package/src/commands/goap.js +11 -4
  246. package/src/commands/knowledge.js +59 -2
  247. package/src/commands/machine-binary.js +109 -0
  248. package/src/commands/machine-runtime.js +266 -0
  249. package/src/commands/machine.js +208 -0
  250. package/src/commands/package.js +530 -0
  251. package/src/commands/plan-ledger.js +370 -0
  252. package/src/commands/plan.js +183 -0
  253. package/src/commands/security.js +67 -4
  254. package/src/commands/setup.js +33 -16
  255. package/src/commands/skill.js +78 -17
  256. package/src/commands/status.js +8 -1
  257. package/src/features/resolve.js +1 -0
  258. package/src/knowledge/engine.js +21 -2
  259. package/src/knowledge/runtime-discovery.js +127 -0
  260. package/src/knowledge/source.js +1 -1
  261. package/src/lib/identity-scaffold.js +53 -13
  262. package/src/lib/setup/recipes.js +1 -1
  263. package/src/lib/slash-commands.js +1 -1
  264. package/src/local/runtime-store.js +24 -2
  265. package/src/local/sqlite.js +17 -0
  266. package/src/models/discovery.js +40 -22
  267. package/src/models/inventory-core.d.ts +105 -0
  268. package/src/models/inventory-core.js +54 -6
  269. package/src/sam/operations/package-audit.js +41 -0
  270. package/src/sam/seed.js +2 -0
  271. package/src/security/logs.js +3 -0
  272. package/src/ui/cli/help.js +41 -0
  273. package/apps/client-cms-editor/backend/src/demo-bootstrap.ts +0 -145
  274. package/test/account-session.test.mjs +0 -101
  275. package/test/agent-capabilities.test.mjs +0 -67
  276. package/test/app-authority-context.test.mjs +0 -32
  277. package/test/app-building-skills.test.mjs +0 -61
  278. package/test/apps-scaffold-contract.test.mjs +0 -114
  279. package/test/blender-cad.test.mjs +0 -146
  280. package/test/cad-docker.test.mjs +0 -96
  281. package/test/capabilities.test.mjs +0 -133
  282. package/test/cli/api-key-env-whoami.test.mjs +0 -129
  283. package/test/cli/codebaseindex-plan-ux.test.mjs +0 -37
  284. package/test/cli/eval-live.test.mjs +0 -219
  285. package/test/cli/go.test.mjs +0 -128
  286. package/test/cli/local-vault-and-provider-setup.test.mjs +0 -268
  287. package/test/cli/mcp-client.test.mjs +0 -256
  288. package/test/cli/preferences-runtime.test.mjs +0 -11
  289. package/test/cli/runtime-persist.test.mjs +0 -56
  290. package/test/cli/runtime-ui.test.mjs +0 -141
  291. package/test/cli/rust-command.test.mjs +0 -21
  292. package/test/cli/skill-npm-and-env.test.mjs +0 -52
  293. package/test/cli/status-actions.test.mjs +0 -88
  294. package/test/cli/wireframes-go-registry.test.mjs +0 -139
  295. package/test/cli-preferences.test.mjs +0 -46
  296. package/test/cloudflare-connector.test.mjs +0 -93
  297. package/test/cloudflare-runtime.test.mjs +0 -82
  298. package/test/context.test.mjs +0 -159
  299. package/test/credentials-audit.test.mjs +0 -113
  300. package/test/deploy-health-scan.test.mjs +0 -95
  301. package/test/deploy-receipt.test.mjs +0 -91
  302. package/test/dockerize.test.mjs +0 -93
  303. package/test/error-diagnostics.test.mjs +0 -124
  304. package/test/eval-context.test.mjs +0 -37
  305. package/test/execution-approvals.test.mjs +0 -27
  306. package/test/fixtures/cad/courtyard-house.json +0 -208
  307. package/test/fixtures/knowledge-rpc-worker.mjs +0 -16
  308. package/test/gcloud-auth.test.mjs +0 -111
  309. package/test/go/build-source-identity.test.mjs +0 -31
  310. package/test/go/cloudflare-probe.test.mjs +0 -317
  311. package/test/go/distribution.test.mjs +0 -28
  312. package/test/google-cloud-connection.test.mjs +0 -83
  313. package/test/google-cloud-inventory.test.mjs +0 -64
  314. package/test/indexing-provider.test.mjs +0 -29
  315. package/test/init-options.test.mjs +0 -35
  316. package/test/integration/ai-access-onboarding.test.mjs +0 -47
  317. package/test/integration/app-command.test.mjs +0 -85
  318. package/test/integration/cad-discovery.test.mjs +0 -158
  319. package/test/integration/cad-docker-executor.test.mjs +0 -249
  320. package/test/integration/cad-execution.test.mjs +0 -184
  321. package/test/integration/cad-project-app.test.mjs +0 -31
  322. package/test/integration/cad-project-d1.test.mjs +0 -25
  323. package/test/integration/cad-project-native.test.mjs +0 -24
  324. package/test/integration/cad-project-workflow.test.mjs +0 -9
  325. package/test/integration/cad-project.test.mjs +0 -85
  326. package/test/integration/cli-help.test.mjs +0 -41
  327. package/test/integration/cms-app-scaffold.test.mjs +0 -71
  328. package/test/integration/cms-site-tenancy-contract.test.mjs +0 -257
  329. package/test/integration/codebaseindex-ingest.test.mjs +0 -214
  330. package/test/integration/core-client.integration.test.mjs +0 -55
  331. package/test/integration/error-runtime-rpc.test.mjs +0 -71
  332. package/test/integration/features-resolve.test.mjs +0 -147
  333. package/test/integration/icon-registry.test.mjs +0 -67
  334. package/test/integration/ingest-discover-models.test.mjs +0 -30
  335. package/test/integration/inspect-path-cli.test.mjs +0 -46
  336. package/test/integration/install-script.test.mjs +0 -110
  337. package/test/integration/inventory-core.test.mjs +0 -53
  338. package/test/integration/inventory-evidence.test.mjs +0 -125
  339. package/test/integration/knowledge-rpc.test.mjs +0 -112
  340. package/test/integration/local-fs.test.mjs +0 -113
  341. package/test/integration/merkle-cli.test.mjs +0 -61
  342. package/test/integration/merkle-persistence-identity.test.mjs +0 -48
  343. package/test/integration/plugins-cloudflare.integration.test.mjs +0 -28
  344. package/test/integration/provider-env-cli.test.mjs +0 -50
  345. package/test/integration/provider-factory.test.mjs +0 -197
  346. package/test/integration/repository-company-graph.test.mjs +0 -90
  347. package/test/integration/runtime-migrations.test.mjs +0 -87
  348. package/test/integration/runtime-status.integration.test.mjs +0 -212
  349. package/test/integration/sam-activity-recovery.test.mjs +0 -147
  350. package/test/integration/sam-decision.test.mjs +0 -584
  351. package/test/integration/sam-kernel.test.mjs +0 -99
  352. package/test/integration/sam-planning-astar.test.mjs +0 -279
  353. package/test/integration/scaffold-cloudflare.test.mjs +0 -46
  354. package/test/integration/skill-runtime.test.mjs +0 -240
  355. package/test/integration/storage-architecture.test.mjs +0 -154
  356. package/test/integration/store-discovery.test.mjs +0 -44
  357. package/test/integration/studio-fs-pty-e2e.test.mjs +0 -294
  358. package/test/integration/terminal-exec.integration.test.mjs +0 -86
  359. package/test/integration/tool-runtime.test.mjs +0 -124
  360. package/test/integration/tool-schema.test.mjs +0 -59
  361. package/test/jr-dev-skill.test.mjs +0 -26
  362. package/test/knowledge-context-pack.test.mjs +0 -19
  363. package/test/knowledge-postgres.test.mjs +0 -40
  364. package/test/knowledge-service.test.mjs +0 -110
  365. package/test/knowledge.test.mjs +0 -206
  366. package/test/live/terminal-transport.live.test.mjs +0 -24
  367. package/test/local-sessions.test.mjs +0 -57
  368. package/test/local-studio-deploy.test.mjs +0 -83
  369. package/test/loopback-oauth.test.mjs +0 -53
  370. package/test/merkle-render.test.mjs +0 -31
  371. package/test/mini.test.mjs +0 -140
  372. package/test/model-catalog.test.mjs +0 -70
  373. package/test/models.test.mjs +0 -309
  374. package/test/npm10-lock.test.mjs +0 -29
  375. package/test/ollama.test.mjs +0 -137
  376. package/test/open-url.test.mjs +0 -64
  377. package/test/openai-responses.test.mjs +0 -155
  378. package/test/portable-context.test.mjs +0 -94
  379. package/test/project-config.test.mjs +0 -81
  380. package/test/project-rules.test.mjs +0 -44
  381. package/test/provider-credentials.test.mjs +0 -106
  382. package/test/rehydrate.test.mjs +0 -25
  383. package/test/release-hygiene.test.mjs +0 -42
  384. package/test/repository-snapshot-view.test.mjs +0 -112
  385. package/test/responses-runner.test.mjs +0 -465
  386. package/test/runtime-activity.test.mjs +0 -98
  387. package/test/sdk-worker-contract.test.mjs +0 -72
  388. package/test/security.test.mjs +0 -252
  389. package/test/setup-planner.test.mjs +0 -74
  390. package/test/setup-runtime.test.mjs +0 -51
  391. package/test/shell.test.mjs +0 -281
  392. package/test/skills.test.mjs +0 -22
  393. package/test/smoke.mjs +0 -122
  394. package/test/telemetry.test.mjs +0 -79
  395. package/test/terminal/local-pty.mock.test.mjs +0 -153
  396. package/test/terminal-machine-identity.test.mjs +0 -88
  397. package/test/theme-portability.test.mjs +0 -14
  398. package/test/tools-search.test.mjs +0 -73
  399. package/test/unit/website-assets-bindings.test.mjs +0 -60
  400. package/test/whoami-resume.test.mjs +0 -157
@@ -0,0 +1,1092 @@
1
+ //! AgentSam machine perception core — deterministic inspect receipts.
2
+ //! No Cloudflare, browser, Tauri, or network dependency.
3
+
4
+ mod assets;
5
+ mod bound;
6
+ mod enrich;
7
+ mod frontend;
8
+ mod liquid;
9
+ mod source_types;
10
+
11
+ use bound::{externalize_and_bound, BoundLimits};
12
+ use enrich::enrich_receipt;
13
+ use source_types::SourceTypeIndex;
14
+
15
+ use serde::Serialize;
16
+ use serde_json::Value;
17
+ use sha2::{Digest, Sha256};
18
+ use std::collections::BTreeMap;
19
+ use std::fs::{self, File, Metadata};
20
+ use std::io::{self, Read};
21
+ use std::path::{Path, PathBuf};
22
+ use std::time::{SystemTime, UNIX_EPOCH};
23
+
24
+ pub const RECEIPT_SCHEMA: &str = "agentsam.machine.receipt.v1";
25
+ pub const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
26
+ pub const PARSER_VERSION: &str = "machine-parser.v1";
27
+ pub const ARTIFACT_SCHEMA_VERSION: &str = "machine-artifacts.v1";
28
+
29
+ /// Directories skipped during normal inspect (summarized, not flooded into facts).
30
+ const IGNORE_DIRS: &[&str] = &[
31
+ ".git",
32
+ ".agentsam",
33
+ "node_modules",
34
+ "python_modules",
35
+ "vendor",
36
+ "target",
37
+ "build",
38
+ "dist",
39
+ ".next",
40
+ ".wrangler",
41
+ ".vinext",
42
+ "__pycache__",
43
+ ".cache",
44
+ ".sites-runtime",
45
+ "npm-cache",
46
+ "_cacache",
47
+ ".npm",
48
+ ".pnpm-store",
49
+ ".turbo",
50
+ ".vite",
51
+ ".parcel-cache",
52
+ "coverage",
53
+ ".venv",
54
+ "venv",
55
+ ".vercel",
56
+ ".TemporaryItems",
57
+ ".Trashes",
58
+ ".Spotlight-V100",
59
+ ".fseventsd",
60
+ ];
61
+
62
+ /// Pure Rust domain logic: no Cloudflare, browser, Tauri, or network dependency.
63
+ pub fn normalize_resource_key(input: &str) -> String {
64
+ input
65
+ .trim()
66
+ .to_ascii_lowercase()
67
+ .chars()
68
+ .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
69
+ .collect::<String>()
70
+ .split('-')
71
+ .filter(|part| !part.is_empty())
72
+ .collect::<Vec<_>>()
73
+ .join("-")
74
+ }
75
+
76
+ #[derive(Debug, Clone, Default)]
77
+ pub struct InspectOptions {
78
+ pub run_id: Option<String>,
79
+ /// When true, walk generated/cache trees instead of summarizing them.
80
+ pub include_generated: bool,
81
+ /// When false, skip writing artifacts / bounding (tests only).
82
+ pub externalize: bool,
83
+ }
84
+
85
+ impl InspectOptions {
86
+ pub fn new() -> Self {
87
+ Self {
88
+ run_id: None,
89
+ include_generated: false,
90
+ externalize: true,
91
+ }
92
+ }
93
+ }
94
+
95
+ #[derive(Debug, Clone, Serialize)]
96
+ pub struct MachineReceipt {
97
+ pub schema: String,
98
+ pub capability: String,
99
+ pub root: String,
100
+ pub run_id: String,
101
+ pub inputs: BTreeMap<String, String>,
102
+ #[serde(skip_serializing_if = "Option::is_none")]
103
+ pub summary: Option<Value>,
104
+ #[serde(skip_serializing_if = "Option::is_none")]
105
+ pub detail: Option<Value>,
106
+ pub facts: Vec<FileFact>,
107
+ pub edges: Vec<Value>,
108
+ pub findings: Vec<Value>,
109
+ pub artifacts: Vec<Value>,
110
+ pub errors: Vec<MachineError>,
111
+ pub stats: InspectStats,
112
+ pub provenance: Provenance,
113
+ }
114
+
115
+ #[derive(Debug, Clone, Serialize)]
116
+ pub struct FileFact {
117
+ pub id: String,
118
+ /// Filesystem object kind: file | symlink
119
+ pub fs_kind: String,
120
+ pub path: String,
121
+ pub size: u64,
122
+ pub mtime_unix_ms: Option<u64>,
123
+ pub sha256: String,
124
+ pub extension: Option<String>,
125
+ pub source: SourceInfo,
126
+ pub content: ContentEvidence,
127
+ pub classification: Classification,
128
+ /// Execution provenance for this fact (not file-content classification).
129
+ pub analysis: FileAnalysis,
130
+ }
131
+
132
+ #[derive(Debug, Clone, Serialize)]
133
+ pub struct FileAnalysis {
134
+ /// True when Machine reused a prior analysis cache entry for this SHA.
135
+ pub cache_hit: bool,
136
+ }
137
+
138
+ #[derive(Debug, Clone, Serialize)]
139
+ pub struct SourceInfo {
140
+ #[serde(rename = "type")]
141
+ pub type_id: String,
142
+ pub language: Option<String>,
143
+ pub syntax: Option<String>,
144
+ /// code | document | asset | binary
145
+ pub kind: Option<String>,
146
+ pub ast_capable: bool,
147
+ }
148
+
149
+ #[derive(Debug, Clone, Serialize)]
150
+ pub struct ContentEvidence {
151
+ /// text | image | archive | binary | database | symlink
152
+ pub kind: String,
153
+ /// Character encoding when content is text (e.g. utf-8). Not a magic signature.
154
+ pub encoding: Option<String>,
155
+ /// Binary magic signature when known (png, zip, wasm, …).
156
+ pub signature: Option<String>,
157
+ }
158
+
159
+ #[derive(Debug, Clone, Serialize)]
160
+ pub struct Classification {
161
+ pub category: String,
162
+ pub role: String,
163
+ pub generated: bool,
164
+ pub include_by_default: bool,
165
+ }
166
+
167
+ #[derive(Debug, Clone, Serialize)]
168
+ pub struct MachineError {
169
+ pub path: String,
170
+ pub operation: String,
171
+ pub message: String,
172
+ }
173
+
174
+ #[derive(Debug, Clone, Default, Serialize)]
175
+ pub struct InspectStats {
176
+ pub files: u64,
177
+ pub symlinks: u64,
178
+ pub directories: u64,
179
+ pub ignored_directories: u64,
180
+ pub skipped_noise_files: u64,
181
+ pub bytes: u64,
182
+ pub excluded_subtrees: Vec<ExcludedSubtree>,
183
+ }
184
+
185
+ #[derive(Debug, Clone, Serialize)]
186
+ pub struct ExcludedSubtree {
187
+ pub path: String,
188
+ pub reason: String,
189
+ pub files: u64,
190
+ pub bytes: u64,
191
+ pub default_disposition: String,
192
+ }
193
+
194
+ #[derive(Debug, Clone, Serialize)]
195
+ pub struct Provenance {
196
+ pub engine: String,
197
+ pub engine_version: String,
198
+ pub parser_version: String,
199
+ pub artifact_schema_version: String,
200
+ pub source_mutated: bool,
201
+ pub network_used: bool,
202
+ pub cache: CacheProvenance,
203
+ }
204
+
205
+ #[derive(Debug, Clone, Serialize)]
206
+ pub struct CacheProvenance {
207
+ pub schema: String,
208
+ pub hits: u64,
209
+ pub misses: u64,
210
+ pub enabled: bool,
211
+ }
212
+
213
+ pub fn inspect_path(target: &Path, requested_run_id: Option<&str>) -> io::Result<MachineReceipt> {
214
+ let mut opts = InspectOptions::new();
215
+ opts.run_id = requested_run_id.map(str::to_owned);
216
+ inspect_path_with_options(target, opts)
217
+ }
218
+
219
+ pub fn inspect_path_with_options(
220
+ target: &Path,
221
+ options: InspectOptions,
222
+ ) -> io::Result<MachineReceipt> {
223
+ let inspection_target = fs::canonicalize(target)?;
224
+ let metadata = fs::symlink_metadata(&inspection_target)?;
225
+
226
+ // Single-file inspect: perception walks the file, but cache/artifacts live under
227
+ // the containing directory (never `<file>/.agentsam/...`).
228
+ let content_root: PathBuf = if metadata.is_file() {
229
+ let parent = inspection_target
230
+ .parent()
231
+ .filter(|p| !p.as_os_str().is_empty())
232
+ .ok_or_else(|| {
233
+ io::Error::new(
234
+ io::ErrorKind::InvalidInput,
235
+ "cannot determine parent directory for file inspect target",
236
+ )
237
+ })?;
238
+ fs::canonicalize(parent)?
239
+ } else {
240
+ inspection_target.clone()
241
+ };
242
+
243
+ let root_text = inspection_target.to_string_lossy().into_owned();
244
+ let content_root_text = content_root.to_string_lossy().into_owned();
245
+ let run_id = options
246
+ .run_id
247
+ .as_deref()
248
+ .filter(|value| !value.trim().is_empty())
249
+ .map(ToOwned::to_owned)
250
+ .unwrap_or_else(|| default_run_id(&root_text));
251
+
252
+ let mut inputs = BTreeMap::new();
253
+ inputs.insert("target".to_string(), root_text.clone());
254
+ inputs.insert("content_root".to_string(), content_root_text.clone());
255
+ inputs.insert(
256
+ "include_generated".to_string(),
257
+ options.include_generated.to_string(),
258
+ );
259
+
260
+ let mut receipt = MachineReceipt {
261
+ schema: RECEIPT_SCHEMA.to_string(),
262
+ capability: "machine.inspect".to_string(),
263
+ root: root_text,
264
+ run_id,
265
+ inputs,
266
+ summary: None,
267
+ detail: None,
268
+ facts: Vec::new(),
269
+ edges: Vec::new(),
270
+ findings: Vec::new(),
271
+ artifacts: Vec::new(),
272
+ errors: Vec::new(),
273
+ stats: InspectStats::default(),
274
+ provenance: Provenance {
275
+ engine: "agentsam-machine".to_string(),
276
+ engine_version: ENGINE_VERSION.to_string(),
277
+ parser_version: PARSER_VERSION.to_string(),
278
+ artifact_schema_version: ARTIFACT_SCHEMA_VERSION.to_string(),
279
+ source_mutated: false,
280
+ network_used: false,
281
+ cache: CacheProvenance {
282
+ schema: "agentsam.machine.cache.v1".to_string(),
283
+ hits: 0,
284
+ misses: 0,
285
+ enabled: true,
286
+ },
287
+ },
288
+ };
289
+
290
+ if metadata.is_file() {
291
+ inspect_file(&content_root, &inspection_target, &metadata, &mut receipt);
292
+ } else if metadata.is_dir() {
293
+ walk_directory(
294
+ &content_root,
295
+ &content_root,
296
+ &mut receipt,
297
+ options.include_generated,
298
+ )?;
299
+ }
300
+
301
+ receipt.facts.sort_by(|a, b| a.path.cmp(&b.path));
302
+ receipt.errors.sort_by(|a, b| {
303
+ a.path
304
+ .cmp(&b.path)
305
+ .then_with(|| a.operation.cmp(&b.operation))
306
+ .then_with(|| a.message.cmp(&b.message))
307
+ });
308
+
309
+ // Incrementality hook: reuse prior fact cache entries when sha256 + parser match.
310
+ apply_fact_cache(&content_root, &mut receipt);
311
+
312
+ enrich_receipt(&content_root, &mut receipt);
313
+ persist_fact_cache(&content_root, &receipt);
314
+
315
+ if options.externalize {
316
+ externalize_and_bound(&content_root, &mut receipt, BoundLimits::default())?;
317
+ }
318
+
319
+ Ok(receipt)
320
+ }
321
+
322
+ fn walk_directory(
323
+ root: &Path,
324
+ current: &Path,
325
+ receipt: &mut MachineReceipt,
326
+ include_generated: bool,
327
+ ) -> io::Result<()> {
328
+ receipt.stats.directories += 1;
329
+ let mut entries = match fs::read_dir(current) {
330
+ Ok(entries) => entries.filter_map(Result::ok).collect::<Vec<_>>(),
331
+ Err(error) => {
332
+ receipt.errors.push(MachineError {
333
+ path: relative_path(root, current),
334
+ operation: "read_dir".to_string(),
335
+ message: error.to_string(),
336
+ });
337
+ return Ok(());
338
+ }
339
+ };
340
+ entries.sort_by_key(|entry| entry.file_name());
341
+
342
+ for entry in entries {
343
+ let path = entry.path();
344
+ let file_name = entry.file_name().to_string_lossy().into_owned();
345
+ let metadata = match fs::symlink_metadata(&path) {
346
+ Ok(metadata) => metadata,
347
+ Err(error) => {
348
+ receipt.errors.push(MachineError {
349
+ path: relative_path(root, &path),
350
+ operation: "metadata".to_string(),
351
+ message: error.to_string(),
352
+ });
353
+ continue;
354
+ }
355
+ };
356
+
357
+ if metadata.file_type().is_symlink() {
358
+ inspect_symlink(root, &path, &metadata, receipt);
359
+ continue;
360
+ }
361
+
362
+ if metadata.is_dir() {
363
+ if !include_generated && is_ignored_directory(&file_name) {
364
+ receipt.stats.ignored_directories += 1;
365
+ let (files, bytes) = summarize_tree(&path);
366
+ receipt.stats.excluded_subtrees.push(ExcludedSubtree {
367
+ path: relative_path(root, &path),
368
+ reason: format!("ignored_directory:{file_name}"),
369
+ files,
370
+ bytes,
371
+ default_disposition: "exclude".to_string(),
372
+ });
373
+ continue;
374
+ }
375
+ // Never recurse into our own machine artifact store even with --include-generated,
376
+ // unless the user pointed inspect directly at it (handled by root walk start).
377
+ if file_name == ".agentsam" && current == root && include_generated {
378
+ // allow if explicitly including generated — still skip machine runs flood
379
+ let machine_runs = path.join("machine").join("runs");
380
+ if machine_runs.exists() {
381
+ receipt.stats.ignored_directories += 1;
382
+ let (files, bytes) = summarize_tree(&machine_runs);
383
+ receipt.stats.excluded_subtrees.push(ExcludedSubtree {
384
+ path: relative_path(root, &machine_runs),
385
+ reason: "ignored_directory:machine_runs".to_string(),
386
+ files,
387
+ bytes,
388
+ default_disposition: "exclude".to_string(),
389
+ });
390
+ }
391
+ }
392
+ walk_directory(root, &path, receipt, include_generated)?;
393
+ continue;
394
+ }
395
+
396
+ if metadata.is_file() {
397
+ if is_noise_file(&file_name) {
398
+ receipt.stats.skipped_noise_files += 1;
399
+ continue;
400
+ }
401
+ inspect_file(root, &path, &metadata, receipt);
402
+ }
403
+ }
404
+
405
+ Ok(())
406
+ }
407
+
408
+ fn summarize_tree(path: &Path) -> (u64, u64) {
409
+ let mut files = 0u64;
410
+ let mut bytes = 0u64;
411
+ let mut stack = vec![path.to_path_buf()];
412
+ while let Some(current) = stack.pop() {
413
+ let Ok(entries) = fs::read_dir(&current) else {
414
+ continue;
415
+ };
416
+ for entry in entries.flatten() {
417
+ let p = entry.path();
418
+ let name = entry.file_name().to_string_lossy().into_owned();
419
+ if is_noise_file(&name) {
420
+ continue;
421
+ }
422
+ let Ok(meta) = fs::symlink_metadata(&p) else {
423
+ continue;
424
+ };
425
+ if meta.is_dir() {
426
+ if is_ignored_directory(&name) {
427
+ continue;
428
+ }
429
+ stack.push(p);
430
+ } else if meta.is_file() {
431
+ files += 1;
432
+ bytes = bytes.saturating_add(meta.len());
433
+ }
434
+ }
435
+ }
436
+ (files, bytes)
437
+ }
438
+
439
+ fn inspect_file(root: &Path, path: &Path, metadata: &Metadata, receipt: &mut MachineReceipt) {
440
+ match hash_file(path) {
441
+ Ok((sha256, prefix)) => {
442
+ let rel = relative_path(root, path);
443
+ let content = classify_content_prefix(&prefix);
444
+ let (source, classification) = classify_semantic(&rel, &content);
445
+ receipt.stats.files += 1;
446
+ receipt.stats.bytes = receipt.stats.bytes.saturating_add(metadata.len());
447
+ receipt.facts.push(FileFact {
448
+ id: format!("file:{rel}"),
449
+ fs_kind: "file".to_string(),
450
+ path: rel.clone(),
451
+ size: metadata.len(),
452
+ mtime_unix_ms: modified_ms(metadata),
453
+ sha256,
454
+ extension: path
455
+ .extension()
456
+ .and_then(|value| value.to_str())
457
+ .map(|value| value.to_ascii_lowercase()),
458
+ source,
459
+ content,
460
+ classification,
461
+ analysis: FileAnalysis { cache_hit: false },
462
+ });
463
+ }
464
+ Err(error) => receipt.errors.push(MachineError {
465
+ path: relative_path(root, path),
466
+ operation: "hash".to_string(),
467
+ message: error.to_string(),
468
+ }),
469
+ }
470
+ }
471
+
472
+ fn inspect_symlink(root: &Path, path: &Path, metadata: &Metadata, receipt: &mut MachineReceipt) {
473
+ match fs::read_link(path) {
474
+ Ok(target) => {
475
+ let literal = target.to_string_lossy().into_owned();
476
+ let digest = sha256_bytes(literal.as_bytes());
477
+ let rel = relative_path(root, path);
478
+ receipt.stats.symlinks += 1;
479
+ receipt.facts.push(FileFact {
480
+ id: format!("symlink:{rel}"),
481
+ fs_kind: "symlink".to_string(),
482
+ path: rel,
483
+ size: literal.len() as u64,
484
+ mtime_unix_ms: modified_ms(metadata),
485
+ sha256: digest,
486
+ extension: path
487
+ .extension()
488
+ .and_then(|value| value.to_str())
489
+ .map(|value| value.to_ascii_lowercase()),
490
+ source: SourceInfo {
491
+ type_id: "unknown".to_string(),
492
+ language: None,
493
+ syntax: None,
494
+ kind: Some("binary".to_string()),
495
+ ast_capable: false,
496
+ },
497
+ content: ContentEvidence {
498
+ kind: "symlink".to_string(),
499
+ encoding: None,
500
+ signature: None,
501
+ },
502
+ classification: Classification {
503
+ category: "link".to_string(),
504
+ role: "symlink".to_string(),
505
+ generated: false,
506
+ include_by_default: false,
507
+ },
508
+ analysis: FileAnalysis { cache_hit: false },
509
+ });
510
+ }
511
+ Err(error) => receipt.errors.push(MachineError {
512
+ path: relative_path(root, path),
513
+ operation: "read_link".to_string(),
514
+ message: error.to_string(),
515
+ }),
516
+ }
517
+ }
518
+
519
+ fn classify_semantic(rel: &str, content: &ContentEvidence) -> (SourceInfo, Classification) {
520
+ let index = SourceTypeIndex::global();
521
+ if let Some(def) = index.for_path(rel) {
522
+ let syntax = index.syntax_for(rel, def);
523
+ let role = match def.kind.as_str() {
524
+ "code" if rel.contains("frontend/") || rel.contains("/src/") => "frontend_source",
525
+ "code" => "source",
526
+ "document" if def.id == "html" => "page_candidate",
527
+ "document" if def.id == "liquid" && rel.starts_with("sections/") => "section_candidate",
528
+ "document" if def.id == "liquid" && rel.starts_with("snippets/") => "snippet_candidate",
529
+ "document" if def.id == "liquid" && rel.starts_with("layout/") => "layout_shell",
530
+ "document" if def.id == "liquid" && rel.starts_with("templates/") => "template_candidate",
531
+ "document" if def.id == "json" && rel.ends_with("theme.json") => "theme_manifest",
532
+ "document" if def.id == "json" && rel.starts_with("templates/") => "template_candidate",
533
+ "document" if def.id == "css" => "stylesheet",
534
+ "asset" if def.id == "svg" => "image",
535
+ "asset" => "asset",
536
+ "binary" => "binary",
537
+ _ => "file",
538
+ };
539
+ let category = match def.kind.as_str() {
540
+ "code" => "source",
541
+ "document" if def.id == "markdown" => "docs",
542
+ "document" => "source",
543
+ "asset" => "asset",
544
+ "binary" => "binary",
545
+ _ => "other",
546
+ };
547
+ return (
548
+ SourceInfo {
549
+ type_id: def.id.clone(),
550
+ language: Some(def.label.clone()),
551
+ syntax,
552
+ kind: Some(def.kind.clone()),
553
+ ast_capable: def.ast,
554
+ },
555
+ Classification {
556
+ category: category.to_string(),
557
+ role: role.to_string(),
558
+ generated: false,
559
+ include_by_default: true,
560
+ },
561
+ );
562
+ }
563
+
564
+ // Unknown is first-class — never emit null type.
565
+ let (kind, category, role) = match content.kind.as_str() {
566
+ "image" => ("asset", "asset", "image"),
567
+ "archive" => ("binary", "archive", "archive"),
568
+ "database" => ("binary", "data", "database"),
569
+ "text" => ("document", "source", "text"),
570
+ _ => ("binary", "binary", "binary"),
571
+ };
572
+ (
573
+ SourceInfo {
574
+ type_id: "unknown".to_string(),
575
+ language: None,
576
+ syntax: None,
577
+ kind: Some(kind.to_string()),
578
+ ast_capable: false,
579
+ },
580
+ Classification {
581
+ category: category.to_string(),
582
+ role: role.to_string(),
583
+ generated: false,
584
+ include_by_default: kind != "binary",
585
+ },
586
+ )
587
+ }
588
+
589
+ fn hash_file(path: &Path) -> io::Result<(String, Vec<u8>)> {
590
+ let mut file = File::open(path)?;
591
+ let mut hasher = Sha256::new();
592
+ let mut prefix = Vec::with_capacity(64);
593
+ let mut buffer = [0u8; 64 * 1024];
594
+
595
+ loop {
596
+ let read = file.read(&mut buffer)?;
597
+ if read == 0 {
598
+ break;
599
+ }
600
+ if prefix.len() < 64 {
601
+ let remaining = 64 - prefix.len();
602
+ prefix.extend_from_slice(&buffer[..read.min(remaining)]);
603
+ }
604
+ hasher.update(&buffer[..read]);
605
+ }
606
+
607
+ Ok((format!("{:x}", hasher.finalize()), prefix))
608
+ }
609
+
610
+ fn sha256_bytes(bytes: &[u8]) -> String {
611
+ let mut hasher = Sha256::new();
612
+ hasher.update(bytes);
613
+ format!("{:x}", hasher.finalize())
614
+ }
615
+
616
+ fn classify_content_prefix(prefix: &[u8]) -> ContentEvidence {
617
+ if prefix.starts_with(b"\x89PNG\r\n\x1a\n") {
618
+ return ContentEvidence {
619
+ kind: "image".into(),
620
+ encoding: None,
621
+ signature: Some("png".into()),
622
+ };
623
+ }
624
+ if prefix.starts_with(&[0xff, 0xd8, 0xff]) {
625
+ return ContentEvidence {
626
+ kind: "image".into(),
627
+ encoding: None,
628
+ signature: Some("jpeg".into()),
629
+ };
630
+ }
631
+ if prefix.starts_with(b"GIF87a") || prefix.starts_with(b"GIF89a") {
632
+ return ContentEvidence {
633
+ kind: "image".into(),
634
+ encoding: None,
635
+ signature: Some("gif".into()),
636
+ };
637
+ }
638
+ if prefix.starts_with(b"%PDF-") {
639
+ return ContentEvidence {
640
+ kind: "document".into(),
641
+ encoding: None,
642
+ signature: Some("pdf".into()),
643
+ };
644
+ }
645
+ if prefix.starts_with(b"PK\x03\x04") {
646
+ return ContentEvidence {
647
+ kind: "archive".into(),
648
+ encoding: None,
649
+ signature: Some("zip".into()),
650
+ };
651
+ }
652
+ if prefix.starts_with(&[0x1f, 0x8b]) {
653
+ return ContentEvidence {
654
+ kind: "archive".into(),
655
+ encoding: None,
656
+ signature: Some("gzip".into()),
657
+ };
658
+ }
659
+ if prefix.starts_with(b"\0asm") {
660
+ return ContentEvidence {
661
+ kind: "binary".into(),
662
+ encoding: None,
663
+ signature: Some("wasm".into()),
664
+ };
665
+ }
666
+ if prefix.starts_with(b"SQLite format 3\0") {
667
+ return ContentEvidence {
668
+ kind: "database".into(),
669
+ encoding: None,
670
+ signature: Some("sqlite".into()),
671
+ };
672
+ }
673
+ if prefix.starts_with(&[0x7f, b'E', b'L', b'F']) {
674
+ return ContentEvidence {
675
+ kind: "binary".into(),
676
+ encoding: None,
677
+ signature: Some("elf".into()),
678
+ };
679
+ }
680
+ if !prefix.contains(&0) && std::str::from_utf8(prefix).is_ok() {
681
+ return ContentEvidence {
682
+ kind: "text".into(),
683
+ encoding: Some("utf-8".into()),
684
+ signature: None,
685
+ };
686
+ }
687
+ ContentEvidence {
688
+ kind: "binary".into(),
689
+ encoding: None,
690
+ signature: None,
691
+ }
692
+ }
693
+
694
+ fn is_ignored_directory(name: &str) -> bool {
695
+ IGNORE_DIRS.iter().any(|candidate| *candidate == name)
696
+ }
697
+
698
+ fn is_noise_file(name: &str) -> bool {
699
+ let lower = name.to_ascii_lowercase();
700
+ name == ".DS_Store"
701
+ || name == "Thumbs.db"
702
+ || name == "desktop.ini"
703
+ || name.starts_with("._")
704
+ || lower.ends_with(".min.js")
705
+ || lower.ends_with(".bundle.js")
706
+ || lower.ends_with(".map")
707
+ }
708
+
709
+ fn relative_path(root: &Path, path: &Path) -> String {
710
+ path.strip_prefix(root)
711
+ .unwrap_or(path)
712
+ .to_string_lossy()
713
+ .replace('\\', "/")
714
+ }
715
+
716
+ fn modified_ms(metadata: &Metadata) -> Option<u64> {
717
+ metadata
718
+ .modified()
719
+ .ok()
720
+ .and_then(|value| value.duration_since(UNIX_EPOCH).ok())
721
+ .map(|duration| duration.as_millis().min(u64::MAX as u128) as u64)
722
+ }
723
+
724
+ fn default_run_id(root: &str) -> String {
725
+ let now = SystemTime::now()
726
+ .duration_since(UNIX_EPOCH)
727
+ .map(|duration| duration.as_nanos())
728
+ .unwrap_or_default();
729
+ let seed = format!("{root}:{now}:{}", std::process::id());
730
+ format!("run_{}", &sha256_bytes(seed.as_bytes())[..16])
731
+ }
732
+
733
+ fn cache_dir(root: &Path) -> std::path::PathBuf {
734
+ root.join(".agentsam")
735
+ .join("machine")
736
+ .join("cache")
737
+ .join(PARSER_VERSION)
738
+ }
739
+
740
+ fn apply_fact_cache(root: &Path, receipt: &mut MachineReceipt) {
741
+ let dir = cache_dir(root);
742
+ if !dir.exists() {
743
+ receipt.provenance.cache.misses = receipt.facts.len() as u64;
744
+ return;
745
+ }
746
+ for fact in &mut receipt.facts {
747
+ let path = dir.join(format!("{}.json", fact.sha256));
748
+ if let Ok(text) = fs::read_to_string(&path) {
749
+ if let Ok(cached) = serde_json::from_str::<CachedFact>(&text) {
750
+ if cached.parser_version == PARSER_VERSION
751
+ && cached.engine_version == ENGINE_VERSION
752
+ && cached.source_type == fact.source.type_id
753
+ {
754
+ fact.analysis.cache_hit = true;
755
+ receipt.provenance.cache.hits += 1;
756
+ continue;
757
+ }
758
+ }
759
+ }
760
+ receipt.provenance.cache.misses += 1;
761
+ }
762
+ }
763
+
764
+ fn persist_fact_cache(root: &Path, receipt: &MachineReceipt) {
765
+ let dir = cache_dir(root);
766
+ let _ = fs::create_dir_all(&dir);
767
+ for fact in &receipt.facts {
768
+ let cached = CachedFact {
769
+ parser_version: PARSER_VERSION.to_string(),
770
+ engine_version: ENGINE_VERSION.to_string(),
771
+ sha256: fact.sha256.clone(),
772
+ path: fact.path.clone(),
773
+ source_type: fact.source.type_id.clone(),
774
+ role: fact.classification.role.clone(),
775
+ };
776
+ if let Ok(text) = serde_json::to_string(&cached) {
777
+ let _ = fs::write(dir.join(format!("{}.json", fact.sha256)), text);
778
+ }
779
+ }
780
+ }
781
+
782
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
783
+ struct CachedFact {
784
+ parser_version: String,
785
+ engine_version: String,
786
+ sha256: String,
787
+ path: String,
788
+ source_type: String,
789
+ role: String,
790
+ }
791
+
792
+ #[cfg(test)]
793
+ mod tests {
794
+ use super::*;
795
+ use std::io::Write;
796
+
797
+ #[test]
798
+ fn normalizes_keys_deterministically() {
799
+ assert_eq!(normalize_resource_key(" Hello / World "), "hello-world");
800
+ }
801
+
802
+ #[test]
803
+ fn inspect_classifies_source_types_and_splits_encoding_from_signature() {
804
+ let temp = tempfile::tempdir().unwrap();
805
+ fs::create_dir_all(temp.path().join("src")).unwrap();
806
+ fs::create_dir_all(temp.path().join("target")).unwrap();
807
+ fs::write(
808
+ temp.path().join("src/lib.rs"),
809
+ b"pub fn answer() -> u32 { 42 }\n",
810
+ )
811
+ .unwrap();
812
+ fs::write(temp.path().join("README.md"), b"# Example\n").unwrap();
813
+ fs::write(temp.path().join("target/generated.rs"), b"generated").unwrap();
814
+ fs::write(temp.path().join("._noise"), b"appledouble").unwrap();
815
+
816
+ let mut opts = InspectOptions::new();
817
+ opts.run_id = Some("run_test".into());
818
+ opts.externalize = false;
819
+ let receipt = inspect_path_with_options(temp.path(), opts).unwrap();
820
+ let paths: Vec<_> = receipt.facts.iter().map(|f| f.path.as_str()).collect();
821
+ assert_eq!(paths, vec!["README.md", "src/lib.rs"]);
822
+ assert!(receipt.stats.skipped_noise_files >= 1);
823
+
824
+ let rust = receipt.facts.iter().find(|f| f.path == "src/lib.rs").unwrap();
825
+ assert_eq!(rust.fs_kind, "file");
826
+ assert_eq!(rust.source.type_id, "rust");
827
+ assert_eq!(rust.source.language.as_deref(), Some("Rust"));
828
+ assert_eq!(rust.source.kind.as_deref(), Some("code"));
829
+ assert_eq!(rust.content.encoding.as_deref(), Some("utf-8"));
830
+ assert!(rust.content.signature.is_none());
831
+ assert!(!receipt.findings.is_empty());
832
+ assert!(!receipt.artifacts.is_empty());
833
+ assert_eq!(receipt.stats.excluded_subtrees.len(), 1);
834
+ assert_eq!(receipt.stats.excluded_subtrees[0].path, "target");
835
+ }
836
+
837
+ #[test]
838
+ fn unknown_type_never_null() {
839
+ let temp = tempfile::tempdir().unwrap();
840
+ fs::write(temp.path().join("mystery"), b"not a known type\n").unwrap();
841
+ let mut opts = InspectOptions::new();
842
+ opts.run_id = Some("run_unknown".into());
843
+ opts.externalize = false;
844
+ let receipt = inspect_path_with_options(temp.path(), opts).unwrap();
845
+ let fact = receipt.facts.first().unwrap();
846
+ assert_eq!(fact.source.type_id, "unknown");
847
+ }
848
+
849
+ #[test]
850
+ fn include_generated_walks_ignored_dirs() {
851
+ let temp = tempfile::tempdir().unwrap();
852
+ fs::create_dir_all(temp.path().join("dist")).unwrap();
853
+ fs::write(temp.path().join("dist/out.js"), b"console.log(1)\n").unwrap();
854
+ let mut opts = InspectOptions::new();
855
+ opts.run_id = Some("run_inc".into());
856
+ opts.include_generated = true;
857
+ opts.externalize = false;
858
+ let receipt = inspect_path_with_options(temp.path(), opts).unwrap();
859
+ assert!(receipt.facts.iter().any(|f| f.path == "dist/out.js"));
860
+ }
861
+
862
+ #[test]
863
+ fn binary_signature_is_not_called_magic_utf8() {
864
+ let temp = tempfile::tempdir().unwrap();
865
+ let path = temp.path().join("module.bin");
866
+ let mut file = File::create(&path).unwrap();
867
+ file.write_all(b"\0asm\x01\0\0\0").unwrap();
868
+
869
+ let mut opts = InspectOptions::new();
870
+ opts.run_id = Some("run_magic".into());
871
+ opts.externalize = false;
872
+ let receipt = inspect_path_with_options(temp.path(), opts).unwrap();
873
+ let fact = receipt.facts.first().unwrap();
874
+ assert_eq!(fact.content.kind, "binary");
875
+ assert_eq!(fact.content.signature.as_deref(), Some("wasm"));
876
+ assert!(fact.content.encoding.is_none());
877
+ assert_eq!(fact.source.type_id, "unknown");
878
+ }
879
+
880
+ #[test]
881
+ fn church_site_fixture_produces_semantic_perception() {
882
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
883
+ .join("../../../apps/client-cms-editor/fixtures/donor-themes/church-site");
884
+ assert!(
885
+ fixture.join("site/index.html").exists(),
886
+ "church-site fixture missing at {}",
887
+ fixture.display()
888
+ );
889
+
890
+ let mut opts = InspectOptions::new();
891
+ opts.run_id = Some("run_church".into());
892
+ // Keep full facts in-memory for assertions; still exercise enrichment.
893
+ opts.externalize = false;
894
+ let receipt = inspect_path_with_options(&fixture, opts).unwrap();
895
+
896
+ let html = receipt
897
+ .facts
898
+ .iter()
899
+ .filter(|f| f.source.type_id == "html")
900
+ .count();
901
+ let svg = receipt
902
+ .facts
903
+ .iter()
904
+ .filter(|f| f.source.type_id == "svg")
905
+ .count();
906
+ let json = receipt
907
+ .facts
908
+ .iter()
909
+ .filter(|f| f.source.type_id == "json")
910
+ .count();
911
+
912
+ assert_eq!(html, 5, "expected 5 HTML pages");
913
+ assert_eq!(svg, 9, "expected 9 SVG assets");
914
+ assert_eq!(json, 1, "expected theme.json");
915
+ assert!(receipt.edges.len() > 0, "edges must not be empty");
916
+ assert!(receipt.findings.len() > 0, "findings must not be empty");
917
+ assert!(receipt.artifacts.len() > 0, "artifacts must not be empty");
918
+
919
+ assert!(receipt
920
+ .findings
921
+ .iter()
922
+ .any(|f| f.get("kind") == Some(&serde_json::json!("theme_manifest_candidate"))
923
+ || f.get("observation") == Some(&serde_json::json!("theme_manifest"))));
924
+ assert!(receipt
925
+ .findings
926
+ .iter()
927
+ .any(|f| f.get("kind") == Some(&serde_json::json!("composition"))
928
+ || f.get("observation") == Some(&serde_json::json!("html_documents"))));
929
+ assert!(!receipt.edges.iter().any(|e| {
930
+ e.get("type") == Some(&serde_json::json!("theme_applies_to"))
931
+ }));
932
+ assert!(receipt
933
+ .artifacts
934
+ .iter()
935
+ .any(|a| a.get("kind") == Some(&serde_json::json!("route_manifest"))
936
+ || a.get("kind") == Some(&serde_json::json!("asset_manifest"))));
937
+ assert!(receipt.edges.iter().any(|e| {
938
+ e.get("type") == Some(&serde_json::json!("asset_reference"))
939
+ && e
940
+ .pointer("/evidence/literal")
941
+ .and_then(|v| v.as_str())
942
+ .is_some_and(|s| s.starts_with("https://"))
943
+ }));
944
+ assert!(!receipt.provenance.network_used);
945
+
946
+ for finding in &receipt.findings {
947
+ let has_support = finding.get("evidence").is_some()
948
+ || finding.get("observations").is_some()
949
+ || finding.get("composition").is_some();
950
+ assert!(
951
+ has_support,
952
+ "finding missing evidence/observations/composition: {finding}"
953
+ );
954
+ }
955
+
956
+ let beliefs = receipt
957
+ .facts
958
+ .iter()
959
+ .find(|f| f.path == "site/beliefs.html")
960
+ .unwrap();
961
+ assert_eq!(beliefs.fs_kind, "file");
962
+ assert_eq!(beliefs.source.type_id, "html");
963
+ assert_eq!(beliefs.classification.role, "page_candidate");
964
+ assert!(!beliefs.classification.generated);
965
+ assert_eq!(beliefs.content.encoding.as_deref(), Some("utf-8"));
966
+ assert!(beliefs.content.signature.is_none());
967
+ // analysis.cache_hit is execution provenance, not "this file is cache content"
968
+ assert!(!beliefs.analysis.cache_hit || receipt.provenance.cache.hits > 0);
969
+
970
+ assert_graph_edge_ids_unique(&receipt.edges);
971
+ }
972
+
973
+ #[test]
974
+ fn single_file_inspect_externalizes_under_parent_not_file() {
975
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join(
976
+ "../../../apps/client-cms-editor/fixtures/frontend-perception/page.tsx",
977
+ );
978
+ assert!(fixture.exists(), "missing fixture {}", fixture.display());
979
+
980
+ let mut opts = InspectOptions::new();
981
+ opts.run_id = Some("run_single_file".into());
982
+ opts.externalize = true;
983
+ let receipt = inspect_path_with_options(&fixture, opts).unwrap();
984
+
985
+ assert_eq!(receipt.stats.files, 1);
986
+ assert!(receipt.facts.iter().any(|f| f.path.ends_with("page.tsx")));
987
+ assert!(
988
+ receipt
989
+ .inputs
990
+ .get("content_root")
991
+ .is_some_and(|r| Path::new(r).is_dir()),
992
+ "content_root must be a directory"
993
+ );
994
+ let artifact_dir = receipt
995
+ .detail
996
+ .as_ref()
997
+ .and_then(|d| d.get("artifact_dir"))
998
+ .and_then(|v| v.as_str())
999
+ .expect("artifact_dir");
1000
+ assert!(
1001
+ artifact_dir.contains("/.agentsam/machine/runs/"),
1002
+ "unexpected artifact_dir: {artifact_dir}"
1003
+ );
1004
+ assert!(
1005
+ !artifact_dir.contains("page.tsx/.agentsam"),
1006
+ "must not nest .agentsam under the file path: {artifact_dir}"
1007
+ );
1008
+ assert!(Path::new(artifact_dir).is_dir());
1009
+ }
1010
+
1011
+ #[test]
1012
+ fn frontend_perception_page_tsx_emits_generic_candidates() {
1013
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join(
1014
+ "../../../apps/client-cms-editor/fixtures/frontend-perception/page.tsx",
1015
+ );
1016
+ let mut opts = InspectOptions::new();
1017
+ opts.run_id = Some("run_frontend_perception".into());
1018
+ opts.externalize = false;
1019
+ let receipt = inspect_path_with_options(&fixture, opts).unwrap();
1020
+
1021
+ let finding = receipt
1022
+ .findings
1023
+ .iter()
1024
+ .find(|f| f.get("kind") == Some(&serde_json::json!("frontend_source_candidates")))
1025
+ .expect("frontend_source_candidates finding");
1026
+ let candidates = finding.get("candidates").expect("candidates");
1027
+ let routes = candidates
1028
+ .get("route_candidate")
1029
+ .and_then(|v| v.as_array())
1030
+ .expect("route_candidate");
1031
+ assert!(
1032
+ routes.iter().any(|r| r.as_str() == Some("/")),
1033
+ "expected slug / in routes: {routes:?}"
1034
+ );
1035
+ assert!(routes.iter().any(|r| r.as_str() == Some("/work")));
1036
+
1037
+ let shells = candidates
1038
+ .get("shell_candidate")
1039
+ .and_then(|v| v.as_array())
1040
+ .expect("shell_candidate");
1041
+ for zone in ["HEADER", "BODY", "FOOTER"] {
1042
+ assert!(
1043
+ shells.iter().any(|s| s.as_str() == Some(zone)),
1044
+ "missing shell zone {zone}"
1045
+ );
1046
+ }
1047
+
1048
+ let sections = candidates
1049
+ .get("section_candidate")
1050
+ .and_then(|v| v.as_array())
1051
+ .expect("section_candidate");
1052
+ assert!(
1053
+ sections.iter().any(|s| {
1054
+ s.get("type").and_then(|t| t.as_str()) == Some("Hero")
1055
+ && s.get("zone").and_then(|z| z.as_str()) == Some("BODY")
1056
+ }),
1057
+ "expected Hero BODY section candidate: {sections:?}"
1058
+ );
1059
+
1060
+ let tokens = candidates
1061
+ .get("design_token_evidence")
1062
+ .and_then(|v| v.as_array())
1063
+ .expect("design_token_evidence");
1064
+ assert!(tokens.iter().any(|t| {
1065
+ t.as_str()
1066
+ .is_some_and(|s| s.starts_with("--") || s.starts_with('#'))
1067
+ }));
1068
+
1069
+ // Must stay generic — no CMS identity productization in Machine.
1070
+ let blob = serde_json::to_string(&receipt.findings).unwrap();
1071
+ assert!(!blob.contains("hero.cinematic"));
1072
+ assert!(!blob.contains("navigation.minimal"));
1073
+
1074
+ assert_graph_edge_ids_unique(&receipt.edges);
1075
+ }
1076
+
1077
+ fn assert_graph_edge_ids_unique(edges: &[Value]) {
1078
+ use std::collections::BTreeSet;
1079
+ let mut seen = BTreeSet::new();
1080
+ for edge in edges {
1081
+ let id = edge
1082
+ .get("id")
1083
+ .and_then(|v| v.as_str())
1084
+ .filter(|s| !s.is_empty())
1085
+ .unwrap_or_else(|| panic!("edge missing non-empty id: {edge}"));
1086
+ assert!(
1087
+ seen.insert(id.to_string()),
1088
+ "duplicate edge id: {id}"
1089
+ );
1090
+ }
1091
+ }
1092
+ }