primitive-admin 1.0.61 → 1.0.62

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