primitive-admin 1.0.61 → 1.0.63

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 (320) hide show
  1. package/README.md +125 -69
  2. package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
  3. package/dist/bin/primitive.js +25 -14
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +36 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +52 -188
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +13 -153
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/collection-type-configs.js +14 -100
  12. package/dist/src/commands/collection-type-configs.js.map +1 -1
  13. package/dist/src/commands/collections.js +47 -8
  14. package/dist/src/commands/collections.js.map +1 -1
  15. package/dist/src/commands/config.d.ts +46 -0
  16. package/dist/src/commands/config.js +479 -0
  17. package/dist/src/commands/config.js.map +1 -0
  18. package/dist/src/commands/connections.js +12 -7
  19. package/dist/src/commands/connections.js.map +1 -1
  20. package/dist/src/commands/cron-triggers.js +84 -176
  21. package/dist/src/commands/cron-triggers.js.map +1 -1
  22. package/dist/src/commands/database-type-configs.d.ts +2 -0
  23. package/dist/src/commands/database-type-configs.js +171 -0
  24. package/dist/src/commands/database-type-configs.js.map +1 -0
  25. package/dist/src/commands/database-types.js +4 -4
  26. package/dist/src/commands/database-types.js.map +1 -1
  27. package/dist/src/commands/databases.d.ts +63 -0
  28. package/dist/src/commands/databases.js +649 -87
  29. package/dist/src/commands/databases.js.map +1 -1
  30. package/dist/src/commands/documents.js +869 -12
  31. package/dist/src/commands/documents.js.map +1 -1
  32. package/dist/src/commands/email-templates.js +21 -128
  33. package/dist/src/commands/email-templates.js.map +1 -1
  34. package/dist/src/commands/env.d.ts +12 -1
  35. package/dist/src/commands/env.js +115 -26
  36. package/dist/src/commands/env.js.map +1 -1
  37. package/dist/src/commands/feature-flags.d.ts +14 -0
  38. package/dist/src/commands/feature-flags.js +116 -0
  39. package/dist/src/commands/feature-flags.js.map +1 -0
  40. package/dist/src/commands/group-type-configs.js +14 -109
  41. package/dist/src/commands/group-type-configs.js.map +1 -1
  42. package/dist/src/commands/init.d.ts +24 -1
  43. package/dist/src/commands/init.js +1194 -279
  44. package/dist/src/commands/init.js.map +1 -1
  45. package/dist/src/commands/integrations.js +166 -435
  46. package/dist/src/commands/integrations.js.map +1 -1
  47. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  48. package/dist/src/commands/metadata-category-configs.js +112 -0
  49. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  50. package/dist/src/commands/metadata.js +33 -141
  51. package/dist/src/commands/metadata.js.map +1 -1
  52. package/dist/src/commands/prompts.js +206 -563
  53. package/dist/src/commands/prompts.js.map +1 -1
  54. package/dist/src/commands/rule-sets.d.ts +1 -1
  55. package/dist/src/commands/rule-sets.js +16 -132
  56. package/dist/src/commands/rule-sets.js.map +1 -1
  57. package/dist/src/commands/scripts.d.ts +10 -0
  58. package/dist/src/commands/scripts.js +203 -241
  59. package/dist/src/commands/scripts.js.map +1 -1
  60. package/dist/src/commands/sessions.js +17 -5
  61. package/dist/src/commands/sessions.js.map +1 -1
  62. package/dist/src/commands/sync-app-settings.d.ts +113 -70
  63. package/dist/src/commands/sync-app-settings.js +193 -242
  64. package/dist/src/commands/sync-app-settings.js.map +1 -1
  65. package/dist/src/commands/sync.d.ts +1904 -51
  66. package/dist/src/commands/sync.js +9468 -1790
  67. package/dist/src/commands/sync.js.map +1 -1
  68. package/dist/src/commands/tokens.js +8 -2
  69. package/dist/src/commands/tokens.js.map +1 -1
  70. package/dist/src/commands/users.js +92 -1
  71. package/dist/src/commands/users.js.map +1 -1
  72. package/dist/src/commands/vars.js +44 -58
  73. package/dist/src/commands/vars.js.map +1 -1
  74. package/dist/src/commands/webhooks.js +318 -156
  75. package/dist/src/commands/webhooks.js.map +1 -1
  76. package/dist/src/commands/workflows.d.ts +49 -12
  77. package/dist/src/commands/workflows.js +652 -1086
  78. package/dist/src/commands/workflows.js.map +1 -1
  79. package/dist/src/lib/access-rule-display.d.ts +21 -0
  80. package/dist/src/lib/access-rule-display.js +34 -0
  81. package/dist/src/lib/access-rule-display.js.map +1 -0
  82. package/dist/src/lib/api-client.d.ts +541 -61
  83. package/dist/src/lib/api-client.js +559 -102
  84. package/dist/src/lib/api-client.js.map +1 -1
  85. package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
  86. package/dist/src/lib/app-settings-descriptor.js +403 -82
  87. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  88. package/dist/src/lib/canonical-json.d.ts +12 -0
  89. package/dist/src/lib/canonical-json.js +35 -0
  90. package/dist/src/lib/canonical-json.js.map +1 -0
  91. package/dist/src/lib/cli-manifest.d.ts +8 -0
  92. package/dist/src/lib/cli-manifest.js +1 -0
  93. package/dist/src/lib/cli-manifest.js.map +1 -1
  94. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
  95. package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
  96. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  97. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
  98. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
  99. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  100. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  101. package/dist/src/lib/config-object-descriptor.js +658 -0
  102. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  103. package/dist/src/lib/config-payload.d.ts +85 -0
  104. package/dist/src/lib/config-payload.js +116 -0
  105. package/dist/src/lib/config-payload.js.map +1 -0
  106. package/dist/src/lib/config-surface.d.ts +130 -0
  107. package/dist/src/lib/config-surface.js +300 -0
  108. package/dist/src/lib/config-surface.js.map +1 -0
  109. package/dist/src/lib/credentials-store.d.ts +10 -0
  110. package/dist/src/lib/credentials-store.js +23 -0
  111. package/dist/src/lib/credentials-store.js.map +1 -1
  112. package/dist/src/lib/data-input.d.ts +23 -0
  113. package/dist/src/lib/data-input.js +50 -0
  114. package/dist/src/lib/data-input.js.map +1 -0
  115. package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
  116. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  117. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
  118. package/dist/src/lib/env-resolver-core.d.ts +187 -0
  119. package/dist/src/lib/env-resolver-core.js +325 -0
  120. package/dist/src/lib/env-resolver-core.js.map +1 -0
  121. package/dist/src/lib/env-resolver.d.ts +28 -6
  122. package/dist/src/lib/env-resolver.js +44 -32
  123. package/dist/src/lib/env-resolver.js.map +1 -1
  124. package/dist/src/lib/generated-allowlist.js +12 -0
  125. package/dist/src/lib/generated-allowlist.js.map +1 -1
  126. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  127. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  128. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  129. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  130. package/dist/src/lib/generated-template-lint.js +624 -0
  131. package/dist/src/lib/generated-template-lint.js.map +1 -0
  132. package/dist/src/lib/init-adopt.d.ts +16 -0
  133. package/dist/src/lib/init-adopt.js +34 -0
  134. package/dist/src/lib/init-adopt.js.map +1 -0
  135. package/dist/src/lib/init-assets.d.ts +39 -0
  136. package/dist/src/lib/init-assets.js +97 -0
  137. package/dist/src/lib/init-assets.js.map +1 -0
  138. package/dist/src/lib/init-client-platforms.d.ts +14 -0
  139. package/dist/src/lib/init-client-platforms.js +71 -0
  140. package/dist/src/lib/init-client-platforms.js.map +1 -0
  141. package/dist/src/lib/init-config.d.ts +44 -5
  142. package/dist/src/lib/init-config.js +80 -7
  143. package/dist/src/lib/init-config.js.map +1 -1
  144. package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
  145. package/dist/src/lib/init-email-redirect-uris.js +46 -0
  146. package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
  147. package/dist/src/lib/init-ios-links.d.ts +90 -0
  148. package/dist/src/lib/init-ios-links.js +218 -0
  149. package/dist/src/lib/init-ios-links.js.map +1 -0
  150. package/dist/src/lib/init-plan.d.ts +80 -0
  151. package/dist/src/lib/init-plan.js +95 -0
  152. package/dist/src/lib/init-plan.js.map +1 -0
  153. package/dist/src/lib/init-production-env.d.ts +48 -0
  154. package/dist/src/lib/init-production-env.js +59 -0
  155. package/dist/src/lib/init-production-env.js.map +1 -0
  156. package/dist/src/lib/init-schema.d.ts +74 -0
  157. package/dist/src/lib/init-schema.js +358 -0
  158. package/dist/src/lib/init-schema.js.map +1 -0
  159. package/dist/src/lib/init-xcode.d.ts +33 -0
  160. package/dist/src/lib/init-xcode.js +114 -0
  161. package/dist/src/lib/init-xcode.js.map +1 -0
  162. package/dist/src/lib/integration-request-config.d.ts +30 -0
  163. package/dist/src/lib/integration-request-config.js +145 -0
  164. package/dist/src/lib/integration-request-config.js.map +1 -0
  165. package/dist/src/lib/integration-selector.d.ts +42 -0
  166. package/dist/src/lib/integration-selector.js +46 -0
  167. package/dist/src/lib/integration-selector.js.map +1 -0
  168. package/dist/src/lib/local-state.d.ts +55 -0
  169. package/dist/src/lib/local-state.js +167 -0
  170. package/dist/src/lib/local-state.js.map +1 -0
  171. package/dist/src/lib/local-test-cases.d.ts +63 -0
  172. package/dist/src/lib/local-test-cases.js +135 -0
  173. package/dist/src/lib/local-test-cases.js.map +1 -0
  174. package/dist/src/lib/log-inspection.d.ts +568 -0
  175. package/dist/src/lib/log-inspection.js +639 -0
  176. package/dist/src/lib/log-inspection.js.map +1 -0
  177. package/dist/src/lib/migration-nag.d.ts +2 -2
  178. package/dist/src/lib/migration-nag.js +3 -3
  179. package/dist/src/lib/object-status-filter.d.ts +22 -0
  180. package/dist/src/lib/object-status-filter.js +45 -0
  181. package/dist/src/lib/object-status-filter.js.map +1 -0
  182. package/dist/src/lib/output.d.ts +24 -1
  183. package/dist/src/lib/output.js +42 -1
  184. package/dist/src/lib/output.js.map +1 -1
  185. package/dist/src/lib/package-manager.d.ts +140 -0
  186. package/dist/src/lib/package-manager.js +305 -0
  187. package/dist/src/lib/package-manager.js.map +1 -0
  188. package/dist/src/lib/paginate.d.ts +50 -0
  189. package/dist/src/lib/paginate.js +53 -0
  190. package/dist/src/lib/paginate.js.map +1 -1
  191. package/dist/src/lib/platform-owned.d.ts +29 -3
  192. package/dist/src/lib/platform-owned.js +42 -3
  193. package/dist/src/lib/platform-owned.js.map +1 -1
  194. package/dist/src/lib/project-config.d.ts +8 -0
  195. package/dist/src/lib/project-config.js +30 -5
  196. package/dist/src/lib/project-config.js.map +1 -1
  197. package/dist/src/lib/query-operators.d.ts +1 -1
  198. package/dist/src/lib/query-operators.js +1 -1
  199. package/dist/src/lib/record-filter.d.ts +18 -0
  200. package/dist/src/lib/record-filter.js +55 -0
  201. package/dist/src/lib/record-filter.js.map +1 -0
  202. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  203. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  204. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  205. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  206. package/dist/src/lib/resolve-init-server.js +77 -0
  207. package/dist/src/lib/resolve-init-server.js.map +1 -0
  208. package/dist/src/lib/resolve-platform.d.ts +43 -14
  209. package/dist/src/lib/resolve-platform.js +74 -12
  210. package/dist/src/lib/resolve-platform.js.map +1 -1
  211. package/dist/src/lib/run-status.d.ts +19 -0
  212. package/dist/src/lib/run-status.generated.d.ts +39 -0
  213. package/dist/src/lib/run-status.generated.js +66 -0
  214. package/dist/src/lib/run-status.generated.js.map +1 -0
  215. package/dist/src/lib/run-status.js +19 -0
  216. package/dist/src/lib/run-status.js.map +1 -0
  217. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  218. package/dist/src/lib/server-text-normalization.js +90 -0
  219. package/dist/src/lib/server-text-normalization.js.map +1 -0
  220. package/dist/src/lib/server-url.d.ts +22 -0
  221. package/dist/src/lib/server-url.js +33 -0
  222. package/dist/src/lib/server-url.js.map +1 -0
  223. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  224. package/dist/src/lib/signing-secret-status.js +116 -0
  225. package/dist/src/lib/signing-secret-status.js.map +1 -0
  226. package/dist/src/lib/skill-installer.js +6 -3
  227. package/dist/src/lib/skill-installer.js.map +1 -1
  228. package/dist/src/lib/snapshots.d.ts +2 -2
  229. package/dist/src/lib/snapshots.js +2 -2
  230. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
  231. package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
  232. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  233. package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
  234. package/dist/src/lib/swift-codegen/generator.js +181 -7
  235. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  236. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
  237. package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
  238. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
  239. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  240. package/dist/src/lib/sync-dir-selector.js +30 -0
  241. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  242. package/dist/src/lib/sync-paths.d.ts +39 -0
  243. package/dist/src/lib/sync-paths.js +70 -2
  244. package/dist/src/lib/sync-paths.js.map +1 -1
  245. package/dist/src/lib/sync-resource-types.d.ts +138 -31
  246. package/dist/src/lib/sync-resource-types.js +241 -47
  247. package/dist/src/lib/sync-resource-types.js.map +1 -1
  248. package/dist/src/lib/sync-selectors.d.ts +95 -0
  249. package/dist/src/lib/sync-selectors.js +228 -0
  250. package/dist/src/lib/sync-selectors.js.map +1 -0
  251. package/dist/src/lib/template.d.ts +62 -4
  252. package/dist/src/lib/template.js +147 -39
  253. package/dist/src/lib/template.js.map +1 -1
  254. package/dist/src/lib/test-case-file-names.d.ts +40 -0
  255. package/dist/src/lib/test-case-file-names.js +91 -0
  256. package/dist/src/lib/test-case-file-names.js.map +1 -0
  257. package/dist/src/lib/test-case-keys.d.ts +29 -0
  258. package/dist/src/lib/test-case-keys.js +55 -0
  259. package/dist/src/lib/test-case-keys.js.map +1 -0
  260. package/dist/src/lib/test-case-variables.d.ts +15 -0
  261. package/dist/src/lib/test-case-variables.js +29 -0
  262. package/dist/src/lib/test-case-variables.js.map +1 -0
  263. package/dist/src/lib/toml-database-config.d.ts +4 -4
  264. package/dist/src/lib/toml-database-config.js +27 -34
  265. package/dist/src/lib/toml-database-config.js.map +1 -1
  266. package/dist/src/lib/toml-metadata-config.d.ts +9 -5
  267. package/dist/src/lib/toml-metadata-config.js +40 -45
  268. package/dist/src/lib/toml-metadata-config.js.map +1 -1
  269. package/dist/src/lib/toml-native-form.d.ts +1 -1
  270. package/dist/src/lib/toml-native-form.js +1 -1
  271. package/dist/src/lib/toml-params-validator.d.ts +1 -1
  272. package/dist/src/lib/toml-params-validator.js +1 -1
  273. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  274. package/dist/src/lib/toml-scalar-edit.js +283 -0
  275. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  276. package/dist/src/lib/user-selector.d.ts +24 -0
  277. package/dist/src/lib/user-selector.js +33 -0
  278. package/dist/src/lib/user-selector.js.map +1 -0
  279. package/dist/src/lib/version-check.d.ts +25 -0
  280. package/dist/src/lib/version-check.js +77 -8
  281. package/dist/src/lib/version-check.js.map +1 -1
  282. package/dist/src/lib/watch.d.ts +121 -0
  283. package/dist/src/lib/watch.js +169 -0
  284. package/dist/src/lib/watch.js.map +1 -0
  285. package/dist/src/lib/web-url.d.ts +40 -0
  286. package/dist/src/lib/web-url.js +76 -0
  287. package/dist/src/lib/web-url.js.map +1 -0
  288. package/dist/src/lib/workflow-apply.d.ts +69 -45
  289. package/dist/src/lib/workflow-apply.js +99 -72
  290. package/dist/src/lib/workflow-apply.js.map +1 -1
  291. package/dist/src/lib/workflow-codegen/generator.js +4 -4
  292. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  293. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
  294. package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
  295. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  296. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  297. package/dist/src/lib/workflow-config-apply.js +137 -0
  298. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  299. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  300. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  301. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  302. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  303. package/dist/src/lib/workflow-defaults.js +41 -0
  304. package/dist/src/lib/workflow-defaults.js.map +1 -0
  305. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  306. package/dist/src/lib/workflow-include-preserve.js +286 -0
  307. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  308. package/dist/src/lib/workflow-payload.d.ts +31 -20
  309. package/dist/src/lib/workflow-payload.js +100 -110
  310. package/dist/src/lib/workflow-payload.js.map +1 -1
  311. package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
  312. package/dist/src/lib/workflow-toml-validator.js +141 -16
  313. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  314. package/dist/src/types/index.d.ts +78 -22
  315. package/dist/src/validators.d.ts +7 -7
  316. package/dist/src/validators.js +6 -6
  317. package/package.json +11 -6
  318. package/dist/src/commands/settings.d.ts +0 -15
  319. package/dist/src/commands/settings.js +0 -102
  320. package/dist/src/commands/settings.js.map +0 -1
@@ -1,6 +1,13 @@
1
1
  import { Command } from "commander";
2
2
  import { ApiClient } from "../lib/api-client.js";
3
- import { type PresenceOutcome } from "../lib/sync-resource-types.js";
3
+ import { type OperationFormHints } from "../lib/toml-database-config.js";
4
+ import { type FieldForm } from "../lib/toml-native-form.js";
5
+ import { type PushMode } from "../lib/config-payload.js";
6
+ import { type ConfigObjectSurface, type ConfigTable } from "../lib/generated-config-surfaces.js";
7
+ import { type PresenceOutcome, type TestBlockType } from "../lib/sync-resource-types.js";
8
+ export { resolveSlugCollisions, slugifyTestCaseName, testCaseFileBasenames, } from "../lib/test-case-file-names.js";
9
+ import type { SyncState } from "../types/index.js";
10
+ import { type SyncSelection } from "../lib/sync-selectors.js";
4
11
  /**
5
12
  * Wrap a server-side error so the printed message identifies which entity
6
13
  * was in flight. Used by every entity create/update/delete call site in the
@@ -13,8 +20,30 @@ import { type PresenceOutcome } from "../lib/sync-resource-types.js";
13
20
  * - Other errors are rewrapped as a new `ApiError` carrying the wrapped
14
21
  * message + the original `details[]` and `statusCode`.
15
22
  */
16
- export declare function wrapEntityError(err: unknown, action: "create" | "update" | "delete", kind: string, key: string): Error;
23
+ export declare function wrapEntityError(err: unknown, action: "create" | "update" | "delete" | "activate", kind: string, key: string): Error;
24
+ /**
25
+ * Attribute a `[[configs]]` failure to the prompt AND the config (#2972).
26
+ *
27
+ * The server describes such a failure in terms of the entry alone ("Config
28
+ * name already exists for this prompt"), and push applies a prompt through
29
+ * several requests, so a run over a tree of prompts printed one bare line the
30
+ * operator could only attribute by reading the create log above it. The pair
31
+ * is the same identity the server stores the entry under (`promptId#configName`).
32
+ *
33
+ * The result is marked `entityNamed`, so the prompt-level wrapper around the
34
+ * shared update body leaves it alone.
35
+ */
36
+ export declare function wrapPromptConfigError(err: unknown, action: "create" | "update" | "activate", promptKey: string, configName: string | undefined): Error;
17
37
  export declare function computeFileHash(filePath: string): string;
38
+ /**
39
+ * A transform's comparison hash: the script body itself (#2731 B7).
40
+ *
41
+ * Unlike the config types there is no projection to do — the `.rhai` bytes are
42
+ * exactly what push sends and exactly what diff compares — so the "semantic"
43
+ * hash is just a stable hash of the body, computable from a server response
44
+ * without going through the file.
45
+ */
46
+ export declare function computeScriptBodyHash(body: string): string;
18
47
  export declare function shouldPushFile(filePath: string, storedHash: string | undefined): boolean;
19
48
  /**
20
49
  * Hash the *expanded* (post-fragment-splice) content of a workflow TOML.
@@ -38,17 +67,70 @@ export declare function computeExpandedContentHash(parsed: any): string;
38
67
  */
39
68
  export declare function shouldPushExpandedFile(parsed: any, storedHash: string | undefined): boolean;
40
69
  /**
41
- * Normalize a parsed workflow TOML object so that any of the
42
- * serializer-emitted default fields the *local* file omits are filled in with
43
- * the model default. Used by `diff` so a hand-authored workflow that omits e.g.
44
- * `perUserMaxRunning` compares equal to a server that defaulted it to 4
45
- * (the #1175 false-positive guard) while a server value that was
46
- * *explicitly set* to a non-default (e.g. 8) still shows as Modified.
70
+ * The `[workflow].activeConfigName` a file actually states, trimmed or
71
+ * `undefined` when the file has no opinion about activation.
72
+ *
73
+ * Push reads the key the same way: `applyWorkflowConfigSidecars` activates a
74
+ * configuration only for a truthy name and otherwise leaves the running one
75
+ * alone (`cli/src/lib/workflow-config-apply.ts`). Every diff decision about
76
+ * activation keys off this one predicate so the comparison cannot claim a
77
+ * difference push has no way to reconcile (#2743, review follow-up).
78
+ */
79
+ export declare function authoredActiveConfigName(parsed: any): string | undefined;
80
+ /**
81
+ * The configuration a server workflow is RUNNING — its `activeConfigId`, or the
82
+ * first config as the fallback `serializeWorkflow` has always used, so pull's
83
+ * `activeConfigName` and the diff's reading of it can never disagree.
84
+ */
85
+ export declare function resolveActiveWorkflowConfig(workflow: any, configs: any[]): any;
86
+ /**
87
+ * Normalize a parsed workflow TOML object into the form the SERVER would hold
88
+ * after a `config push` of that file. Used by `diff` so a hand-authored workflow
89
+ * compares equal to the running state it already describes — while a server
90
+ * value that genuinely differs still shows as Modified.
91
+ *
92
+ * In order:
93
+ * 1. Trim the fields the server trims (`name`, `description`).
94
+ * 2. Drop explicit-empty authored values — a `""` string or an empty
95
+ * `capabilities` array. Push sends these as `null` (or the model default),
96
+ * and the pull serializer omits the key for a cleared field, so "spelled
97
+ * out as empty" and "absent" have to hash the same (#2743).
98
+ * 3. Lowercase the two enums and floor the five queue limits, mirroring the
99
+ * server's accepted-value canonicalization
100
+ * (`src/workflows/config/workflow-field-handlers.ts`). A value the server
101
+ * would REJECT is left alone: push fails loudly for those, so there is
102
+ * nothing to preview. #2743 — without this, `dequeueOrder = "LIFO"` or
103
+ * `perUserMaxRunning = 4.9` pushes fine and then reports Modified forever.
104
+ * 4. Fill the model defaults (`WORKFLOW_MODEL_DEFAULTS`) for what the file
105
+ * omits. `perAppMax*` / `queueTtlSeconds` joined that list in #1177: once
106
+ * the pull serializer emitted them (a GET always returns them, they carry
107
+ * a non-null model default), an unchanged workflow hashed unequal to a
108
+ * local TOML that omits them — a false `modified` that made `config pull`
109
+ * rewrite the file just to inject defaults. (`status` was on this list
110
+ * until #2803 took availability off the config wire entirely.)
111
+ * 5. Fill `activeConfigName` with the name the server itself creates
112
+ * (`default`), so a hand-authored file that never named a config compares
113
+ * equal to the workflow that pushing it produces (#2743). This is the
114
+ * compare-BY-NAME case only: when the local file names no configuration at
115
+ * all, `config diff` hashes both sides with `compareActivation: false` and
116
+ * the fill never decides anything.
117
+ * 6. Fill `key` from `resolvedKey` when the file omits it. `[workflow].key`
118
+ * is optional — push and diff both resolve it from the FILENAME
119
+ * (`tomlData.workflow?.key || basename(file, ".toml")`) — while the pull
120
+ * serializer always emits it. Without this a file relying on the filename
121
+ * hashes unequal to the very workflow it describes and reports Modified
122
+ * forever, which is exactly the non-convergence #2743 removes. Callers
123
+ * that already know the resolved key pass it; the rest leave the key as
124
+ * authored.
125
+ *
126
+ * Only the DIFF-hash path (`hashWorkflowTomlForDiff`) normalizes; the stored
127
+ * content-hash path that push's skip check compares is untouched, so upgrading
128
+ * forces no one-time re-push (same note as #1446's schema canonicalization).
47
129
  *
48
130
  * Returns a shallow clone with a normalized `workflow` table; the input is
49
131
  * not mutated. Non-workflow TOML (no `workflow` table) is returned unchanged.
50
132
  */
51
- export declare function normalizeWorkflowTomlDefaults(parsed: any): any;
133
+ export declare function normalizeWorkflowTomlDefaults(parsed: any, resolvedKey?: string): any;
52
134
  /**
53
135
  * Canonical content hash of a workflow TOML for `diff`'s content comparison.
54
136
  * Applies `normalizeWorkflowTomlDefaults` first so omitted-vs-defaulted fields
@@ -56,17 +138,42 @@ export declare function normalizeWorkflowTomlDefaults(parsed: any): any;
56
138
  * pull stores and push compares. Both the local file and the
57
139
  * `serializeWorkflow`-produced remote form flow through this single function,
58
140
  * so the two sides are normalized identically by construction.
141
+ *
142
+ * `resolvedKey` is the key the caller already paired the two sides on (the
143
+ * `[workflow].key`, or the filename when the file omits it). Passing it lets a
144
+ * file that relies on the filename fallback hash equal to the remote form,
145
+ * which always spells the key out — see `normalizeWorkflowTomlDefaults`.
146
+ *
147
+ * `compareActivation: false` drops `activeConfigName` from the hash. `config diff`
148
+ * passes it when the local file names no configuration: push activates nothing
149
+ * for such a file, so the configuration the server happens to be running is not
150
+ * a difference push could ever reconcile, and hashing it in would report the
151
+ * workflow Modified after every successful push — the non-convergence #2743
152
+ * exists to remove (review follow-up). The remote name stays visible as a hint
153
+ * on the Synced row instead. Defaults to comparing, so the include-reconcile
154
+ * path (`reconcileWorkflowIncludes`, which asks whether the local file already
155
+ * expands to the running state) keeps seeing activation as content.
59
156
  */
60
- export declare function hashWorkflowTomlForDiff(parsed: any): string;
157
+ export declare function hashWorkflowTomlForDiff(parsed: any, resolvedKey?: string, options?: {
158
+ compareActivation?: boolean;
159
+ }): string;
61
160
  /**
62
161
  * Build the canonical content hash for a *server* workflow (as returned by
63
162
  * `getWorkflow` + active `getWorkflowConfig`), mirroring exactly what a fresh
64
- * `sync pull` would write to disk. Serializes via `serializeWorkflow`, parses
163
+ * `config pull` would write to disk. Serializes via `serializeWorkflow`, parses
65
164
  * the resulting TOML (remote serialized TOML never carries `include`s, so a
66
165
  * plain `parseConfigToml` is sufficient — no fragment path needed), then hashes
67
166
  * through `hashWorkflowTomlForDiff` so it lines up with the local-file hash.
167
+ *
168
+ * `logger` is optional but `config diff` passes it: the serializer's
169
+ * unrecognized-server-key warning (#2644) is the only signal that this CLI
170
+ * version cannot represent a field the server holds, and a diff that swallowed
171
+ * it would report the workflow as in sync with no hint that anything is
172
+ * missing.
68
173
  */
69
- export declare function hashRemoteWorkflowForDiff(workflow: any, draft: any, configs: any[]): string;
174
+ export declare function hashRemoteWorkflowForDiff(workflow: any, draft: any, configs: any[], logger?: (message: string) => void, options?: {
175
+ compareActivation?: boolean;
176
+ }): string;
70
177
  export interface ParsedAuthSettings {
71
178
  settings: Record<string, any>;
72
179
  warnings: string[];
@@ -78,8 +185,7 @@ export interface ParsedAuthSettings {
78
185
  * (`cli/src/lib/app-settings-descriptor.ts`), which now owns the [auth] field
79
186
  * set that `AUTH_BOOLEAN_KEYS`/`RECOGNIZED_AUTH_KEYS` used to hard-code. The
80
187
  * descriptor drives all four sections in one place, so pull, push, and the
81
- * unrecognized-key warning can never drift apart. `googleClientSecret` is a
82
- * secret descriptor entry and is never written.
188
+ * unrecognized-key warning can never drift apart.
83
189
  */
84
190
  export declare function serializeAuthBlock(settings: any): Record<string, any>;
85
191
  /**
@@ -87,7 +193,7 @@ export declare function serializeAuthBlock(settings: any): Record<string, any>;
87
193
  * direction). Kept as a focused helper over the shared descriptor; the full
88
194
  * push path uses `parseTomlToAppSettings` for every section. Only keys present
89
195
  * in the TOML are forwarded, so an omitted key never overwrites server state.
90
- * Descriptor errors (a secret key, a type mismatch) are surfaced alongside the
196
+ * Descriptor errors (a type mismatch) are surfaced alongside the
91
197
  * unrecognized-key warnings.
92
198
  */
93
199
  export declare function parseAppAuthSettings(auth: Record<string, any>): ParsedAuthSettings;
@@ -96,7 +202,7 @@ type VarEntity = {
96
202
  contentHash?: string;
97
203
  };
98
204
  /**
99
- * Decide whether `sync pull` should (over)write `vars.toml`, given the outcome
205
+ * Decide whether `config pull` should (over)write `vars.toml`, given the outcome
100
206
  * of fetching the app's config vars.
101
207
  *
102
208
  * The distinction that matters (issue #1423 review): a SUCCESSFUL fetch that
@@ -120,23 +226,94 @@ export declare function planVarsPull(outcome: {
120
226
  content: string | null;
121
227
  varEntities: Record<string, VarEntity>;
122
228
  };
229
+ /**
230
+ * `config pull --only var/<KEY>` — the same plan, narrowed to the named keys
231
+ * (issue #2645).
232
+ *
233
+ * `vars.toml` is one file for the whole var surface, so a scoped pull cannot
234
+ * just re-emit what the server returned: that would drop every var the
235
+ * operator did not name, including local edits they have not pushed yet. The
236
+ * merge is therefore local-file-first — the file's current keys are kept as
237
+ * they are, and only the SELECTED keys are taken from the server (or removed,
238
+ * when the server no longer has them).
239
+ *
240
+ * Pure like `planVarsPull`, for the same reason: this is the one place a pull
241
+ * can silently discard an author's work, so it has to be testable without a
242
+ * live server.
243
+ */
244
+ export declare function planScopedVarsPull(serverVars: Array<{
245
+ key: string;
246
+ value?: string;
247
+ updatedAt?: string;
248
+ modifiedAt?: string;
249
+ }>, selectedKeys: ReadonlySet<string>, localVars: Record<string, string>, priorVars?: Record<string, VarEntity>): {
250
+ content: string;
251
+ varEntities: Record<string, VarEntity>;
252
+ };
253
+ /**
254
+ * Validate a `config push`'s webhook writes against the server's per-app cap.
255
+ * Returns a list of human-readable errors (empty when the push may proceed).
256
+ *
257
+ * Run in `config push`'s up-front preflight (the `planVarsPush` cap pattern) so a
258
+ * push that would run past the cap aborts BEFORE any mutation. Without it the
259
+ * push creates webhooks one at a time until the server rejects one at the cap,
260
+ * leaving a partial push.
261
+ *
262
+ * **Only creates are capped.** The server grandfathers apps that already hold
263
+ * more webhooks than the cap: it refuses new creates and nothing else. A plain
264
+ * file count would break that — `config pull` writes one toml per webhook, so a
265
+ * grandfathered app pulls more files than the cap and could then never push
266
+ * again, its workflows, databases and vars included. So the check compares the
267
+ * local keys against the keys already on the server: a push that creates
268
+ * nothing new is always allowed, however far over the cap the app is, and a
269
+ * push that does create is rejected only when the resulting row count would
270
+ * exceed the cap.
271
+ *
272
+ * The remote key list this runs against comes from `GET
273
+ * /admin/api/apps/{appId}/webhooks`, which applies no default status filter, so
274
+ * archived rows are in `remoteKeys` exactly as they are in the server's count.
275
+ * The two therefore agree on what is already on the server.
276
+ *
277
+ * Still advisory rather than authoritative: this counts creates the way the
278
+ * *config dir* implies, while the apply loop decides create-vs-update from
279
+ * local sync state, so a checkout with missing sync state issues creates this
280
+ * check did not predict. Those creates name keys the server already holds, and
281
+ * the server does not cap a create that adds no row — it answers 409
282
+ * `WEBHOOK_KEY_EXISTS`, which `adoptByKeyOnCreate409` converges on. A push
283
+ * allowed here can still be rejected server-side (concurrent writes, a stale
284
+ * list); that rejection carries `WEBHOOK_LIMIT_REACHED` and names the limit.
285
+ *
286
+ * Planned prunes are deliberately NOT netted out of the count: `--prune` runs
287
+ * after the webhook create loop, so a push that only fits once its prunes land
288
+ * would still fail mid-apply. Prune first, then push the additions.
289
+ */
290
+ export declare function validateWebhookCapForPush(localKeys: string[], remoteKeys: string[]): string[];
123
291
  /**
124
292
  * Validate a parsed `vars.toml` table against the same key/value constraints
125
293
  * the server enforces (key format, string type, non-empty, size cap) plus the
126
294
  * aggregate per-app var-count cap. Returns a list of human-readable errors
127
295
  * (empty when valid).
128
296
  *
129
- * Run in `sync push`'s up-front preflight pass (issue #1423 review) so an
297
+ * Run in `config push`'s up-front preflight pass (issue #1423 review) so an
130
298
  * invalid entry — or a file that would exceed the server's `MAX_VARS_PER_APP`
131
299
  * cap — aborts BEFORE any mutation is applied. Validating only individual
132
300
  * entries let a 101-entry file create/update many vars before the server
133
301
  * rejected a later create at the cap, leaving a partial push (issue #1423
134
302
  * review pass 3). Pure and side-effect-free so it's unit-testable without a
135
303
  * live server.
304
+ *
305
+ * `onlyKeys` narrows the check to the keys a `config push --only var/<key>` will
306
+ * actually write (issue #2645). A single-object push must not be aborted by an
307
+ * entry it is not going to touch, so the per-entry checks run over the selected
308
+ * keys and the file-wide count cap — a property of the whole file, not of these
309
+ * entries — is left to the plan-level check the caller makes against the final
310
+ * server count.
136
311
  */
137
- export declare function validateVarsFile(parsedVars: Record<string, unknown>): string[];
312
+ export declare function validateVarsFile(parsedVars: Record<string, unknown>, options?: {
313
+ onlyKeys?: ReadonlySet<string>;
314
+ }): string[];
138
315
  /**
139
- * Plan the config-var writes a `sync push` should make, detecting concurrent
316
+ * Plan the config-var writes a `config push` should make, detecting concurrent
140
317
  * remote edits before overwriting them (issue #1423 review).
141
318
  *
142
319
  * This is the client-side half of a two-layer concurrency guard. The vars
@@ -156,7 +333,7 @@ export declare function validateVarsFile(parsedVars: Record<string, unknown>): s
156
333
  * removed locally whose remote value no longer matches the baseline was edited
157
334
  * remotely since the last sync, and deleting it would silently discard that
158
335
  * edit — reported as a conflict instead. `force` skips the conflict guard,
159
- * matching `sync push --force` for every other entity.
336
+ * matching `config push --force` for every other entity.
160
337
  *
161
338
  * Pure and side-effect-free (the caller owns the API calls and sync-state
162
339
  * updates) so the guard is unit-testable without a live server. Assumes
@@ -211,11 +388,11 @@ export declare function countVarsAfterPush(remoteVars: Array<{
211
388
  deletions: string[];
212
389
  }): number;
213
390
  /**
214
- * Compute the `sync diff` rows for config vars (issue #1423 review). Before
215
- * this, `sync diff` ignored vars entirely, so an add/remove/value-drift between
391
+ * Compute the `config diff` rows for config vars (issue #1423 review). Before
392
+ * this, `config diff` ignored vars entirely, so an add/remove/value-drift between
216
393
  * the local `vars.toml` and the server read as no difference. Value-aware: a
217
394
  * var present on both sides whose value differs is reported as `modified`
218
- * (framed like the other content-aware entities — `sync pull` would rewrite the
395
+ * (framed like the other content-aware entities — `config pull` would rewrite the
219
396
  * local value). Pure and side-effect-free so it's unit-testable without a live
220
397
  * server.
221
398
  */
@@ -225,10 +402,386 @@ export declare function diffVars(localVars: Map<string, string>, remoteVars: Map
225
402
  status: string;
226
403
  hint?: string;
227
404
  }>;
228
- export declare function serializeWebhook(webhook: any): string;
405
+ /**
406
+ * Which of a workflow's configs `config pull` writes as sidecar files
407
+ * (issue #2645, spec §Contracts/Named configs).
408
+ *
409
+ * Every config EXCEPT the active one. The active config's body is already the
410
+ * `steps` at the top of `workflows/<key>.toml` — that is where authors edit it
411
+ * and where `applyWorkflowBody` writes it — so emitting it again as a sidecar
412
+ * would put the same body in two files and create a mismatch to adjudicate on
413
+ * the next push. Two declarations of one piece of state is the shape of problem
414
+ * this whole issue removes; it would be perverse to introduce one here.
415
+ *
416
+ * A config whose steps were not fetched is SKIPPED rather than written empty.
417
+ * A sidecar is a claim about the server, and `steps = []` is a claim that would
418
+ * tell the next push to blank a live configuration.
419
+ *
420
+ * Pure, so the selection is unit-testable without a live server.
421
+ */
422
+ export declare function workflowConfigSidecarsToWrite(workflow: any, configs: any[]): Array<{
423
+ name: string;
424
+ content: string;
425
+ }>;
426
+ /**
427
+ * The names a workflow's configs occupy on the server, whatever their body.
428
+ *
429
+ * `workflowConfigSidecarsToWrite` deliberately skips a config whose steps were
430
+ * not fetched, so it cannot answer "which sidecar files are stale". This can:
431
+ * a local file whose name is absent HERE names a config the server no longer
432
+ * has, which is the only safe basis for deleting it (issue #2645, review
433
+ * follow-up).
434
+ */
435
+ export declare function workflowConfigNamesOnServer(workflow: any, configs: any[]): Set<string>;
436
+ /**
437
+ * Delete the sidecars naming a config the server no longer has (#2645).
438
+ *
439
+ * Pull is what makes the repo describe the server, and a sidecar left behind
440
+ * after its config was archived — or after that config became the active one,
441
+ * whose body is the workflow file's `steps` — is a file that would RE-CREATE it
442
+ * on the next push. The repo would then be silently undoing the archive.
443
+ *
444
+ * Returns the file names removed, so pull can say what it did.
445
+ */
446
+ export declare function removeStaleWorkflowConfigSidecars(configDir: string, key: string, namesOnServer: Set<string>): string[];
447
+ /**
448
+ * The content hash `config push` skips a workflow on — sidecars included.
449
+ *
450
+ * The stored hash used to cover `workflows/<key>.toml` alone, so editing or
451
+ * adding a named config sidecar without touching the workflow file left the
452
+ * hash unchanged: push reported success and made no call at all, which is
453
+ * worse than failing (issue #2645, review follow-up). The named configs are
454
+ * part of what push applies, so they are part of what push compares.
455
+ *
456
+ * A workflow with no sidecars hashes exactly as before, so the common case
457
+ * does not re-push once on upgrade. An unreadable sidecar hashes to a distinct
458
+ * value rather than throwing: a hash is a comparison, and the parse error
459
+ * belongs to the apply path, which raises it with the file name attached.
460
+ */
461
+ export declare function workflowHashInput(configDir: string, key: string, parsedWorkflowToml: any): any;
462
+ /** See `workflowHashInput`. */
463
+ export declare function computeWorkflowContentHash(configDir: string, key: string, parsedWorkflowToml: any): string;
464
+ /**
465
+ * Canonical hash of a named config sidecar, for `config diff` (#2645).
466
+ *
467
+ * Hashes the parsed body rather than the bytes so comment and key-order
468
+ * differences do not read as drift — the same semantic comparison `config diff`
469
+ * makes everywhere else (spec criterion 3).
470
+ */
471
+ export declare function hashWorkflowConfigSidecar(parsed: any): string;
472
+ /**
473
+ * Does the file's `[workflow].activeConfigName` name a configuration that the
474
+ * push could actually activate (#2743, review follow-up)?
475
+ *
476
+ * The diff normalizer equates an omitted `activeConfigName` with `default` —
477
+ * the name the server itself gives the config it creates — so a file that
478
+ * spells `default` out hashes equal to the omitted form. For a workflow with no
479
+ * configuration at all (a legacy row the GET's auto-migration could not heal),
480
+ * the remote side serializes an EMPTY active config name and normalizes to
481
+ * `default` too, so the pair hashes equal while `config push` would stop on
482
+ * `activeConfigName "default" names no configuration`. Reporting Synced for a
483
+ * file whose next push fails is the one drift a change gate must not skip.
484
+ *
485
+ * A name the repo carries as a sidecar resolves: push creates that config and
486
+ * activates it (and the sidecar comparison already reports the difference).
487
+ * Read-only and non-throwing, like every other diff predicate.
488
+ */
489
+ export declare function workflowActiveConfigNameUnresolvable(configDir: string, key: string, localParsed: any, configs: any[]): boolean;
490
+ /**
491
+ * The `config diff` note for a workflow whose file names no configuration while
492
+ * the server runs one other than the `default` it creates itself (#2743, review
493
+ * follow-up).
494
+ *
495
+ * Such a file is not drift push can act on: activation is preserved, not
496
+ * converged, so the row is Synced — the alternative, reporting Modified,
497
+ * repeats forever because no push changes it. But the repo does not describe
498
+ * which configuration is live, and only `config pull` records it, so the fact is
499
+ * carried as a hint on the Synced row — the same way an operational disable is
500
+ * (`outOfServiceHint`). The server-created `default` is the shape a
501
+ * hand-authored file already means, so it is silent.
502
+ *
503
+ * Pure and non-throwing, like every other diff predicate. Returns the existing
504
+ * hint untouched when there is nothing to add.
505
+ */
506
+ export declare function unmanagedActiveConfigHint(localParsed: any, workflow: any, configs: any[], existingHint?: string): string | undefined;
507
+ /**
508
+ * Drop the archived rows from a pull listing (#2803).
509
+ *
510
+ * A tombstone is retired, not configuration. Exporting one wrote a file the
511
+ * repo then claimed to describe — and, once `status` left the TOML surface, a
512
+ * file whose very next push would fail on the `status` line it carried.
513
+ * `config push --prune` is the path that reclaims an archived row; pull's job
514
+ * is to describe what is configured.
515
+ */
516
+ export declare function skipArchivedForPull<T extends {
517
+ status?: unknown;
518
+ }>(items: T[]): T[];
519
+ /**
520
+ * The sync-state entries a pull keeps for the archived rows it did NOT export
521
+ * (#2803).
522
+ *
523
+ * Pull removes a tombstone's file, and prune candidates come only from prior
524
+ * sync state — so dropping the state entry with the file would strand the row:
525
+ * no file to delete, no managed key to prune, and (for webhooks and cron
526
+ * triggers) a cap slot and a reserved key nothing could ever reclaim, which is
527
+ * the exact failure #2232 fixed. So the pull records the tombstone instead: no
528
+ * file, but a managed entry carrying the archived row's CURRENT `modifiedAt`,
529
+ * so the next opt-in, confirmed `config push --prune` point-reads it, finds it
530
+ * unchanged, and hard-deletes it — the reclamation path the model promises.
531
+ *
532
+ * Only keys the config tree already managed are recorded. A row archived
533
+ * server-side that this repo never described is not this repo's to delete.
534
+ *
535
+ * Pure, so it is unit-testable without a live server.
536
+ */
537
+ export declare function archivedTombstoneEntries(items: any[], select: {
538
+ keyOf: (item: any) => string;
539
+ idOf: (item: any) => string;
540
+ }, managedKeys: Iterable<string>): Record<string, {
541
+ id: string;
542
+ modifiedAt: string;
543
+ }>;
544
+ /**
545
+ * Fold one type's tombstone entries into the sync state this pull is building
546
+ * (#2803, corrected by #2887).
547
+ *
548
+ * Two rules, and the second one is the fix. A tombstone is recorded only for a
549
+ * key this pull did NOT export (an exported key is a live row and its own
550
+ * record) and only when the selection covers it. And a recorded key is marked
551
+ * as MATCHED: archived rows are dropped before `selectPull` runs, so the
552
+ * selector that named one never reached the set that decides whether the pull
553
+ * found what it was asked for. `config pull --only workflow/<key>` against an
554
+ * archived managed row therefore removed the file, saved the tombstone, and
555
+ * then exited 1 saying "the server has no workflow/<key>" — a failure report
556
+ * about a mutation that had already succeeded, which is the worst thing an
557
+ * automation can be told. The fix is generic, so integrations and webhooks
558
+ * (which had the bug first) get it too.
559
+ *
560
+ * Mutates `built` and `matchedSelectors` and returns the keys it recorded, so
561
+ * the caller can report each one. Pure otherwise, and unit-testable.
562
+ */
563
+ export declare function recordArchivedTombstones(built: Record<string, {
564
+ id: string;
565
+ modifiedAt: string;
566
+ }>, tombstones: Record<string, {
567
+ id: string;
568
+ modifiedAt: string;
569
+ }>, context: {
570
+ label: string;
571
+ selection: SyncSelection | null;
572
+ matchedSelectors: Set<string>;
573
+ }): string[];
574
+ /**
575
+ * The `config diff` annotation for an object an operator took out of service
576
+ * (issue #2645, criterion 9; #2803).
577
+ *
578
+ * Availability is deliberately not a TOML key, so an object that is out of
579
+ * service is NOT drift — the committed configuration still describes it
580
+ * exactly. Saying so out loud is the point: the reason the control exists is
581
+ * that an out-of-band write used to make `config diff` report a difference
582
+ * nobody intentionally created (#1976). Reporting it as a hint on an
583
+ * otherwise-`Synced` row is what keeps that promise visible instead of merely
584
+ * true.
585
+ *
586
+ * Reads either spelling: the canonical `status: "inactive"` for a type that has
587
+ * cut over to #2803's single control, and the superseded
588
+ * `operationallyDisabled` boolean for one that has not yet.
589
+ *
590
+ * An ARCHIVED row gets its own annotation. `config pull` no longer exports
591
+ * tombstones, so without one an archived webhook or integration whose file is
592
+ * still in the tree read as plain `Synced` — the one status where "the server
593
+ * has exactly what the file says" is most misleading.
594
+ *
595
+ * When the row already carries a hint — a real content difference — the toggle
596
+ * is APPENDED rather than substituted: the drift is the part that needs acting
597
+ * on, and dropping it to announce the pause would hide the more urgent fact.
598
+ *
599
+ * Pure, so it is unit-testable without a live server.
600
+ */
601
+ export declare function outOfServiceHint(remote: any, existingHint?: string): string | undefined;
602
+ /**
603
+ * Serialize an integration DETAIL response to its `integrations/<key>.toml`.
604
+ *
605
+ * #2631: `accessRule` is the CEL expression gating who may call the
606
+ * integration. It comes back on the detail only (not the list summary), and it
607
+ * is written to TOML so `config pull` → `config push` round-trips the rule instead
608
+ * of clearing it. A null/empty rule has no TOML representation and is simply
609
+ * omitted; the push path sends `null` for an omitted rule, which matches the
610
+ * server state it came from.
611
+ */
612
+ export declare function serializeIntegration(integration: any, logger?: (message: string) => void): string;
613
+ /**
614
+ * Build the `config push` body for one integration TOML.
615
+ *
616
+ * `accessRule` follows the #1567 TOML-owned rule: send value-or-`null` so
617
+ * removing the line from the file clears the rule server-side rather than
618
+ * leaving a stale one live. There is deliberately NO local "rule is required"
619
+ * check here — a remote integration missing from local sync state is pushed as
620
+ * a create, expected to 409, and adopted by key (#1006); a local check would
621
+ * strand that path. The server decides (#2631).
622
+ */
623
+ export declare function buildIntegrationPushPayload(key: string, tomlData: any, { mode }?: {
624
+ mode?: PushMode;
625
+ }): Record<string, any>;
626
+ export declare function webhookConfigToTomlTable(scheme: string, config: any): Record<string, any> | undefined;
627
+ export declare function tomlTableToWebhookConfig(scheme: string, table: any): Record<string, any> | undefined;
628
+ export declare function serializeWebhook(webhook: any, logger?: (message: string) => void): string;
629
+ /**
630
+ * Build the `config push` body for one webhook TOML.
631
+ *
632
+ * The `[webhook]`-derived half comes from the vendored definition; the four
633
+ * structural tables the file also carries (`[allowedIps]`, `[inputMapping]`,
634
+ * `[metadata]`, `[verification.<scheme>]`) keep their own conversion, which is
635
+ * why the definition declares them `structural` rather than as fields.
636
+ */
637
+ export declare function buildWebhookPushPayload(key: string, tomlData: any, { mode }?: {
638
+ mode?: PushMode;
639
+ }): Record<string, any>;
640
+ export declare function serializeCronTrigger(trigger: any, logger?: (message: string) => void): string;
641
+ /**
642
+ * Build the `config push` body for one cron-trigger TOML.
643
+ *
644
+ * `state` is absent from the create body because the definition says the create
645
+ * handler assigns it (`writableOn: ["update"]`) — the CLI used to send it and
646
+ * the server used to drop it silently.
647
+ */
648
+ export declare function buildCronTriggerPushPayload(key: string, tomlData: any, { mode }?: {
649
+ mode?: PushMode;
650
+ }): Record<string, any>;
651
+ export declare function serializeBlobBucket(bucket: any, logger?: (message: string) => void): string;
652
+ /**
653
+ * Build the PATCH payload for a blob-bucket update from local TOML.
654
+ *
655
+ * EXACT extraction of the inline builder shared by the existing-update and
656
+ * 409-adopt branches — do NOT "clean up" the truthiness checks on the access
657
+ * model. The server treats `preset` and `accessPolicy` as mutually exclusive,
658
+ * only clears `ruleSetId` when a preset/accessPolicy is also present, and
659
+ * refuses to leave a bucket with no access model or a blank name — so those
660
+ * fields are not clearable. `bucketKey` and `ttlTier` are immutable and never
661
+ * sent. Both call sites must go through this helper so their field sets never
662
+ * drift.
663
+ */
664
+ export declare function buildBlobBucketUpdatePayload(bucket: any): any;
665
+ /**
666
+ * Which `[[configs]]` entry a prompt file marks as live (issue #2645).
667
+ *
668
+ * Exactly one entry may carry `active = true`. Two is an error rather than a
669
+ * first-wins rule: making the file's meaning depend on entry order is the kind
670
+ * of implicit behavior this issue exists to remove, and silently picking one
671
+ * would let the repo claim something the server does not do.
672
+ *
673
+ * `isActive` is accepted as a legacy spelling — the create leg read it before
674
+ * pull ever wrote a marker, so a hand-authored file carrying it keeps working.
675
+ */
676
+ export declare function promptActiveConfigName(configs: any[]): string | undefined;
677
+ /**
678
+ * The push payload for one `[[configs]]` entry of a prompt (issue #2645).
679
+ *
680
+ * One builder for both directions so create and update cannot drift: a field
681
+ * the file owns has to reach the server whether the config is new or already
682
+ * there, which is what "removing a flag removes no capability" means in
683
+ * practice.
684
+ *
685
+ * `description` is sent as `?? null` on update and omitted on create (#1567):
686
+ * dropping the key from a `[[configs]]` block must CLEAR it on an existing
687
+ * config, and the PATCH nulls on an explicit `null` while preserving on omit.
688
+ * `status` is the opposite — omitted means "unchanged", because the server
689
+ * defaults a new config to active and an existing one keeps what it has.
690
+ */
691
+ export declare function promptConfigPayload(tomlConfig: any, options?: {
692
+ create?: boolean;
693
+ }): Record<string, any>;
694
+ /**
695
+ * Build the POST body for a blob-bucket create from local TOML.
696
+ *
697
+ * `ttlTier` is here and absent from the update body because the definition says
698
+ * so (`writableOn: ["create"]`) — the retention tier is fixed at creation.
699
+ */
700
+ export declare function buildBlobBucketCreatePayload(key: string, bucket: any): Record<string, any>;
701
+ export declare function serializePrompt(prompt: any, logger?: (message: string) => void): string;
702
+ /**
703
+ * The `config push` warning for a resource pushed with no `accessRule` (#2652).
704
+ *
705
+ * Returns `null` when a rule is declared — and for a `runAs = "system"`
706
+ * workflow, which rejects any rule and cannot be started by a member at all.
707
+ * Otherwise it names the resource, the consequence, and the one-line restore,
708
+ * because after the #2652 flip a ruleless prompt or caller workflow denies
709
+ * every non-admin caller from the deploy onward.
710
+ *
711
+ * Both kinds now push `accessRule` as value-or-`null` — the prompt since #1567,
712
+ * the workflow since #2743's always-send conversion — so for either one an
713
+ * omitted key really does leave the resource ruleless on the server. The
714
+ * workflow wording says so explicitly, because it used to say the opposite
715
+ * (the update path preserved a stored rule) and an operator who learned that
716
+ * caveat needs to see it retracted.
717
+ */
718
+ export declare function missingAccessRuleWarning(opts: {
719
+ kind: "prompt" | "workflow";
720
+ key: string;
721
+ accessRule: unknown;
722
+ runAs?: unknown;
723
+ }): string | null;
724
+ /**
725
+ * Build the `config push` body for one prompt TOML's `[prompt]` table.
726
+ *
727
+ * The prompt's own fields only — its `[[configs]]` entries are separate
728
+ * requests, built by `buildPromptConfigPayload`.
729
+ */
730
+ export declare function buildPromptPushPayload(key: string, tomlData: any, { mode }?: {
731
+ mode?: PushMode;
732
+ }): Record<string, any>;
733
+ /**
734
+ * Build the POST body for a prompt create.
735
+ *
736
+ * `POST /prompts` is the one endpoint that writes BOTH models: the prompt, and
737
+ * the "default" `AppPromptConfig` it seeds from the same body. So this body is
738
+ * the `[prompt]` table's create half plus the FIRST `[[configs]]` entry's LLM
739
+ * settings — minus the three keys the prompt half owns or the handler assigns
740
+ * itself:
741
+ *
742
+ * - `configName` — the seeded config is always named "default".
743
+ * - `description` — the body's slot belongs to the PROMPT, and the handler
744
+ * hard-codes the seeded config's to "Default configuration". An authored
745
+ * `[[configs]]` description therefore has nowhere to go in this body, so
746
+ * push applies it right after the create rather than losing it.
747
+ * - `outputSchema` — same shape: the body's slot is the prompt's, so the
748
+ * seeded config's own value is applied by the follow-up PATCH
749
+ * (`seededOnlyPromptConfigFields`) instead of being dropped.
750
+ *
751
+ * Every other config field rides along from the definition, so a new LLM
752
+ * setting reaches a brand-new prompt without an edit at this call site.
753
+ */
754
+ export declare function buildPromptCreatePayload(key: string, tomlData: any): Record<string, any>;
755
+ /**
756
+ * The PATCH body reconciling the seeded config with the first `[[configs]]`
757
+ * entry, or `null` when the entry needs none of it.
758
+ *
759
+ * #2972 — the entry's NAME belongs in this patch too. The create handler names
760
+ * the seeded config "default" whatever the entry is called, so a file whose
761
+ * first entry is named anything else got that entry's settings stored under
762
+ * "default": a later entry actually named "default" then collided with it and
763
+ * the push died half-applied, and a file naming no entry "default" quietly
764
+ * landed a config it never mentions while its first entry never existed. Both
765
+ * end here, because the rename runs before any further config is created.
766
+ *
767
+ * `seededConfigName` is what the server called it — read from the create
768
+ * response rather than assumed, so a handler that one day honors the name
769
+ * produces no needless PATCH.
770
+ */
771
+ export declare function seededOnlyPromptConfigFields(firstConfig: any, seededConfigName?: string): Record<string, any> | null;
772
+ /**
773
+ * Build the create/update body for ONE `[[configs]]` entry.
774
+ *
775
+ * `create` carries the provider/model defaults a brand-new config needs; on
776
+ * update those defaults must NOT be applied, or editing an unrelated key would
777
+ * silently re-point a config at the default model.
778
+ */
779
+ export declare function buildPromptConfigPayload(tomlConfig: any, { mode }?: {
780
+ mode?: PushMode;
781
+ }): Record<string, any>;
229
782
  /**
230
783
  * Rewrite a workflow TOML file's string-form `inputSchema` / `outputSchema` to
231
- * native object form for `sync migrate-toml` (issue #1446).
784
+ * native object form for `config migrate-toml` (issue #1446).
232
785
  *
233
786
  * Purely local, no server fetch. Parses the raw source WITHOUT expanding
234
787
  * fragment `include`s (so those directives survive the rewrite), swaps a
@@ -245,19 +798,236 @@ export declare function rewriteWorkflowSchemasToNative(rawToml: string, workflow
245
798
  content: string;
246
799
  changed: boolean;
247
800
  };
801
+ /**
802
+ * #2644 — `config push` rejects any key the vendored definition does not
803
+ * recognize, so a mistyped or newer-server key fails locally instead of being
804
+ * silently dropped on the way to the server (design gate, 2026-08-12). Returns
805
+ * the error message, or null when every key is known.
806
+ *
807
+ * Works for any migrated type, single or repeated: a repeated table's entries
808
+ * are checked one by one and the unknown keys reported together, so a typo in
809
+ * the third `[[configs]]` block is as loud as one in the first.
810
+ */
811
+ export declare function configUnknownKeyError(filePath: string, tomlData: any, table: ConfigTable): string | null;
812
+ /**
813
+ * #2644 — the document-level half of the same rejection: a top-level table the
814
+ * object's definition does not declare, or a field table written with the wrong
815
+ * array markers. Returns one message per problem, or `[]` when the file's shape
816
+ * is recognized.
817
+ *
818
+ * Unrecognized keys INSIDE a table were already rejected; a mistyped table
819
+ * HEADER was not, and it is the more destructive of the two — `[integraton]`
820
+ * leaves `[integration]` absent, so the push builder reads an empty table and
821
+ * sends `description: null` / `accessRule: null`, CLEARING them server-side.
822
+ * The design gate settled the posture (2026-08-12): push rejects everything
823
+ * unknown, tables included.
824
+ */
825
+ export declare function configDocumentShapeErrors(filePath: string, tomlData: any, surface: ConfigObjectSurface): string[];
826
+ /**
827
+ * Which directories `config push` checks for unrecognized keys, and against which
828
+ * tables.
829
+ *
830
+ * Derived from the registry crossed with `SYNC_RESOURCE_TYPES`, so a type
831
+ * gains the check by being defined — nobody has to remember to list it here.
832
+ *
833
+ * Workflows carry an EMPTY `tables` list: `validateWorkflowToml` already
834
+ * reports their `[workflow]` keys alongside the #685 misnest check, in one
835
+ * message per file, but nothing checked a workflow file's top-level tables —
836
+ * so the surface is listed here for the document-level check and skipped for
837
+ * the per-table one.
838
+ */
839
+ export declare function unknownKeyPreflightTargets(): Array<{
840
+ dir: string;
841
+ surface: ConfigObjectSurface;
842
+ tables: ConfigTable[];
843
+ }>;
844
+ /** One config file the TOML preflight rejects, and the row it belongs to. */
845
+ export interface ConfigFileValidationError {
846
+ /** The `config diff` row type, e.g. `prompt`. */
847
+ type: string;
848
+ /** The row key the local file pairs on. */
849
+ key: string;
850
+ filePath: string;
851
+ /** The messages, verbatim — the same text `config push` aborts with. */
852
+ messages: string[];
853
+ }
854
+ /**
855
+ * Every per-entity config file whose TOML `config push` would refuse (#2880
856
+ * criterion 1, symmetry).
857
+ *
858
+ * One collector, two commands. Push aborts on the messages before issuing a
859
+ * request; `config diff` renders them as their own rows, because the failure
860
+ * this closes is a file diff calls Synced and push then rejects — the state the
861
+ * issue's reporter hit after #2803 retired `[prompt].status`, where a local key
862
+ * with no server counterpart is invisible to a comparison and fatal to a push.
863
+ *
864
+ * Three checks, all definition-driven: the document's table shape, keys no
865
+ * definition declares (retired keys among them), and — since #2880 — values
866
+ * whose spelling is not the declared type. A file that does not parse is left
867
+ * to the per-type loop that reads it, which names the parse error itself.
868
+ */
869
+ export declare function collectConfigFileValidationErrors(configDir: string): ConfigFileValidationError[];
870
+ /**
871
+ * Every `<key>.tests/*.toml` sidecar under a config directory.
872
+ *
873
+ * Test cases are a registered surface (#2644 phase 3) but they are not a
874
+ * `SYNC_RESOURCE_TYPES` directory — they live beside the block they test, in
875
+ * `prompts/<key>.tests/`, `workflows/<key>.tests/`, `transforms/<name>.tests/`
876
+ * and (since #2769) `integrations/<key>.tests/` — so
877
+ * `unknownKeyPreflightTargets`, which crosses
878
+ * the registry with that table, never reaches them. Listing them here gives the
879
+ * sidecars the same unrecognized-key rejection every per-entity file has
880
+ * (criterion 6): a mistyped `[test]` key fails the push instead of being
881
+ * dropped on the way to the server.
882
+ */
883
+ export declare function testCaseTomlFiles(configDir: string): string[];
884
+ /** The `[workflow]` case of `configUnknownKeyError` (#2644 phase 1). */
885
+ export declare function workflowUnknownKeyError(filePath: string, tomlData: any): string | null;
886
+ /**
887
+ * Will `config push` reach this workflow key through its UPDATE path?
888
+ *
889
+ * The push takes that path when sync state already holds an id for the key —
890
+ * the `existingId` the apply loop reads. Everything else is a create (including
891
+ * a create that 409s and adopts by key, which cannot be known before the server
892
+ * answers). Exported so the front-loaded preflight can decide, before the first
893
+ * mutating call, which files the update-only `name` pre-check applies to.
894
+ */
895
+ export declare function isWorkflowUpdateTarget(key: string, syncState: {
896
+ entities?: {
897
+ workflows?: Record<string, any>;
898
+ };
899
+ } | null): boolean;
900
+ /**
901
+ * Will the apply loop actually SEND this workflow, or skip it as unchanged?
902
+ *
903
+ * Mirrors the loop's skip condition (`!force && existingId &&
904
+ * !shouldPushExpandedFile(...)`): a create always sends, `--force` always
905
+ * sends, and an update sends only when the expanded content (workflow file plus
906
+ * its config sidecars) differs from the hash sync state recorded for it.
907
+ *
908
+ * Exported so the front-loaded preflight can scope the update-only `name`
909
+ * pre-check to the files this push will really update. Without that scoping a
910
+ * workflow created through the manifest-key `name` fallback — an authoring
911
+ * shape the create path deliberately supports — would push once and then fail
912
+ * every later push, including a no-change one and its `--dry-run`, on a file
913
+ * the push was never going to send (#2743, review follow-up).
914
+ */
915
+ export declare function workflowPushSendsUpdate(configDir: string, key: string, parsedWorkflowToml: any, syncState: {
916
+ entities?: {
917
+ workflows?: Record<string, any>;
918
+ };
919
+ } | null, force?: boolean): boolean;
920
+ /**
921
+ * The UPDATE path's `name` pre-check (#2743).
922
+ *
923
+ * Always-send means the update payload carries `name: value ?? null`, and the
924
+ * server rejects a `null` name — it is required and cannot be cleared. Caught
925
+ * here, the operator gets an error naming their file before any PATCH is sent;
926
+ * caught server-side, they get a bare 400 part-way through a push. Pull always
927
+ * writes `name`, so a file reaching the update path without one is a deliberate
928
+ * authoring state, not a round-trip artifact.
929
+ *
930
+ * Called from the front-loaded preflight (#976) for every workflow this push
931
+ * will SEND through the update path (`workflowPushSendsUpdate` — an unchanged
932
+ * file the apply loop skips is not one) — so it aborts before the first
933
+ * mutating call and reports under `--dry-run` too — and again inside the update
934
+ * closure as defense-in-depth, which is also where the adopt-by-key path (a
935
+ * create the server 409s, unknowable up front) meets it.
936
+ *
937
+ * The CREATE path deliberately does NOT call this: it falls back to the
938
+ * manifest key (`name: workflow.name || key`), which is the accepted way to
939
+ * author a new workflow whose display name is its key.
940
+ *
941
+ * Returns `null` when the name is present.
942
+ */
943
+ export declare function workflowUpdateNameError(filePath: string, workflow: any): string | null;
248
944
  export declare function serializeWorkflow(workflow: any, draft: any, configs: any[], logger?: (message: string) => void): string;
249
- export declare function serializeRuleSet(ruleSet: any): string;
250
- export declare function serializeGroupTypeConfig(config: any, ruleSetIdToName: Map<string, string>): string;
945
+ export declare function serializeDatabaseType(typeConfig: any, operations: any[], ruleSetIdToName: Map<string, string>, options?: {
946
+ /** Per-op form hints derived from the existing file. */
947
+ hints?: OperationFormHints;
948
+ /** Default form for ops with no hint. New files → "native". */
949
+ defaultForm?: FieldForm;
950
+ /** Sink for human-readable fallback messages (logged via `info`). */
951
+ logger?: (message: string) => void;
952
+ /** Server subscription rows to emit as `[[subscriptions]]` (issue #803). */
953
+ subscriptions?: any[];
954
+ }): string;
955
+ /**
956
+ * Flatten an email-template detail response onto its field surface.
957
+ *
958
+ * The API returns `{ emailType, hasOverride, override: {…}, default: {…} }` —
959
+ * only the `override` half is the field surface (the `default` half is what the
960
+ * platform ships), which is why both envelope keys are declared
961
+ * `responseOnlyKeys` in the definition.
962
+ */
963
+ export declare function emailTemplateOverrideRecord(template: any): Record<string, any>;
964
+ export declare function serializeEmailTemplate(template: any, logger?: (message: string) => void): string;
965
+ /**
966
+ * Build the upsert body for one email-template TOML.
967
+ *
968
+ * `emailType` is deliberately absent: it identifies WHICH built-in template the
969
+ * override replaces and travels in the URL path, which is what the definition's
970
+ * `notExposed`/`tomlOnlyKeys` pair records.
971
+ */
972
+ export declare function buildEmailTemplatePushPayload(tomlData: any): {
973
+ subject: string;
974
+ htmlBody: string;
975
+ textBody: string;
976
+ };
977
+ export declare function serializeRuleSet(ruleSet: any, logger?: (message: string) => void): string;
251
978
  /**
252
- * Issue #1567 the TOML-owned scalar fields of a database-type config.
979
+ * The create/update body for one rule-set TOML (#2644).
253
980
  *
254
- * These are fields the `database-types/*.toml` file OWNS: the local file is the
981
+ * The field half comes from the definition which is what keeps
982
+ * `resourceType` off the update body, since the update handler never reads it —
983
+ * and the structural `[rules]` tree is attached beside it.
984
+ *
985
+ * `name` is trimmed here because the server trims it before storing
986
+ * (`rule-sets-controller.ts`, create and update). This body is also what the
987
+ * comparator projects the local side through (#2731 B1), so leaving the
988
+ * untrimmed spelling in would compare a name the server will never hold: the
989
+ * push would "succeed", change nothing, and be planned again on every run.
990
+ */
991
+ export declare function buildRuleSetPushPayload(tomlData: any, mode: PushMode): Record<string, any>;
992
+ export declare function serializeGroupTypeConfig(config: any, ruleSetIdToName: Map<string, string>, logger?: (message: string) => void): string;
993
+ /**
994
+ * The create/update body for one group-type config (#2644).
995
+ *
996
+ * Takes the entity `parseGroupTypeConfigToml` produced, AFTER the rule-set
997
+ * name → id resolution: the definition says which fields the server accepts in
998
+ * each mode, so `groupType` rides the create body and the URL on update.
999
+ */
1000
+ export declare function buildGroupTypeConfigPayload(configData: any, mode: PushMode): Record<string, any>;
1001
+ export declare function serializeCollectionTypeConfig(config: any, ruleSetIdToName: Map<string, string>, logger?: (message: string) => void): string;
1002
+ /** The create/update body for one collection-type config (#2644). */
1003
+ export declare function buildCollectionTypeConfigPayload(configData: any, mode: PushMode): Record<string, any>;
1004
+ /**
1005
+ * The upsert body for one metadata-category config (#2644).
1006
+ *
1007
+ * One endpoint serves create and update, so every field is writable in both
1008
+ * modes and the body carries the whole definition-declared surface. The
1009
+ * identity pair rides the URL too (`PUT …/{resourceType}/{category}`); sending
1010
+ * it in the body as well is what the POST form has always taken.
1011
+ */
1012
+ export declare function buildMetadataCategoryPayload(configData: any): Record<string, any>;
1013
+ /**
1014
+ * Issue #1567 — the TOML-owned fields of a database-type config, derived from
1015
+ * the definition (#2644 phase 3).
1016
+ *
1017
+ * These are fields the `database-type-configs/*.toml` file OWNS: the local file is the
255
1018
  * source of truth, so removing one from the TOML must clear it server-side
256
- * (config-as-code), not silently preserve the stale value. `schema` is NOT in
257
- * this list — it keeps its own `hasSchema` prior-state discriminator, since a
258
- * schema is a large sub-tree, not a scalar.
1019
+ * (config-as-code), not silently preserve the stale value.
1020
+ *
1021
+ * The list used to be written out here, a second statement of the `[type]`
1022
+ * field surface that a new field would have had to be added to by hand. It is
1023
+ * now every exposed `[type]` scalar except the immutable identity, plus the two
1024
+ * sub-trees the definition classifies `structural` and the file nonetheless
1025
+ * owns whole (`triggers`, `metadataManifest`). `schema` is deliberately NOT
1026
+ * owned this way — it keeps its own `hasSchema` prior-state discriminator,
1027
+ * since a schema is a large sub-tree whose absence is ambiguous.
259
1028
  */
260
- export declare const DB_TYPE_OWNED_SCALARS: readonly ["ruleSetId", "triggers", "metadataAccess", "defaultAccess", "autoPopulatedFields", "timestamps", "metadataManifest"];
1029
+ export declare function dbTypeOwnedScalars(table?: ConfigTable): string[];
1030
+ export declare const DB_TYPE_OWNED_SCALARS: readonly string[];
261
1031
  /** The user-facing TOML key for a wire field (for push output). */
262
1032
  export declare function dbTypeFieldLabel(key: string): string;
263
1033
  /**
@@ -287,24 +1057,660 @@ export declare function buildOwnedScalarUpdate(typeConfig: any): Record<string,
287
1057
  * GET; without it (fresh type or a failed fetch) nothing is reported cleared.
288
1058
  */
289
1059
  export declare function ownedScalarsBeingCleared(updateData: Record<string, any>, serverConfig: any): string[];
1060
+ /** The look-ahead a type-config PATCH carries about this push's operations. */
1061
+ export interface PendingOpClaims {
1062
+ pendingOpDeletes: string[];
1063
+ finalOpNames: string[];
1064
+ pendingOpUpdates: Array<{
1065
+ name: string;
1066
+ access: string | null;
1067
+ params: any;
1068
+ }>;
1069
+ pendingOpUpserts: Array<Record<string, any>>;
1070
+ }
1071
+ /**
1072
+ * What this push's operation set will BE, stated to the server (issues #813,
1073
+ * #1336, #2732).
1074
+ *
1075
+ * The type-config PATCH runs its gates BEFORE the same push's operation calls,
1076
+ * so without a look-ahead every gate judges the pre-push operations: a push
1077
+ * that removes a model is blocked by the very references it is deleting. The
1078
+ * file IS the target state (config-as-code), so the claims are derived from it:
1079
+ *
1080
+ * - `finalOpNames` — the names the type ends with. Both other claims are
1081
+ * verified against it server-side, which is what keeps them from being a
1082
+ * gate bypass for a direct-API caller.
1083
+ * - `pendingOpDeletes` — its complement among the ops last sync recorded, so
1084
+ * an op this push deletes is excluded from the schema / OPS_EXIST gates.
1085
+ * - `pendingOpUpserts` — the post-push BODY of every declared op, so the
1086
+ * schema-edit gate lints a rewritten op as rewritten and a model removal
1087
+ * plus its operation rewrites lands in one push (#2732).
1088
+ * - `pendingOpUpdates` — the rule-only form (`access`/`params`) the manifest
1089
+ * re-lint reads (#1336).
1090
+ *
1091
+ * All four are built here and sent together, on the dry-run and on the real
1092
+ * PATCH alike. They are never conditioned on the push having a deletion: a push
1093
+ * that only REWRITES operations is exactly the case #2732 exists for.
1094
+ */
1095
+ export declare function buildPendingOpClaims(operations: any[], existingOpNames: string[]): PendingOpClaims;
290
1096
  export declare function parseDatabaseTypeToml(tomlData: any): {
291
1097
  typeConfig: any;
292
1098
  operations: any[];
293
1099
  subscriptions: any[];
294
1100
  };
1101
+ /**
1102
+ * The parsed shape of a rule-set TOML: the definition's fields plus the
1103
+ * structural `[rules]` tree. Identical to the create body, which is what
1104
+ * `config diff` hashes both sides through.
1105
+ */
295
1106
  export declare function parseRuleSetToml(tomlData: any): any;
296
1107
  export declare function parseGroupTypeConfigToml(tomlData: any): any;
297
1108
  export declare function parseCollectionTypeConfigToml(tomlData: any): any;
298
- export declare function hashLocalRuleSetForDiff(parsedToml: any): string;
299
- export declare function hashRemoteRuleSetForDiff(ruleSet: any): string;
300
- export declare function hashLocalGroupTypeConfigForDiff(parsedToml: any, ruleSetNameToId: Map<string, string>): string;
301
- export declare function hashRemoteGroupTypeConfigForDiff(config: any, ruleSetIdToName: Map<string, string>, ruleSetNameToId: Map<string, string>): string;
302
- export declare function hashLocalCollectionTypeConfigForDiff(parsedToml: any, ruleSetNameToId: Map<string, string>): string;
303
- export declare function hashRemoteCollectionTypeConfigForDiff(config: any, ruleSetIdToName: Map<string, string>, ruleSetNameToId: Map<string, string>): string;
304
- export declare function hashLocalDatabaseTypeForDiff(parsedToml: any, ruleSetNameToId: Map<string, string>): string;
305
- export declare function hashRemoteDatabaseTypeForDiff(typeConfig: any, operations: any[], subscriptions: any[], ruleSetIdToName: Map<string, string>, ruleSetNameToId: Map<string, string>): string;
306
- export declare function hashLocalMetadataCategoryForDiff(parsedToml: any): string;
307
- export declare function hashRemoteMetadataCategoryForDiff(config: any): string;
1109
+ /**
1110
+ * `config diff`'s hash for one configuration object, over the field set its
1111
+ * DEFINITION declares (#2644 criterion 8).
1112
+ *
1113
+ * Both sides the local file and the server entity — run through this one
1114
+ * function, so a field is visible to `config diff` exactly when the definition
1115
+ * exposes it. Each type used to carry a hand-written
1116
+ * `hashLocal*ForDiff` / `hashRemote*ForDiff` pair naming its own fields: a
1117
+ * third copy of the field surface, with the same failure mode as the other two
1118
+ * (a field pushed and pulled but missing from the hash is invisible to diff, so
1119
+ * an edit reads as "nothing differs" until push applies it).
1120
+ *
1121
+ * `extras` carries the values that are NOT definition fields and must still be
1122
+ * compared: the authored sub-trees the definition classifies `structural`
1123
+ * (`[rules]`, `[[operations]]`, the `[metadata]` manifest) and the
1124
+ * `_unresolvedRuleSetName` marker below. `undefined` entries are dropped so an
1125
+ * absent extra and an omitted one hash the same.
1126
+ */
1127
+ export declare function projectConfigForComparison(table: ConfigTable, entity: any, extras?: Record<string, any>): Record<string, any>;
1128
+ export declare function hashConfigForDiff(table: ConfigTable, entity: any, extras?: Record<string, any>): string;
1129
+ /**
1130
+ * The fields two projected records disagree on — what a conflict report prints
1131
+ * (#2731 B5) and what the immutable-field pre-check reads (B8).
1132
+ *
1133
+ * Compared over the UNION of keys, so a key present on one side only is a
1134
+ * difference rather than a silently ignored one. Values compare by canonical
1135
+ * JSON, which is key-order insensitive for objects and order-SENSITIVE for
1136
+ * arrays — a reordered array is a real change, per the comparator caveats.
1137
+ * Structural sub-trees (`rules`, `schema`, `metadataManifest`, operations,
1138
+ * subscriptions) arrive as single extras keys and therefore compare as units.
1139
+ */
1140
+ export declare function diffProjectedRecords(local: Record<string, any>, remote: Record<string, any>): Array<{
1141
+ field: string;
1142
+ local: any;
1143
+ server: any;
1144
+ }>;
1145
+ /**
1146
+ * The differing fields the definition says an UPDATE does not accept (#2731 B8).
1147
+ *
1148
+ * A local edit to such a field (rule-set `resourceType`, `writableOn:
1149
+ * CREATE_ONLY`) would PATCH "successfully" while changing nothing server-side,
1150
+ * and then re-report forever — the exact shape of non-convergence this issue
1151
+ * exists to end. Extras keys are not definition fields and are never named.
1152
+ */
1153
+ export declare function findImmutableFieldDiffs(table: ConfigTable, fieldDiffs: ReadonlyArray<{
1154
+ field: string;
1155
+ }>): string[];
1156
+ /**
1157
+ * Which side of a converted resource changed (#2731 B4).
1158
+ *
1159
+ * `baselineHash` is the manifest's `semanticHash`: the comparator's hash of the
1160
+ * state both sides last agreed on. With it the answer is exact. Without it —
1161
+ * a manifest written before this issue landed — the direction is INFERRED from
1162
+ * the legacy signals rather than defaulting to "apply": a stored `contentHash`
1163
+ * still matching the file's bytes proves the local side did not move, and a
1164
+ * stored `modifiedAt` still matching the live one proves the server did not.
1165
+ * When neither signal establishes a side (or the row was just adopted), the
1166
+ * answer is `unknown-direction`, which callers treat as a conflict needing
1167
+ * `config pull` or `--force`. An upgraded installation's first push must never
1168
+ * silently overwrite server drift.
1169
+ */
1170
+ export type PushChangeClass = "unchanged" | "local-edited" | "server-drifted" | "both-changed" | "unknown-direction";
1171
+ export declare function classifyPushChange(input: {
1172
+ localHash: string;
1173
+ remoteHash: string;
1174
+ baselineHash?: string;
1175
+ legacy?: {
1176
+ localBytesMatchStoredContentHash?: boolean;
1177
+ liveModifiedAtMatchesStored?: boolean;
1178
+ };
1179
+ }): PushChangeClass;
1180
+ /** The rule-set name/id maps `config diff` resolves references through. */
1181
+ export interface ConfigDiffMaps {
1182
+ ruleSetIdToName: Map<string, string>;
1183
+ ruleSetNameToId: Map<string, string>;
1184
+ }
1185
+ /**
1186
+ * How one configuration type is hashed for `config diff`: its definition, the
1187
+ * definition-driven parse both sides go through, the pull serializer that turns
1188
+ * a server entity into the file pull would have written, and the structural
1189
+ * values hashed alongside the definition's fields.
1190
+ */
1191
+ export interface ConfigDiffSpec {
1192
+ label: string;
1193
+ table: ConfigTable;
1194
+ /**
1195
+ * TOML doc -> the wire-shaped entity (the same parse `config push` uses).
1196
+ *
1197
+ * `extra` is the per-type context the projection needs and the document does
1198
+ * not carry (#2880): a test case's id→name lookups, through which the two
1199
+ * spellings of a reference meet. Mirrors `serialize`, which has taken one
1200
+ * since the database types joined.
1201
+ */
1202
+ parse(doc: any, extra?: any): any;
1203
+ /** Server entity -> the TOML `config pull` would write. */
1204
+ serialize(record: any, maps: ConfigDiffMaps, extra?: any): string;
1205
+ /** Structural values to hash beside the definition's fields. */
1206
+ extras?(entity: any, doc: any, extra?: any): Record<string, any>;
1207
+ /**
1208
+ * Rewrite the parsed entity the way the SERVER rewrites an accepted one
1209
+ * (#2880 DSO-003).
1210
+ *
1211
+ * Some handlers canonicalize on the way in — an integration's base URL gains
1212
+ * a trailing slash, its methods are upper-cased — so the state the server
1213
+ * returns is not the text the file holds. Comparing them raw makes an
1214
+ * authored-but-noncanonical value `local-edited` on every run and push
1215
+ * re-apply the same update forever; restamping the baseline cannot fix it,
1216
+ * because the local side never moves. Applied to both sides (the remote
1217
+ * reaches it through the same projection), so it must be idempotent.
1218
+ */
1219
+ canonicalize?(entity: any): void;
1220
+ /**
1221
+ * The projected keys THIS FILE does not manage (#2880 criterion 3).
1222
+ *
1223
+ * A handful of fields are present-only by design: push sends them when the
1224
+ * file spells them and leaves the server's value alone when it does not,
1225
+ * because "absent" here means "not authored here" rather than "cleared" — a
1226
+ * webhook with no `[verification]` section at all, whose signing material a
1227
+ * push must not revoke as a side effect. Comparing such a field against a
1228
+ * server that holds one reports a difference push will never act on: the
1229
+ * update omits the key, the server keeps its value, and the row comes back
1230
+ * Modified on every run. So the local side inherits the remote value for
1231
+ * exactly the keys the file leaves unmanaged, and the two commands agree
1232
+ * that there is nothing to do.
1233
+ */
1234
+ unmanaged?(doc: any): string[];
1235
+ /** Whether the entity carries a rule-set reference needing resolution. */
1236
+ ruleSetRef?: boolean;
1237
+ /** How the type names itself in the rule-set resolution message. */
1238
+ describe(entity: any): string;
1239
+ }
1240
+ /**
1241
+ * Every type the shared comparator serves — the types `config diff` compares
1242
+ * field-for-field, and that `config push` gates on the same projection
1243
+ * (#2731 B1/B2; webhooks joined the comparator in #2757 and their push joined
1244
+ * the same gate in #2880, so no registered type answers "changed?" from the
1245
+ * file's bytes any more).
1246
+ *
1247
+ * Published so the round-trip acceptance bar (#2731 B9) can require a fixture
1248
+ * per type instead of listing them a second time by hand: converting a type is
1249
+ * then one edit here, and the bar says so if its round trip is untested.
1250
+ */
1251
+ export declare function configDiffSpecLabels(): string[];
1252
+ /** The diff spec for a configuration type. Throws rather than skipping a check. */
1253
+ export declare function configDiffSpec(label: string): ConfigDiffSpec;
1254
+ /**
1255
+ * Project a LOCAL config file onto the record both `config diff` and `config push`
1256
+ * compare (#2644 field set, #2731 shared gate). The hash is a thin wrapper, so
1257
+ * a push that needs to SAY what differs and a diff that only needs to know THAT
1258
+ * something differs read the same projection.
1259
+ */
1260
+ export declare function projectLocalConfig(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps, extra?: any): Record<string, any>;
1261
+ /** The same projection for a SERVER entity — see `hashRemoteConfigForDiff`. */
1262
+ export declare function projectRemoteConfig(spec: ConfigDiffSpec, record: any, maps: ConfigDiffMaps, extra?: any): Record<string, any>;
1263
+ /**
1264
+ * BOTH sides of one comparison, with the file's unmanaged keys reconciled
1265
+ * (#2880) — the one place `spec.unmanaged` is honored, so `config diff` and
1266
+ * `config push` cannot read the same file differently.
1267
+ */
1268
+ export declare function projectConfigPair(spec: ConfigDiffSpec, localParsed: any, remoteRecord: any, maps: ConfigDiffMaps, extra?: any, localExtra?: any): {
1269
+ local: Record<string, any>;
1270
+ remote: Record<string, any>;
1271
+ };
1272
+ /**
1273
+ * One `config diff` row's content verdict for a registered type (#2880).
1274
+ *
1275
+ * The three outcomes the per-type blocks were each spelling out by hand:
1276
+ * equal (`exists`), different (`modified`, framed as a preview of
1277
+ * `config pull`), and "could not tell" — a missing record or a comparison that
1278
+ * threw, which degrades THIS row and never the whole diff. Written once so a
1279
+ * type joining the comparator cannot accidentally report a fourth thing.
1280
+ *
1281
+ * The two `extra` arguments are the same split `decidePushForConfig` makes:
1282
+ * `extra` carries what only the SERVER side has (a database type's operation
1283
+ * rows), while `localExtra` is context BOTH sides read a value through — a test
1284
+ * case's id→name lookups. Giving the local side nothing was a silent
1285
+ * mistranslation: a sidecar pinned by a resolvable `configId` projected the id
1286
+ * while the server's projection resolved it to the name, so an untouched file
1287
+ * reported Modified in diff and drifted in push.
1288
+ */
1289
+ export declare function compareLocalToRemote(spec: ConfigDiffSpec, localParsed: any, remoteRecord: any, maps?: ConfigDiffMaps, extra?: any, localExtra?: any): {
1290
+ status: string;
1291
+ hint?: string;
1292
+ };
1293
+ /** Hash a LOCAL config file's definition-projected field set (#2644). */
1294
+ export declare function hashLocalConfigForDiff(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps, extra?: any): string;
1295
+ /**
1296
+ * Hash a SERVER entity the same way, by serializing it into the file `sync
1297
+ * pull` would write and hashing that — so the two sides are normalized
1298
+ * identically by construction, and a legacy encoding on disk (a JSON-string
1299
+ * operation, an id-based rule-set reference) is not a false `Modified`.
1300
+ *
1301
+ * `extra` carries the sibling rows a type's file also holds: a database type's
1302
+ * operations and subscriptions, which the list response does not include.
1303
+ */
1304
+ export declare function hashRemoteConfigForDiff(spec: ConfigDiffSpec, record: any, maps: ConfigDiffMaps, extra?: any): string;
1305
+ /** One field's disagreement between the local file and the live server entity. */
1306
+ export interface PushFieldDiff {
1307
+ field: string;
1308
+ local: any;
1309
+ server: any;
1310
+ }
1311
+ /**
1312
+ * What `config push` should do with one converted-type file, and why.
1313
+ *
1314
+ * `create`/`skip`/`update` are the applying outcomes; `drift`, `conflict`,
1315
+ * `immutable` and `live-unavailable` are the four ways push declines to apply
1316
+ * and says so.
1317
+ */
1318
+ export type PushGateOutcome = {
1319
+ action: "live-unavailable";
1320
+ } | {
1321
+ action: "create";
1322
+ } | {
1323
+ action: "skip";
1324
+ localHash: string;
1325
+ remoteHash: string;
1326
+ } | {
1327
+ action: "update";
1328
+ direction: "local-edited" | "forced" | "adopt-untracked";
1329
+ localHash: string;
1330
+ remoteHash?: string;
1331
+ expectedModifiedAt?: string;
1332
+ fields: PushFieldDiff[];
1333
+ } | {
1334
+ action: "drift";
1335
+ localHash: string;
1336
+ remoteHash: string;
1337
+ fields: PushFieldDiff[];
1338
+ } | {
1339
+ action: "conflict";
1340
+ direction: "both-changed" | "unknown-direction";
1341
+ localHash: string;
1342
+ remoteHash: string;
1343
+ fields: PushFieldDiff[];
1344
+ serverModifiedAt?: string;
1345
+ } | {
1346
+ action: "immutable";
1347
+ localHash: string;
1348
+ remoteHash: string;
1349
+ fields: PushFieldDiff[];
1350
+ immutableFields: string[];
1351
+ };
1352
+ /** The manifest entry a converted type's gate reads (all fields optional). */
1353
+ export interface PushBaselineEntry {
1354
+ modifiedAt?: string;
1355
+ contentHash?: string;
1356
+ semanticHash?: string;
1357
+ }
1358
+ /**
1359
+ * `config push`'s change decision for one configuration file (#2731 B1/B3/B4/B8).
1360
+ *
1361
+ * This replaces `shouldPushFile` for the converted types. The differences that
1362
+ * matter, in order:
1363
+ *
1364
+ * - the comparison is `config diff`'s — the definition-projected field set on
1365
+ * both sides — so "skip as unchanged" and "Synced" cannot disagree, and a
1366
+ * comment-only or formatting-only edit is not a change;
1367
+ * - the right-hand side is the LIVE entity, not the manifest, so out-of-band
1368
+ * server drift is visible instead of being overwritten;
1369
+ * - the manifest becomes a baseline that answers WHICH side moved, so a
1370
+ * difference is attributed rather than assumed to be a local edit.
1371
+ *
1372
+ * Declining is a first-class outcome here. `drift`, `conflict` and `immutable`
1373
+ * each carry the field diff their report prints, and nothing is applied.
1374
+ *
1375
+ * `adoptsUntrackedByKey` is the one exception a type can ask for, and it is
1376
+ * documented on the field: an object the manifest has never recorded is the
1377
+ * adopt its create path would have performed via a 409 (#1006/#2909), not a
1378
+ * difference to refuse.
1379
+ */
1380
+ /**
1381
+ * What the DEGRADED gate does with one file — the path a converted type takes
1382
+ * when its live read failed (#2880 criterion 7).
1383
+ *
1384
+ * Falling back to the manifest's byte hash is right while there is one to fall
1385
+ * back to. Without one, `shouldPushFile(file, undefined)` answers "push it",
1386
+ * so a fresh checkout or a lost manifest turned "we could not read the server"
1387
+ * into "overwrite the server", unconditionally and silently — the one shape of
1388
+ * blind write this issue's direction attribution exists to prevent. So a
1389
+ * baseline-less update DECLINES: the resource is not written and the operator
1390
+ * is told which of `config pull` / `--force` clears it.
1391
+ *
1392
+ * A file the manifest does not name is declined for the same reason, and this
1393
+ * is the part that is easy to get wrong: an unrecorded file looks like a
1394
+ * create, but only LIVE STATE can say the server has nothing under that key —
1395
+ * and live state is exactly what this path could not read. The create that
1396
+ * follows is adopted by key on the 409 and re-issued as an update, so
1397
+ * "there is no recorded entity to overwrite" would have been a blind overwrite
1398
+ * of a resource this run never compared. Every section's degraded gate is
1399
+ * reached only when the live read failed, so there is no case here where
1400
+ * absence was proved.
1401
+ */
1402
+ export type DegradedPushDecision = {
1403
+ action: "skip";
1404
+ } | {
1405
+ action: "send";
1406
+ } | {
1407
+ action: "decline";
1408
+ reason: "no-baseline";
1409
+ };
1410
+ export declare function decideDegradedPush(input: {
1411
+ force?: boolean;
1412
+ storedContentHash?: string;
1413
+ currentFileHash?: string;
1414
+ }): DegradedPushDecision;
1415
+ export declare function decidePushForConfig(input: {
1416
+ spec: ConfigDiffSpec;
1417
+ maps: ConfigDiffMaps;
1418
+ localParsed: any;
1419
+ /** The live server entity, or null/undefined when the server has none. */
1420
+ live?: {
1421
+ record: any;
1422
+ extra?: any;
1423
+ modifiedAt?: string;
1424
+ } | null;
1425
+ /** The manifest entry, absent when this file has never been synced. */
1426
+ entry?: PushBaselineEntry | null;
1427
+ /** Byte hash of the local file — the legacy direction signal. */
1428
+ localFileHash?: string;
1429
+ /**
1430
+ * Per-type context for the LOCAL projection (#2880) — a test case's id→name
1431
+ * lookups. Separate from `live.extra`, which carries remote-only rows (a
1432
+ * database type's operations) the local parse must never see.
1433
+ */
1434
+ localExtra?: any;
1435
+ force?: boolean;
1436
+ /**
1437
+ * Whether this type's push path ADOPTS an existing object by key (#2909).
1438
+ *
1439
+ * Opt-in, and it changes exactly one outcome: a live entity the manifest has
1440
+ * no row for at all. Prompts (#1006), and the other types whose create path
1441
+ * recovers a 409 through `adoptByKeyOnCreate409`, treat that as the adopt it
1442
+ * has always been — an object pushed from another slot, created out of band,
1443
+ * or orphaned by a push that aborted before recording it, which the local
1444
+ * file is the declared intent for. Without this the gate reads that same
1445
+ * shape as a difference it cannot attribute, refuses, and the create the
1446
+ * adopt guard recovers from is never even sent (the #2909 regression).
1447
+ *
1448
+ * #2934 — the criterion is the CREATE PATH, not which issue converted the
1449
+ * type: cron triggers, webhooks, integrations, blob buckets and the
1450
+ * group/collection type configs all recover their create's key conflict the
1451
+ * same way, so they pass it too. Passing it at one call site left the other
1452
+ * five reporting a conflict for the adopt their own create documents.
1453
+ *
1454
+ * Rule sets keep it OFF, and that is a decision rather than an omission:
1455
+ * #2731 made a never-synced rule set differing from a same-named live one the
1456
+ * conflict the operator resolves with `config pull` or `--force`, its own
1457
+ * tests pin that, and `resourceType` — the field a wrong adopt would silently
1458
+ * strand — is one an update cannot repair.
1459
+ */
1460
+ adoptsUntrackedByKey?: boolean;
1461
+ }): PushGateOutcome;
1462
+ /**
1463
+ * The identity a file may leave to its name, written back into the parsed
1464
+ * document before the shared gate reads it (#2731 B1).
1465
+ *
1466
+ * Four types let the file name stand in for the identity field inside the file
1467
+ * (`orders.toml` for a database type, `group.profile.toml` for a metadata
1468
+ * category, and so on). The gate re-parses the DOCUMENT, so the derived value
1469
+ * has to be in it: an omitted `databaseType` otherwise projects as absent
1470
+ * against a server record that carries it, and — since these identities are
1471
+ * declared create-only — the file reports an immutable-field difference on
1472
+ * every push while `config diff`, which injects the same value before it hashes
1473
+ * (~sync.ts:11744), reads it Synced.
1474
+ *
1475
+ * A value the file states always wins; this only fills the gap the file name
1476
+ * was already filling for the rest of push.
1477
+ */
1478
+ export declare function withDerivedIdentity(doc: any, tomlPath: string, identity: Record<string, string | undefined>): any;
1479
+ /**
1480
+ * Which server-side entity this file's update targets (#2731 B3).
1481
+ *
1482
+ * The manifest is a cache, so it can be wrong in both directions: it can name
1483
+ * an entity the server no longer has (pointing an update at a 404 where the
1484
+ * operator asked for a re-create, #1659), and it can be silent about one that
1485
+ * is there (sending a create that comes back 409 to be adopted). When live
1486
+ * state was read, live state answers; the manifest is the fallback for the
1487
+ * degraded path only, where it is also what the byte-hash gate has always used.
1488
+ */
1489
+ export declare function resolveExistingId(input: {
1490
+ liveOk: boolean;
1491
+ liveId?: string;
1492
+ manifestId?: string;
1493
+ }): string | undefined;
1494
+ /**
1495
+ * Which CHILD rows a database type's push reconciles against (#2731 B3).
1496
+ *
1497
+ * `resolveExistingId` answers this for a whole entity; a database type's
1498
+ * operations and subscriptions are rows with the same question and the same
1499
+ * two possible answers. The manifest can name an operation the server no
1500
+ * longer has (the update PUTs a row that is gone) and be silent about one it
1501
+ * does have (the create POSTs a duplicate key) — both abort the push with a
1502
+ * server error, which is exactly the recovery `--force` is supposed to
1503
+ * perform.
1504
+ *
1505
+ * When the reconcile read the rows, they ARE the baseline: existence, the
1506
+ * timestamp each update guards with, and — as the complement of the file's own
1507
+ * list — which rows this push deletes. `undefined` means the read did not
1508
+ * happen (the degraded path, or a type live state says is absent), and the
1509
+ * manifest stays what it has always been.
1510
+ */
1511
+ export declare function liveChildBaseline(live: Map<string, any> | undefined, manifestChildren?: Record<string, {
1512
+ modifiedAt: string;
1513
+ }>): Record<string, {
1514
+ modifiedAt: string;
1515
+ }> | undefined;
1516
+ /**
1517
+ * The app's live rule sets, indexed the three ways push reads them (#2731 B3).
1518
+ *
1519
+ * One fetch answers three questions, and leaving any of them to the manifest
1520
+ * reintroduces the split this issue is about:
1521
+ *
1522
+ * - `byFileKey` — the rule set this file's own gate compares against;
1523
+ * - `idToName` — how a REFERENCING type's server record is serialized back
1524
+ * into the file pull would write, so a `ruleSetId` is compared as the name
1525
+ * the file spells;
1526
+ * - `nameToId` — how a referencing file's `ruleSetName` resolves to something
1527
+ * push can send. Seeding it from live is what lets `--only
1528
+ * group-type-config/team` resolve a rule set that exists on the server but
1529
+ * was never recorded locally, instead of failing as an unresolved
1530
+ * reference.
1531
+ */
1532
+ export declare function indexLiveRuleSets(ruleSets: any[]): {
1533
+ byFileKey: Map<string, any>;
1534
+ idToName: Map<string, string>;
1535
+ nameToId: Map<string, string>;
1536
+ };
1537
+ /**
1538
+ * The baseline a successful apply stamps, from the SERVER's own record
1539
+ * (#2731 B4).
1540
+ *
1541
+ * The baseline is compared against both sides on the next run, so it has to be
1542
+ * in the server's spelling: handlers normalize what they store (rule-set
1543
+ * create/update trims `name`), and a baseline taken from the local file would
1544
+ * make the very next legitimate local edit read as "both sides changed" — a
1545
+ * conflict the operator can only clear with `--force`.
1546
+ *
1547
+ * `undefined` — no baseline — is the honest answer when the response carries
1548
+ * nothing projectable and no refetch is available. The next run then falls back
1549
+ * to the legacy `contentHash`/`modifiedAt` signals, which is what an upgraded
1550
+ * installation runs on anyway; an invented baseline would instead assert
1551
+ * agreement that was never observed.
1552
+ */
1553
+ export declare function remoteSemanticBaseline(input: {
1554
+ spec: ConfigDiffSpec;
1555
+ maps: ConfigDiffMaps;
1556
+ /** What the write returned. Usable only if it is the stored entity. */
1557
+ returned?: any;
1558
+ extra?: any;
1559
+ /**
1560
+ * How this type recognizes its own stored record. The default — it carries a
1561
+ * `modifiedAt` — is what separates the stored entity from a bare
1562
+ * `{ success: true }` acknowledgement, whose empty projection would otherwise
1563
+ * be stamped as the state both sides agreed on. Types whose comparator reads
1564
+ * an envelope (an email template's `{ emailType, hasOverride, override }`)
1565
+ * say where their timestamp actually lives.
1566
+ */
1567
+ isStoredRecord?: (returned: any) => boolean;
1568
+ }): string | undefined;
1569
+ /** What `config push` decides for one transform (#2731 B7). */
1570
+ export type ScriptPushOutcome = {
1571
+ action: "skip";
1572
+ localHash: string;
1573
+ remoteHash: string;
1574
+ } | {
1575
+ action: "update";
1576
+ direction: "local-edited" | "forced" | "no-active-body";
1577
+ localHash: string;
1578
+ } | {
1579
+ action: "drift";
1580
+ localHash: string;
1581
+ remoteHash: string;
1582
+ } | {
1583
+ action: "conflict";
1584
+ direction: "both-changed" | "unknown-direction";
1585
+ localHash: string;
1586
+ remoteHash: string;
1587
+ };
1588
+ /**
1589
+ * `config push`'s change decision for one transform (#2731 B7).
1590
+ *
1591
+ * A transform's comparator IS its bytes: the body push sends is the body diff
1592
+ * compares, so there is no projection to reconcile — only the same three
1593
+ * questions every converted type asks. Local versus the LIVE active body, with
1594
+ * the manifest demoted to the baseline that says which side moved.
1595
+ *
1596
+ * A script the manifest has never seen reaches here too (adopted by name from
1597
+ * the live index). It has no baseline and no legacy signal, so an unequal body
1598
+ * classifies as `unknown-direction` — a conflict to report rather than a server
1599
+ * body to overwrite on a guess.
1600
+ */
1601
+ export declare function decidePushForScript(input: {
1602
+ localBody: string;
1603
+ /** Byte hash of the local file — the legacy direction signal. */
1604
+ localFileHash?: string;
1605
+ /**
1606
+ * The live active body, and the script row's timestamp beside it — the other
1607
+ * legacy direction signal (#2731 B4). Activating a config updates the script
1608
+ * row, so a timestamp still equal to the manifest's proves the server body
1609
+ * has not moved: an upgraded installation's first push applies a genuine
1610
+ * local edit instead of reporting a direction it could have known.
1611
+ *
1612
+ * `null` is an ANSWER, not a failed read: the script row exists and nothing
1613
+ * is active on it. See below.
1614
+ */
1615
+ live: {
1616
+ body: string;
1617
+ modifiedAt?: string;
1618
+ } | null;
1619
+ entry?: {
1620
+ contentHash?: string;
1621
+ semanticHash?: string;
1622
+ modifiedAt?: string;
1623
+ } | null;
1624
+ force?: boolean;
1625
+ }): ScriptPushOutcome;
1626
+ /**
1627
+ * One side of a field diff, rendered for a conflict report (#2731 B5).
1628
+ *
1629
+ * A rule set's `rules` tree or a template's HTML body can be kilobytes; the
1630
+ * useful statement about them is that they differ and how big they are, not
1631
+ * their contents scrolling past.
1632
+ */
1633
+ export declare function summarizeFieldValue(value: any): string;
1634
+ /**
1635
+ * The push summary line (#2731 criterion 10).
1636
+ *
1637
+ * The two established spellings are preserved exactly; drift — resources push
1638
+ * deliberately did NOT apply — is appended only when there is some, so an
1639
+ * operator never has to infer it from a change count that stayed put.
1640
+ */
1641
+ export declare function formatPushSummary(input: {
1642
+ pushed: number;
1643
+ skipped: number;
1644
+ drifted: number;
1645
+ }): string;
1646
+ /**
1647
+ * The summary line a FAILED push ends on (#2731 B5).
1648
+ *
1649
+ * "Re-run `config push` to converge" is true of one failure only: a database
1650
+ * type the validate-first gate blocked (#813), where the next push carries the
1651
+ * corrected state. A conflict is the opposite — the run declined to apply
1652
+ * precisely because both sides moved, so repeating it reports the same
1653
+ * conflict forever (the alpha.62 field report ends in that loop). Apply
1654
+ * failures, which used to be absent from this line entirely, need the file
1655
+ * fixed rather than either.
1656
+ */
1657
+ export declare function formatPushFailureSummary(input: {
1658
+ pushed: number;
1659
+ blockedDatabaseTypes: number;
1660
+ conflicts: number;
1661
+ applyFailures: number;
1662
+ }): string;
1663
+ /**
1664
+ * How many of a failed push's planned changes actually applied.
1665
+ *
1666
+ * `changes` records what push SET OUT to do, so the honest success count is
1667
+ * that list minus everything the run then refused or the server rejected:
1668
+ *
1669
+ * - a validate-first blocked database type (issue #813) keeps its labels for
1670
+ * visibility, but none of them landed — including its operations;
1671
+ * - a conflict on a change that was already counted (a late 409, recorded
1672
+ * before the write) cancels that change.
1673
+ *
1674
+ * A conflict the GATE declined (`planned: false`) is deliberately NOT
1675
+ * subtracted: it never added a `changes` entry, so charging the count for it
1676
+ * reports a push that did apply something as having applied nothing.
1677
+ */
1678
+ export declare function countAppliedChanges(input: {
1679
+ changes: Array<{
1680
+ type: string;
1681
+ key: string;
1682
+ }>;
1683
+ conflicts: Array<{
1684
+ planned?: boolean;
1685
+ }>;
1686
+ blockedDatabaseTypes: string[];
1687
+ schemaBlockedCount: number;
1688
+ }): number;
1689
+ /**
1690
+ * The semantic baseline to stamp for a file both sides now agree on (#2731 B4).
1691
+ *
1692
+ * `contentHash` records what the local BYTES were; this records what the
1693
+ * comparator SAW, which is what makes "who changed?" answerable: after a pull
1694
+ * or a successful push, local and server agree, so one hash is the baseline for
1695
+ * both. Best-effort by design — an unreadable or unparseable file yields
1696
+ * `undefined`, and a missing baseline degrades to the legacy inference rather
1697
+ * than taking the command down.
1698
+ */
1699
+ export declare function configSemanticHash(label: string, filePath: string, maps: ConfigDiffMaps): string | undefined;
1700
+ /**
1701
+ * The sync state `config push` writes into (#2731 B6, closing #2374).
1702
+ *
1703
+ * `loadSyncState` answers `null` for an absent `.primitive-sync.json`, and
1704
+ * every per-entity state write in the push loops is guarded by `if (syncState)`
1705
+ * — so a first push created every resource and recorded none of them. The next
1706
+ * push then re-entered the create path for everything and depended on 409
1707
+ * adoption to recover, which for database types hard-failed on operations.
1708
+ *
1709
+ * Building the state up front (the same shape the cross-app-push branch
1710
+ * already builds) makes those guards hold from the first run, so the
1711
+ * end-of-push and catch-path saves persist what the push actually did.
1712
+ */
1713
+ export declare function ensurePushSyncState(existing: SyncState | null, appId: string, serverUrl: string): SyncState;
308
1714
  export declare function parseTomlFile(filePath: string): any;
309
1715
  /**
310
1716
  * Paginate through a list endpoint, collecting all items.
@@ -317,7 +1723,7 @@ export declare function fetchAll<T>(listFn: (params: {
317
1723
  nextCursor?: string | null;
318
1724
  }>, pageSize?: number, maxPages?: number): Promise<T[]>;
319
1725
  /**
320
- * Issue #976 / #1006: shared 409 → adopt-by-key recovery for `sync push`
1726
+ * Issue #976 / #1006: shared 409 → adopt-by-key recovery for `config push`
321
1727
  * create paths. When a CREATE hits a per-app unique-key constraint, the resource
322
1728
  * is already on the server but missing from local sync state: orphaned by a
323
1729
  * prior push that aborted before recording it, a mid-apply crash, or an
@@ -370,14 +1776,47 @@ export declare function adoptByKeyOnCreate409<T>(opts: {
370
1776
  */
371
1777
  export declare function resolveRuleSetReference(entityConfig: any, ruleSetNameToId: Map<string, string>, entityLabel: string, options?: {
372
1778
  throwOnMissing?: boolean;
1779
+ hint?: string;
373
1780
  }): void;
374
- export declare function slugifyTestCaseName(name: string): string;
375
- export declare function resolveSlugCollisions(slug: string, usedSlugs: Set<string>): string;
1781
+ /**
1782
+ * Rule-set name→ID for the rule sets a scoped push is NOT applying
1783
+ * (issue #2645, review follow-up).
1784
+ *
1785
+ * Database, group and collection type configs name their rule set by NAME and
1786
+ * resolve it through the map the rule-set apply loop fills in as it goes. Under
1787
+ * `--only` that loop iterates the selected files alone, so `config push --only
1788
+ * database-type-config/orders` threw "Rule set … not found" for a type whose rule set
1789
+ * the operator had no reason to select — the single-object apply path refusing
1790
+ * an ordinary object, which is the opposite of what `--only` is for.
1791
+ *
1792
+ * A rule set the sync state already carries an id for is resolvable without
1793
+ * being applied, which is exactly what a reference needs. One that has never
1794
+ * been pushed still cannot resolve: there is no id to point at, and inventing
1795
+ * one would silently create the reference against nothing. That case keeps
1796
+ * throwing, with the hint to widen the selection.
1797
+ *
1798
+ * Pure, so the seeding is unit-testable without a server or a filesystem.
1799
+ */
1800
+ export declare function unselectedRuleSetIds(params: {
1801
+ /** Every rule-set file in the slot. */
1802
+ files: string[];
1803
+ /** The subset this push is applying — seeded entries never shadow these. */
1804
+ selected: string[];
1805
+ /** The id sync state holds for a rule-set file's key, if it has one. */
1806
+ idForFileKey: (fileKey: string) => string | undefined;
1807
+ /** The `ruleSet.name` a file declares — the name references use. */
1808
+ nameForFile: (file: string) => string | undefined;
1809
+ }): Map<string, string>;
1810
+ export declare function getTestsDir(configDir: string, blockType: string, blockKey: string): string;
376
1811
  export interface TestCaseLookupMaps {
377
1812
  configIdToName: Map<string, string>;
378
1813
  promptIdToKey: Map<string, string>;
379
1814
  }
380
- export declare function serializeTestCase(testCase: any, lookupMaps?: TestCaseLookupMaps): string;
1815
+ export declare function serializeTestCase(testCase: any, lookupMaps?: TestCaseLookupMaps, options?: {
1816
+ /** The sidecar path the warning names, e.g. `prompts/greet.tests/a.toml`. */
1817
+ file?: string;
1818
+ logger?: (message: string) => void;
1819
+ }): string;
381
1820
  export declare function parseTestCaseToml(tomlData: any): any;
382
1821
  /**
383
1822
  * Pull server-side `Script` rows into `transforms/*.rhai` and record
@@ -395,7 +1834,7 @@ export declare function parseTestCaseToml(tomlData: any): any;
395
1834
  * (the old behavior) always produced 0-byte files.
396
1835
  *
397
1836
  * Idempotency: every call writes the active-config body the server
398
- * returned, so re-running `sync pull` on an unchanged server overwrites
1837
+ * returned, so re-running `config pull` on an unchanged server overwrites
399
1838
  * with the same bytes and produces the same `contentHash`. The result
400
1839
  * map always reflects the current server state for the writes performed.
401
1840
  *
@@ -410,11 +1849,17 @@ export declare function parseTestCaseToml(tomlData: any): any;
410
1849
  * `getScript` failure is caught and that one script is skipped without
411
1850
  * aborting the rest of the pull.
412
1851
  */
413
- export declare function pullScripts(client: ApiClient, appId: string, configDir: string, logger?: (msg: string) => void): Promise<{
1852
+ export declare function pullScripts(client: ApiClient, appId: string, configDir: string, logger?: (msg: string) => void,
1853
+ /**
1854
+ * `config pull --only` (issue #2645): which transform names this pull may
1855
+ * write. Defaults to every one the server lists.
1856
+ */
1857
+ selects?: (name: string) => boolean): Promise<{
414
1858
  scriptEntities: Record<string, {
415
1859
  id: string;
416
1860
  modifiedAt: string;
417
1861
  contentHash?: string;
1862
+ semanticHash?: string;
418
1863
  }>;
419
1864
  count: number;
420
1865
  /**
@@ -436,5 +1881,413 @@ export declare function pullScripts(client: ApiClient, appId: string, configDir:
436
1881
  /** Names the server listed, empty when the listing failed. */
437
1882
  serverKeys: string[];
438
1883
  }>;
439
- export declare function registerSyncCommands(program: Command): void;
440
- export {};
1884
+ /** The outcome of one block's test-case pull (#2769). */
1885
+ export type TestCasePullOutcome =
1886
+ /** The listing succeeded; `count` cases were written and stale files removed. */
1887
+ {
1888
+ ok: true;
1889
+ count: number;
1890
+ }
1891
+ /** The listing failed: nothing was written, and the caller preserves state. */
1892
+ | {
1893
+ ok: false;
1894
+ };
1895
+ /**
1896
+ * Every test case a block has, draining the cursor (#2769).
1897
+ *
1898
+ * The endpoint returns 50 per page. Reconciling the sidecar against page one
1899
+ * alone would delete every file past it and drop the ids that keep push from
1900
+ * duplicating them, so the whole set is collected BEFORE anything is written.
1901
+ */
1902
+ export declare function listAllTestCases(client: ApiClient, appId: string, blockType: TestBlockType, blockId: string): Promise<{
1903
+ ok: true;
1904
+ items: any[];
1905
+ } | {
1906
+ ok: false;
1907
+ }>;
1908
+ /**
1909
+ * Copy a block's prior `entities.testCases` records into the pull's fresh map
1910
+ * (#2769), returning how many were carried.
1911
+ *
1912
+ * `config pull` rebuilds the test-case slot from scratch, so a block whose
1913
+ * listing failed would silently lose its ids — and the next push would create a
1914
+ * second copy of every case it could no longer recognize. A failed fetch means
1915
+ * "unknown", so the prior picture stands.
1916
+ */
1917
+ export declare function carryForwardTestCaseEntities(params: {
1918
+ prior: Record<string, any> | undefined;
1919
+ target: Record<string, any>;
1920
+ blockType: string;
1921
+ blockKey: string;
1922
+ }): number;
1923
+ /**
1924
+ * Write one block's test cases into its `<key>.tests/` sidecar and record them
1925
+ * in sync state. Exported so the unit tests can drive it against a stubbed
1926
+ * client on a temp directory.
1927
+ */
1928
+ export declare function pullTestCasesForBlock(params: {
1929
+ client: ApiClient;
1930
+ appId: string;
1931
+ blockType: TestBlockType;
1932
+ blockId: string;
1933
+ blockKey: string;
1934
+ configDir: string;
1935
+ testCaseEntities: Record<string, any>;
1936
+ /** Last sync's records, for the fallbacks a partial failure falls back to. */
1937
+ priorTestCaseEntities?: Record<string, any>;
1938
+ lookupMaps?: TestCaseLookupMaps;
1939
+ logger?: (message: string) => void;
1940
+ }): Promise<TestCasePullOutcome>;
1941
+ /**
1942
+ * One pull leg: every block of a type the pull selected (#2769).
1943
+ *
1944
+ * The selection is the caller's — a leg is handed the blocks `--only` left in,
1945
+ * so a scoped pull never reads, writes or removes a sidecar it was not asked
1946
+ * about. A block whose listing failed keeps its prior state entries and is
1947
+ * reported as skipped rather than silently reconciled to empty.
1948
+ */
1949
+ export declare function pullTestCasesForBlocks(params: {
1950
+ client: ApiClient;
1951
+ appId: string;
1952
+ configDir: string;
1953
+ blockType: TestBlockType;
1954
+ blocks: Array<{
1955
+ id: string;
1956
+ key: string;
1957
+ }>;
1958
+ testCaseEntities: Record<string, any>;
1959
+ priorTestCaseEntities?: Record<string, any>;
1960
+ lookupMaps?: TestCaseLookupMaps;
1961
+ logger?: (message: string) => void;
1962
+ }): Promise<{
1963
+ count: number;
1964
+ skippedBlocks: string[];
1965
+ }>;
1966
+ interface PushResolutionMaps {
1967
+ promptKeyToId: Map<string, string>;
1968
+ promptConfigNameToId: Map<string, string>;
1969
+ workflowConfigNameToId: Map<string, string>;
1970
+ scriptConfigNameToId?: Map<string, string>;
1971
+ integrationConfigNameToId?: Map<string, string>;
1972
+ /**
1973
+ * Fill one block's config name→id entries from the server on demand (#2769),
1974
+ * resolving `false` when the block has no id yet.
1975
+ *
1976
+ * A test case may pin a config on a block this push did not select, or run
1977
+ * under `--dry-run`, where nothing was written to learn the ids from. The
1978
+ * lookup is READ-ONLY and independent of both, so a name that a real push
1979
+ * resolves never reads as broken.
1980
+ */
1981
+ loadBlockConfigs?: (blockType: TestBlockType, blockKey: string) => Promise<boolean>;
1982
+ /** Whether a block is authored locally but not on the server yet. */
1983
+ isPlannedBlock?: (blockType: TestBlockType, blockKey: string) => boolean;
1984
+ }
1985
+ /** A failure row the push reports and exits nonzero on (`applyFailures`). */
1986
+ type ApplyFailure = {
1987
+ type: string;
1988
+ key: string;
1989
+ message: string;
1990
+ };
1991
+ /** The attachment bookkeeping a test case's sync-state record carries. */
1992
+ type RecordedAttachments = {
1993
+ attachments?: Record<string, string>;
1994
+ attachmentFilenames?: string[];
1995
+ };
1996
+ /**
1997
+ * Which attachments a push must upload, and which the SERVER holds that the
1998
+ * sidecar no longer does (#2769).
1999
+ *
2000
+ * Comparison is by content hash: the pre-#2769 state recorded names only, so a
2001
+ * byte change under an unchanged name was invisible. A state entry still in the
2002
+ * old format has no hash to compare against, so every file counts as changed
2003
+ * and re-uploads once (an idempotent overwrite) — after which the state carries
2004
+ * hashes and the next push skips them.
2005
+ *
2006
+ * The removed set is a REPORT, not an action: deleting a remote attachment
2007
+ * because a local file is missing waits for `--prune`.
2008
+ */
2009
+ export declare function planAttachmentPush(params: {
2010
+ local: Array<{
2011
+ filename: string;
2012
+ hash: string;
2013
+ }>;
2014
+ recorded: RecordedAttachments | undefined;
2015
+ }): {
2016
+ upload: string[];
2017
+ removedRemotely: string[];
2018
+ };
2019
+ /**
2020
+ * Create a test case carrying the identity its file name asserts (#2896).
2021
+ *
2022
+ * The one case protocol detection cannot answer is a create into an EMPTY
2023
+ * block: there is no listed record to read `key` off. Push is optimistic there,
2024
+ * and an older server's 400 is the answer — retried once without the key, and
2025
+ * named, because a case created without one will duplicate on the next clone.
2026
+ */
2027
+ export declare function createTestCaseWithIdentity(params: {
2028
+ client: ApiClient;
2029
+ appId: string;
2030
+ blockType: TestBlockType;
2031
+ blockId: string;
2032
+ payload: any;
2033
+ /** Omitted when the server has no keys; then this is exactly the old call. */
2034
+ key?: string;
2035
+ logger?: (message: string) => void;
2036
+ }): Promise<any>;
2037
+ /**
2038
+ * Push one block's authored test-case sidecar. Exported so the unit tests can
2039
+ * drive it against a stubbed client on a temp directory.
2040
+ *
2041
+ * Plain push creates and updates only: a file the operator removed is reported
2042
+ * as a pending deletion and handled by `--prune` (`applyTestCasePrune`).
2043
+ */
2044
+ export declare function pushTestCasesForBlock(params: {
2045
+ client: ApiClient;
2046
+ appId: string;
2047
+ blockType: TestBlockType;
2048
+ blockId: string;
2049
+ blockKey: string;
2050
+ configDir: string;
2051
+ syncState: SyncState | null;
2052
+ dryRun: boolean;
2053
+ changes: Array<{
2054
+ type: string;
2055
+ action: string;
2056
+ key: string;
2057
+ }>;
2058
+ /** Where a rejected create/update/upload goes — it fails the push (#2731 B2). */
2059
+ failures: ApplyFailure[];
2060
+ /**
2061
+ * What push DECLINED to apply (#2880): server drift, a conflict, a live read
2062
+ * that failed with no baseline. Reported exactly as every other converted
2063
+ * type reports it — the caller passes its `recordDeclined`.
2064
+ */
2065
+ declined?: (type: string, key: string, outcome: Extract<PushGateOutcome, {
2066
+ action: "drift" | "conflict" | "immutable" | "live-unavailable";
2067
+ }>, storedModifiedAt?: string) => void;
2068
+ /** The id→name lookups the comparison reads a reference through (#2880). */
2069
+ lookupMaps?: TestCaseLookupMaps;
2070
+ /**
2071
+ * Re-derive `lookupMaps` from the caller's name→id maps (#2880). Called after
2072
+ * this case's references are resolved and before it is compared: resolution
2073
+ * is what LOADS a block's configs, so the comparison would otherwise read a
2074
+ * reference the same push just learned how to read.
2075
+ */
2076
+ refreshLookupMaps?: () => void;
2077
+ resolutionMaps?: PushResolutionMaps;
2078
+ options?: {
2079
+ force?: boolean;
2080
+ };
2081
+ }): Promise<{
2082
+ skipped: number;
2083
+ }>;
2084
+ /**
2085
+ * Whether a sidecar and a live case say the same thing (#2896).
2086
+ *
2087
+ * The corroboration the adoption passes below need: identity nothing states can
2088
+ * only be inferred from content, and only when the inference is unique. It is
2089
+ * the SAME projection `config diff` compares with, memoized per file and per
2090
+ * record, so pairing and the change verdict can never disagree. A file that
2091
+ * cannot be read matches nothing — an unparseable sidecar fails its own push
2092
+ * with a message that names it.
2093
+ */
2094
+ export declare function testCaseContentMatcher(params: {
2095
+ testsDir: string;
2096
+ spec: ConfigDiffSpec;
2097
+ extra?: any;
2098
+ }): (localSlug: string, live: any) => boolean;
2099
+ /**
2100
+ * Which live test case each sidecar manages (#2880 behavior 16, #2896).
2101
+ *
2102
+ * The manifest id comes FIRST, then the identity the COMMITTED tree carries —
2103
+ * the file's basename, matched against the case's stored `key`. Everything
2104
+ * after that is adoption of a case whose identity nothing states: a rename the
2105
+ * manifest still remembers under the old name, a legacy case matched by
2106
+ * content, and finally the slug-of-name rule #2880 shipped.
2107
+ *
2108
+ * The order matters because every pass consumes its claims: identity that IS
2109
+ * recorded always wins over a guess, and a guess is only made when it is
2110
+ * unambiguous in BOTH directions. Where it is not, the file stays unpaired and
2111
+ * is barred from the create path — push refusing to guess is the whole point,
2112
+ * since the failure it replaces is a silently duplicated test case.
2113
+ *
2114
+ * Against a server with no keys the passes that depend on them are skipped
2115
+ * entirely, so pairing is bit-for-bit what #2880 shipped.
2116
+ *
2117
+ * One function for both commands, because a diff row and a push decision that
2118
+ * pair differently are two answers to the same question.
2119
+ */
2120
+ export declare function pairTestCases(input: {
2121
+ blockType: string;
2122
+ blockKey: string;
2123
+ localSlugs: string[];
2124
+ liveCases: any[];
2125
+ managed: Record<string, {
2126
+ id?: string;
2127
+ slug?: string;
2128
+ blockKey?: string;
2129
+ }> | undefined;
2130
+ /** Whether the server carries identity keys at all (#2896). */
2131
+ serverSupportsKeys?: boolean;
2132
+ /** Whether a local sidecar's content equals a live case's projection. */
2133
+ contentMatches?: (localSlug: string, live: any) => boolean;
2134
+ }): {
2135
+ bySlug: Map<string, any>;
2136
+ localOnly: string[];
2137
+ remoteOnly: any[];
2138
+ /** Files adopted from a stale manifest entry: basename → that entry's key. */
2139
+ renamedFrom: Map<string, string>;
2140
+ /** Files barred from the create path this run, with the reason to report. */
2141
+ refused: Map<string, string>;
2142
+ };
2143
+ /**
2144
+ * `config diff` for one block's test-case sidecar (#2769). A failed listing is
2145
+ * an OUTCOME the caller reports as "not compared" — the old helper returned
2146
+ * silently, so a block whose tests could not be fetched simply disappeared from
2147
+ * a report that still read as exhaustive.
2148
+ */
2149
+ export interface TestCaseDiffRow {
2150
+ blockType: string;
2151
+ blockKey: string;
2152
+ slug: string;
2153
+ status: string;
2154
+ /** What a validation error or a degraded comparison has to say. */
2155
+ hint?: string;
2156
+ /** Attachments push would upload from this sidecar (#2880 behavior 19). */
2157
+ attachmentUploads?: string[];
2158
+ /** Managed attachments `push --prune` would delete (never a plain push). */
2159
+ attachmentDeletions?: string[];
2160
+ }
2161
+ export declare function compareTestCasesForBlock(params: {
2162
+ client: ApiClient;
2163
+ appId: string;
2164
+ blockType: TestBlockType;
2165
+ blockId: string;
2166
+ blockKey: string;
2167
+ configDir: string;
2168
+ /** The manifest — identity, and the attachment hashes the plan reads. */
2169
+ syncState?: SyncState | null;
2170
+ lookupMaps?: TestCaseLookupMaps;
2171
+ }): Promise<{
2172
+ ok: true;
2173
+ rows: TestCaseDiffRow[];
2174
+ } | {
2175
+ ok: false;
2176
+ }>;
2177
+ /**
2178
+ * Shape errors in an authored test-case sidecar, for the push preflight
2179
+ * (#2769). The unknown-key check beside it says which keys may appear; this
2180
+ * says whether the ones that are there can be pushed at all — a missing `name`
2181
+ * is a 400 from the server, and malformed JSON text used to be silently dropped
2182
+ * on the way to it, pushing a test case that tested something else.
2183
+ */
2184
+ export declare function testCaseSidecarErrors(filePath: string, tomlData: any): string[];
2185
+ /**
2186
+ * EVERY reason `config push` refuses one test-case sidecar (#2880 criterion 1).
2187
+ *
2188
+ * The four checks the push preflight ran inline: the document's shape, the keys
2189
+ * the definition recognizes, the declared type of each value, and the sidecar's
2190
+ * own authoring rules (#2769). Collected in one place because `config diff`
2191
+ * runs the identical list — it used to run only the last of the four, so a
2192
+ * sidecar with an unknown key or a mistyped ordinary field was reported Synced
2193
+ * by the command whose whole promise is that a Synced file pushes.
2194
+ */
2195
+ export declare function testCaseSidecarPreflightErrors(filePath: string, tomlData: any): string[];
2196
+ /** A managed test case whose authored file is gone (#2769). */
2197
+ export interface TestCaseDeletionCandidate {
2198
+ stateKey: string;
2199
+ blockType: string;
2200
+ blockId: string;
2201
+ blockKey: string;
2202
+ slug: string;
2203
+ /** Absent when the case was never successfully created server-side. */
2204
+ id?: string;
2205
+ /**
2206
+ * The state entry — with a file still on disk — that holds this same id
2207
+ * (#2896). Set when the missing file was RENAMED rather than deleted: the
2208
+ * case is alive under another name, so the row is cleared and nothing is
2209
+ * deleted server-side.
2210
+ */
2211
+ supersededBy?: string;
2212
+ }
2213
+ /** A managed attachment whose local file is gone (#2769). */
2214
+ export interface AttachmentDeletionCandidate extends TestCaseDeletionCandidate {
2215
+ filename: string;
2216
+ }
2217
+ /**
2218
+ * Managed test cases whose `<slug>.toml` the operator removed (#2769).
2219
+ *
2220
+ * Only under a block that is itself still managed: when the BLOCK's file is
2221
+ * gone too, the block's own prune owns the whole sidecar (`removePrunedSidecar`)
2222
+ * and listing its cases here would delete them twice over.
2223
+ */
2224
+ export declare function collectTestCaseDeletions(params: {
2225
+ configDir: string;
2226
+ testCaseEntities: Record<string, any> | undefined;
2227
+ blockExists?: (blockType: string, blockKey: string) => boolean;
2228
+ }): TestCaseDeletionCandidate[];
2229
+ /**
2230
+ * Managed attachments whose local file is gone, for a test case that survives
2231
+ * (#2769). An absent attachment DIRECTORY is the authored spelling of "no
2232
+ * attachments" — Git cannot carry an empty directory — so its recorded files
2233
+ * are candidates too. Still `--prune`-gated: plain push deletes nothing.
2234
+ */
2235
+ export declare function collectAttachmentDeletions(params: {
2236
+ configDir: string;
2237
+ testCaseEntities: Record<string, any> | undefined;
2238
+ blockExists?: (blockType: string, blockKey: string) => boolean;
2239
+ }): AttachmentDeletionCandidate[];
2240
+ /**
2241
+ * What a plain `config push` says about the sidecar deletions it is NOT
2242
+ * applying (#2769), one report for the whole push.
2243
+ *
2244
+ * Attachments belong here rather than in the per-block push: deleting only a
2245
+ * fixture leaves its test-case TOML untouched, so that push skips the case by
2246
+ * hash and never reaches a message of its own — exactly the case an operator
2247
+ * needs told, since the test keeps running against a file the tree no longer
2248
+ * has. Returns the lines to print; empty when nothing is pending.
2249
+ */
2250
+ export declare function formatPendingTestDeletions(params: {
2251
+ testCases: TestCaseDeletionCandidate[];
2252
+ attachments: AttachmentDeletionCandidate[];
2253
+ }): string[];
2254
+ /**
2255
+ * Apply the test-case half of `config push --prune` (#2769).
2256
+ *
2257
+ * Runs after the batch confirmation, alongside the entity prunes: deletes the
2258
+ * cases and attachments whose authored files are gone, drops their state, and
2259
+ * routes failures into the push's failure list. Under `--dry-run` it counts the
2260
+ * same rows and calls nothing.
2261
+ */
2262
+ export declare function applyTestCasePrune(params: {
2263
+ client: ApiClient;
2264
+ appId: string;
2265
+ testCaseEntities: Record<string, any>;
2266
+ candidates: TestCaseDeletionCandidate[];
2267
+ attachmentCandidates: AttachmentDeletionCandidate[];
2268
+ dryRun: boolean;
2269
+ changes: Array<{
2270
+ type: string;
2271
+ action: string;
2272
+ key: string;
2273
+ }>;
2274
+ failures: ApplyFailure[];
2275
+ logger?: (message: string) => void;
2276
+ }): Promise<{
2277
+ deleted: number;
2278
+ deletedAttachments: number;
2279
+ }>;
2280
+ /**
2281
+ * Attach the server-reconciling verbs to the `config` group (issue #2759).
2282
+ *
2283
+ * They used to be a top-level `sync` noun, which split one workflow — managing
2284
+ * the TOML configuration tree — across two nouns by whether a verb happened to
2285
+ * call the API. Users do not think in that distinction; every real workflow
2286
+ * crosses it. So `config` owns both halves, and this function takes the group
2287
+ * `registerConfigCommands` created rather than making one of its own.
2288
+ *
2289
+ * The module keeps its name and its ~50 exported helpers: `sync.ts`,
2290
+ * `resolveSyncDir`, `.primitive-sync.json` and `.primitive/sync/<env>/<appId>/`
2291
+ * are internal identifiers, deliberately left alone (#2759 scope).
2292
+ */
2293
+ export declare function registerConfigSyncCommands(sync: Command): void;