@homeflare/alchemy 0.36.0 → 0.37.1

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 (116) hide show
  1. package/README.md +14 -0
  2. package/dist/argocd/cluster.d.ts +102 -0
  3. package/dist/argocd/cluster.d.ts.map +1 -0
  4. package/dist/argocd/credentials.d.ts +37 -0
  5. package/dist/argocd/credentials.d.ts.map +1 -0
  6. package/dist/argocd/fake-argocd.d.ts +42 -0
  7. package/dist/argocd/fake-argocd.d.ts.map +1 -0
  8. package/dist/argocd/git-credentials.d.ts +29 -0
  9. package/dist/argocd/git-credentials.d.ts.map +1 -0
  10. package/dist/argocd/index.d.ts +19 -0
  11. package/dist/argocd/index.d.ts.map +1 -0
  12. package/dist/argocd/index.js +306 -0
  13. package/dist/argocd/index.js.map +17 -0
  14. package/dist/argocd/providers.d.ts +13 -0
  15. package/dist/argocd/providers.d.ts.map +1 -0
  16. package/dist/argocd/repo-creds.d.ts +67 -0
  17. package/dist/argocd/repo-creds.d.ts.map +1 -0
  18. package/dist/argocd/repository.d.ts +87 -0
  19. package/dist/argocd/repository.d.ts.map +1 -0
  20. package/dist/argocd/resource.d.ts +90 -0
  21. package/dist/argocd/resource.d.ts.map +1 -0
  22. package/dist/cloudflare/index.js +4 -4
  23. package/dist/cloudflare/index.js.map +3 -3
  24. package/dist/cloudflare/r2-bucket-lock.d.ts +14 -5
  25. package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -1
  26. package/dist/litellm/index.js.map +2 -2
  27. package/dist/litellm/operations.d.ts.map +1 -1
  28. package/dist/proxmox/ceph-daemon-form.d.ts +4 -5
  29. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
  30. package/dist/proxmox/ceph-daemon-wire.d.ts +31 -0
  31. package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -0
  32. package/dist/proxmox/ceph-daemon.d.ts +40 -6
  33. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  34. package/dist/proxmox/ceph-fs-distilled.d.ts +33 -0
  35. package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -0
  36. package/dist/proxmox/ceph-fs-wire.d.ts +20 -11
  37. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  38. package/dist/proxmox/ceph-fs.d.ts +30 -6
  39. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  40. package/dist/proxmox/ceph-osd-tree.d.ts +12 -3
  41. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -1
  42. package/dist/proxmox/ceph-osd-write.d.ts +17 -2
  43. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  44. package/dist/proxmox/ceph-osd.d.ts +6 -0
  45. package/dist/proxmox/ceph-osd.d.ts.map +1 -1
  46. package/dist/proxmox/ceph-pool-settle.d.ts +19 -12
  47. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
  48. package/dist/proxmox/ceph-pool-wire.d.ts +37 -0
  49. package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -0
  50. package/dist/proxmox/ceph-pool.d.ts +10 -5
  51. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  52. package/dist/proxmox/group-wire.d.ts +8 -13
  53. package/dist/proxmox/group-wire.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +605 -483
  55. package/dist/proxmox/index.js.map +20 -17
  56. package/dist/proxmox/storage-wire.d.ts +15 -11
  57. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  58. package/dist/proxmox/unreadable-read.d.ts +7 -7
  59. package/dist/proxmox/user-wire.d.ts +8 -11
  60. package/dist/proxmox/user-wire.d.ts.map +1 -1
  61. package/dist/proxmox/user.d.ts +5 -5
  62. package/docs/argocd-kubernetes.md +100 -0
  63. package/docs/argocd.md +139 -0
  64. package/docs/credentials.md +9 -0
  65. package/docs/distilled-interim.md +15 -3
  66. package/docs/litellm.md +17 -0
  67. package/docs/peers.md +7 -0
  68. package/docs/provider-standard.md +24 -6
  69. package/docs/proxmox.md +20 -0
  70. package/docs/unifi.md +45 -0
  71. package/docs/upstream-conformance-bun.md +67 -0
  72. package/docs/upstream-conformance-conforms.md +55 -0
  73. package/docs/upstream-conformance-families.md +95 -0
  74. package/docs/upstream-conformance-grafana.md +106 -0
  75. package/docs/upstream-conformance.md +20 -298
  76. package/package.json +8 -3
  77. package/src/argocd/cluster.test.ts +120 -0
  78. package/src/argocd/cluster.ts +222 -0
  79. package/src/argocd/credentials.ts +38 -0
  80. package/src/argocd/fake-argocd.ts +60 -0
  81. package/src/argocd/git-credentials.ts +71 -0
  82. package/src/argocd/index.ts +40 -0
  83. package/src/argocd/providers.ts +17 -0
  84. package/src/argocd/repo-creds.test.ts +123 -0
  85. package/src/argocd/repo-creds.ts +115 -0
  86. package/src/argocd/repository.test.ts +167 -0
  87. package/src/argocd/repository.ts +165 -0
  88. package/src/argocd/resource.ts +150 -0
  89. package/src/cloudflare/r2-bucket-lock.test.ts +54 -18
  90. package/src/cloudflare/r2-bucket-lock.ts +25 -18
  91. package/src/litellm/operations.ts +8 -4
  92. package/src/proxmox/ceph-daemon-form.ts +7 -8
  93. package/src/proxmox/ceph-daemon-wire.ts +112 -0
  94. package/src/proxmox/ceph-daemon-write.test.ts +158 -0
  95. package/src/proxmox/ceph-daemon.ts +70 -53
  96. package/src/proxmox/ceph-fs-delete.test.ts +81 -0
  97. package/src/proxmox/ceph-fs-distilled.ts +100 -0
  98. package/src/proxmox/ceph-fs-wire.ts +17 -31
  99. package/src/proxmox/ceph-fs-write.test.ts +67 -0
  100. package/src/proxmox/ceph-fs.ts +68 -73
  101. package/src/proxmox/ceph-osd-tree.ts +14 -5
  102. package/src/proxmox/ceph-osd-write.test.ts +61 -0
  103. package/src/proxmox/ceph-osd-write.ts +33 -6
  104. package/src/proxmox/ceph-osd.ts +6 -0
  105. package/src/proxmox/ceph-pool-settle.ts +24 -17
  106. package/src/proxmox/ceph-pool-wire.ts +101 -0
  107. package/src/proxmox/ceph-pool-write.test.ts +146 -0
  108. package/src/proxmox/ceph-pool.ts +87 -98
  109. package/src/proxmox/group-wire.ts +7 -12
  110. package/src/proxmox/read-failure-propagation.test.ts +5 -5
  111. package/src/proxmox/storage-wire.ts +9 -10
  112. package/src/proxmox/typed-absence.test.ts +146 -0
  113. package/src/proxmox/unreadable-read.ts +7 -7
  114. package/src/proxmox/user-wire.ts +7 -10
  115. package/src/proxmox/user.ts +5 -5
  116. package/src/proxmox/distilled-task-await.test.ts +0 -124
@@ -1,10 +1,11 @@
1
1
  # Upstream conformance, family by family
2
2
 
3
- Status: open ledger. Verified 2026-09-22 by reading `origin/main` `925454b` against
3
+ Status: open ledger. Original audit: 2026-09-22, reading `origin/main` `925454b` against
4
4
  [provider-standard.md](./provider-standard.md) (`alchemy@2.0.0-beta.79`). Nothing in any
5
5
  family was changed by this audit. Each row names the rule it breaks and the upstream
6
6
  replacement or fix. A row is crossed off in the PR that fixes it, with that PR's evidence.
7
- A decision marked **maintainer** is not an agent's to make.
7
+ A decision marked **maintainer** is not an agent's to make. S20/S21 and decision-49
8
+ corrections below were checked on 2026-09-24; this is not a fresh audit of every row.
8
9
 
9
10
  ## Ranked findings
10
11
 
@@ -22,12 +23,12 @@ then tidiness.
22
23
  `reconcileLock`/`deleteLock` (S19): distilled's operations are already Effects, so there
23
24
  is nothing left to promise-wrap.
24
25
 
25
- Still open, and out of scope for that task (props/attributes were required to stay
26
- byte-identical, so neither touches the wire contract):
27
- - `readLock`, and now `reconcileLock`/`deleteLock` too, end in `Effect.orDie` for the
28
- error channel distilled leaves after `NoSuchBucket` (S20). A full fix means a typed
29
- refusal error for `InvalidRoute`/`CloudflareRateLimited`/`CloudflareError`, which is a
30
- separate, larger change than the transport swap.
26
+ ✅ **S20 and idempotent delete fixed 2026-09-24:** [kit PR #266](https://github.com/taslabs-net/homeflare-kit/pull/266)
27
+ removes lifecycle `Effect.orDie`, propagates distilled's typed SDK tags unchanged, and
28
+ folds `NoSuchBucket` to success on delete. This follows beta.79's `R2/BucketSippy.ts`;
29
+ no blanket refusal remap is needed. The existing read already folds that tag to absence.
30
+
31
+ Still open (outside that error-handling change):
31
32
  - `reconcileLock` still skips the PUT whenever `output.rules` equals the declaration,
32
33
  without reading the live lock (S10). Neither `--force` nor `drift --repair` can
33
34
  restore lock rules removed out of band. ⚠️ Reasoned from the source, not measured live.
@@ -139,7 +140,7 @@ then tidiness.
139
140
  6. **`openbao/*` has no upstream equivalent, and it conforms on the contract** (Effect
140
141
  `HttpClient`, strict `Unowned`, retain). It diverges in three ways:
141
142
  - 30 `Effect.die` sites in non-test source, several of them on `diff` and
142
- `reconcile` paths (S20);
143
+ `reconcile` paths (defect risk; S20 no longer separately bans `Effect.die`);
143
144
  - 42 test files on `node:test` (S43);
144
145
  - `Bao.*` type strings, where the vendor name is `OpenBao` (H14).
145
146
 
@@ -147,300 +148,21 @@ then tidiness.
147
148
  distilled's shape is the S23 path. **Decision:** maintainer, on whether the kit authors
148
149
  a distilled package.
149
150
 
150
- 7. **`proxmox/*` is the model for S38.** Its types and constraints are generated from the
151
- vendor schema, and a manifest records the version and sha256. It diverges in four ways:
152
- - the generic factory's `read` returns plain attributes, so every family except `Lxc`
153
- adopts silently, against the house's own H1;
154
- - 27 `Effect.die` sites in non-test source (S20);
155
- - 7 test files on `node:test`;
156
- - `Pbs.*` type strings (H14).
157
- 8. **`talos/*`.** Its `talosctl` calls go through `ChildProcessSpawner`, as upstream's Docker
158
- provider does, which conforms. Its file handling uses `Bun.file`, `Bun.write`,
159
- `Bun.YAML` and `node:fs` (S42), and it uses `Effect.promise` in 3 files (S19). The
160
- kubeconfig it writes is meant to be consumed through upstream's
161
- `Kubernetes.KubeConfig({ path })`.
162
- 9. **`netbox/*` conforms mostly.** Its constraints are generated from NetBox 4.7.0's
163
- OpenAPI. Adopting first is documented as deliberate.
164
- - ✅ **S23 fixed 2026-09-23** (branch `claude2/distilled-netbox-family`, mirroring the
165
- Forgejo migration above, decision 42). `Netbox.Prefix` now calls
166
- `@distilled.cloud/netbox`'s typed `ipam` operations instead of a hand-rolled `Effect
167
- HttpClient` client. The old status-carrying `NetboxError` and its `cause.status ===
168
- 404` check are gone entirely — `Netbox.Prefix` locates by a server-side list filter,
169
- which never 404s (an empty page is a normal 200), so nothing here checks a status
170
- code at all, a stronger form of the same rule `catchTag('NotFound', …)` enforces for
171
- a family that reads by direct key. `client.ts` is deleted. Not published upstream
172
- yet, so aliased onto `@homeflare/distilled-netbox@0.2.0` as a plain `dependencies`
173
- entry, not a peer — [distilled-interim.md](./distilled-interim.md). State did not
174
- move: props/attributes stay byte-identical, proven by the family's unchanged
175
- existing tests plus new tests against a fake NetBox exercising the real distilled
176
- protocol.
177
- - ⛔ **Still diverges on credentials (S24):** `NETBOX_TOKEN` / `NETBOX_URL` are read at
178
- call time, now through the SDK's `CredentialsFromEnv` rather than a hand-rolled
179
- `token()` — same divergence, unchanged by the transport swap.
180
- 10. **`litellm/*` has no upstream equivalent** (LiteLLM has no upstream Alchemy family, the
181
- same situation `openbao/*` is in above), and conforms on the transport contract.
182
- - ✅ **S23 fixed 2026-09-24** (branch `claude2/litellm-distilled`, mirroring the NetBox
183
- migration above, decision 43). `LiteLLM.PassThroughEndpoint` now calls
184
- `@distilled.cloud/litellm`'s typed `misc` operations (`operations.ts`) instead of a
185
- hand-rolled `Effect HttpClient` client. The old status-carrying `LitellmBadRequestError`
186
- family is gone: every failure the four operations declare (`BadRequest`, `NotFound` on
187
- update, `UnprocessableEntity`, plus the shared `Unauthorized`/`TooManyRequests`/server
188
- errors) is the SDK's own typed error, `catchTag`'d — `deletePassThroughEndpoint`'s
189
- re-list-on-ambiguous-`BadRequest` trick (S21: a status-and-a-real-read decision, never
190
- body text) is unchanged, now keyed on the tag instead of the status code. The
191
- per-base-URL write semaphore this vendor's whole-list storage forces (docs/litellm.md)
192
- is unchanged too — the SDK has no opinion on it, so it stays one layer above the typed
193
- calls, in `operations.ts`. `client.ts` and the kit's own hand-generated
194
- `generated/pass-through.ts` are both deleted (the SDK's `misc.PassThroughGenericEndpoint`
195
- is the same shape, generated from the same LiteLLM 1.100.0 OpenAPI document — see
196
- `codegen/manifest.json`'s `litellm-openapi` entry, left in place and unconsumed rather
197
- than deleted, the same pattern the two UniFi entries already establish). Not published
198
- upstream yet, so aliased onto `@homeflare/distilled-litellm@0.2.0` as a plain
199
- `dependencies` entry, not a peer — [distilled-interim.md](./distilled-interim.md). State
200
- did not move: props/attributes stay byte-identical, proven by the family's unchanged
201
- existing tests (now against a fake LiteLLM exercising the real distilled protocol
202
- through `FetchHttpClient.Fetch`, not a loopback `Bun.serve`) plus the engine-level
203
- replace test through `fake-stack.ts`. No stack in this estate currently imports
204
- `litellmProviders`/`LiteLLM.PassThroughEndpoint` (measured 2026-09-24 across
205
- homeflare-landscape's own repositories, including this one's `stacks/`), so there is no
206
- live plan to re-run.
207
- - ⛔ **Still diverges on credentials (S24):** `LITELLM_PROXY_URL` / `LITELLM_PROXY_API_KEY`
208
- are read at call time, now through the SDK's `CredentialsFromEnv` rather than a
209
- hand-rolled `resolveCreds` — same divergence, unchanged by the transport swap.
210
- - ✅ **S20 fixed 2026-09-24** (same branch, PR review pass): `CredentialsFromEnv` ended in
211
- `Effect.orDie`, so a missing/misspelled env var died as an engine-crashing defect
212
- instead of the typed `ConfigError` `LitellmOpError` already declared — a regression
213
- this migration made newly reachable (the retired hand-rolled `credentials.ts` failed
214
- typed). Fixed at the source: `@distilled.cloud/litellm`'s own `credentials.ts`
215
- (`homeflare/litellm@4ad19154`, commit local, not pushed — S22), copied forward here.
216
- `netbox/*`'s `CredentialsFromEnv` has the identical `orDie` shape and is unfixed —
217
- out of scope for this item; tracked as its own follow-up (task `task_3bbe1684`).
218
- 11. **Every family repeats `list: () => Effect.succeed([])`**, 30 times, and only
219
- `R2BucketLock` and `MeshNode` declare `nuke`. The constructor already defaults `list`
220
- (S12). **Fix:** write the reason where it differs, and declare `nuke: { skip: true }`
221
- where nuke must never reach the object.
222
- 12. **Resource JSDoc is not in upstream's generator format.** Zero files use `@resource`,
223
- `**Example:**` or `### Section` (S31, H10). **Decision:** maintainer, because it moves
224
- the house's glyph rationale into `//` comments.
225
- 13. **Tests never use `alchemy/Test/Bun`.** Every lifecycle is proven against loopback
226
- fakes (S28, H12). Live suites need a place to run, and that is a maintainer decision.
227
- 14. **`discord/*` is new (2026-09-24, task-authorized, kit PR TBD) and built directly on
228
- `@distilled.cloud/discord` — no hand-rolled `client.ts` ever existed to retire.**
229
- `Discord.ApplicationCommand` and `Discord.GuildApplicationCommand` call the SDK's
230
- typed operations, `catchTag`'d through the shared `DiscordOpError` union (S21), with no
231
- status sniffing anywhere in `resource.ts`. It follows upstream's `Snippet.ts` reference
232
- exactly for a marker-less API (S7, S8): a cold `read` returns `Unowned(attrs)`, and both
233
- convenience constructors pipe `adopt(true)` (H5) — a stricter posture than `netbox/*`'s
234
- documented H1 gap, not a repeat of it. Rate-limit handling is the SDK's own default
235
- `Retry` policy (bounded: `Schedule.recurs(8)`, S26) — this family adds nothing on top.
236
- Full detail, the live Halibut census and its ownership-handover sequence, and every SDK
237
- gap: [`discord.md`](./discord.md). ⛔ **Diverges on S24/S25 the same way every distilled
238
- family in this ledger does:** credentials are read at call time through the SDK's own
239
- `CredentialsFromEnv`, not a house `alchemy/Auth` provider — unchanged from `netbox/*`
240
- and `litellm/*`'s entries above. **Gap, not yet fixed:** no vendor constraint table
241
- (unlike NetBox/Paperless); `options` passed through opaquely rather than modeled from
242
- the schema (`docs/discord.md#sdk-gaps`).
243
-
244
- 15. **`grafana/*` (added 2026-09-24) ships only `Datasource` — a real SDK gap, not scope-trimming.**
245
- Measured against the published `@distilled.cloud/grafana@1.0.0-rc.12` tarball's
246
- `lib/services/grafana.d.ts` (4,938 lines): the package has no create/read/update/delete
247
- operations for folders (only `updateFolderPermissions` exists), plain dashboards (only
248
- snapshot/public-dashboard routes exist — no `POST /dashboards/db`, no
249
- `GET/DELETE /dashboards/uid/{uid}`), alert rules or contact points (both have only a
250
- `routeGet*Export` read-only route under `/v1/provisioning/`, no create/update/delete). S1
251
- found no upstream `Alchemy` family for any of these either. Full detail:
252
- [grafana.md](./grafana.md#the-sdk-gap--why-only-datasource-ships).
253
-
254
- ✅ **S22 fixed 2026-09-24** (branch `claude2/distilled-grafana`): root cause measured to be
255
- `skipDeprecated: true` dropping 64 operations Grafana's own spec marks `deprecated: true` in
256
- favor of a separate, undocumented-here Kubernetes-apiserver route — not a spec omission. RFC-6902
257
- patches un-deprecate exactly the 32 operations this finding named (folders, dashboards,
258
- alerting-provisioning writes); the other 32 stay excluded. Shipped as
259
- `@homeflare/distilled-grafana` (0.1.0 pre-release, 0.2.0 once this PR's `minor` changeset
260
- releases it), aliased onto `@distilled.cloud/grafana` the same S22 route
261
- `@homeflare/distilled-netbox` established. `Grafana.Datasource`'s props/attributes/generated code
262
- are unchanged (diffed operation-by-operation against the prior commit).
263
-
264
- ✅ **`Grafana.Folder`/`Grafana.Dashboard` shipped** (PR 245, on top of the same
265
- `@distilled.cloud/grafana` operations S22 unblocked) — the uid-required doctrine, the
266
- provisioned-object refusal, the parentUid-is-create-only refusal and the volatile-field
267
- normalization that makes an unchanged dashboard a true noop are all in
268
- [grafana-folder-dashboard.md](./grafana-folder-dashboard.md), not repeated here.
269
-
270
- ✅ **`Grafana.ContactPoint`/`Grafana.MuteTiming`/`Grafana.MessageTemplate` shipped** (kit PR
271
- 250, first of three stacked PRs — `Grafana.AlertRuleGroup` then `Grafana.NotificationPolicy`
272
- follow):
273
- the foreign-provenance refusal (`alerting-provenance.ts`, an allowlist of Grafana's own
274
- `ProvenanceNone`/`ProvenanceAPI` values, measured against `pkg/services/ngalert/models` and
275
- Grafana's v13.1 docs) and the secret-settings-ref seam (`secret-refs.ts`, shared with
276
- `Grafana.Datasource`) are both new shared building blocks — full detail:
277
- [grafana-alerting.md](./grafana-alerting.md).
278
- - 🔴 **New SDK gap found, not fixed here:** `MuteTimeInterval`'s generated type has no
279
- `provenance`/`version` fields, and the SDK's own `TimeInterval` type is missing the real
280
- Alertmanager time-interval fields (`weekdays`/`times`/`months`/etc.) entirely — Grafana's real
281
- API returns and accepts all of them. NOT a data-loss gap: `@distilled.cloud/core`'s response
282
- decoding never runs a strict schema decode (`JSON.parse` + a key-rename pass that leaves any
283
- unmodeled key verbatim — `protocol-http.ts`'s `mapKeys`, confirmed by driving the real
284
- operation through this family's fake-Grafana harness), so `mute-timing.ts` reads/writes both
285
- through a locally-widened type instead. Same S22 fix route as the other named gaps
286
- (regenerate the distilled clone's spec, copy forward) — not attempted here, since
287
- `packages/distilled-grafana/src/` is vendored and never hand-edited.
288
- - Measured, read-only, same census as PR 245's: `teslamate-grafana` has no contact points, mute
289
- timings or templates configured either (consistent with the alerting-provisioning gap having
290
- been real, not merely unexploited).
291
-
292
- ✅ **`Grafana.AlertRuleGroup` shipped** (kit PR 254, second of three, branched fresh off `main`
293
- after PR 250 merged): group, not per-rule, is the unit — `PUT .../rule-groups/{Group}` owns
294
- evaluation `interval` and the full ordered `rules[]` together, which per-rule operations
295
- cannot express (no `interval` field, no reordering). Rule order is significant, proven by a
296
- dedicated test. The per-rule provenance refusal (each `ProvisionedAlertRule.provenance`, since
297
- the group type itself carries none) reuses `alerting-provenance.ts` unchanged, including the
298
- missing-means-foreign fix from PR 250's review. A declared group whose folder does not exist
299
- refuses as a typed `BadRequest` (`RoutePutAlertRuleGroupError` has no `NotFound` case) rather
300
- than being auto-created — this resource never calls `Grafana.Folder`'s create path. A whole-group
301
- write silently dropping a live rule not in the declaration (found by that PR's own adversarial
302
- review) is now warned about loudly rather than silent, never a refusal. Full detail:
303
- [grafana-alerting-rules.md](./grafana-alerting-rules.md).
151
+ ## Remaining family findings
304
152
 
305
- ✅ **`Grafana.NotificationPolicy` shipped** (this PR, third and last of the stacked PRs,
306
- branched fresh off `main` after PR 254 merged): a SINGLETON — `fetchLive` never returns
307
- `undefined` (`RouteGetPolicyTreeError` has no `NotFound` case), so this resource has no create
308
- or delete in the sense every sibling resource does, only adopt-and-update. `destroy` never
309
- calls `routeResetPolicyTree` without an explicit `allowReset: true` prop, a gate independent of
310
- (and in addition to) `defaultRemovalPolicy: 'retain'` — two gates, since resetting wipes the
311
- WHOLE instance's routing back to Grafana's bare default, not one object. The tree is opaque
312
- `Record<string, unknown>` (same reasoning as `mute-timing.ts`'s `timeIntervals` — Grafana's
313
- matcher-expression types are complex unions this family does not hand-model), compared via
314
- `subset-match.ts` for Grafana-injected-default tolerance, with route order significant. The
315
- same dropped-item warning kit PR 254 introduced for rule groups is mirrored here for dropped
316
- routes. Full detail: [grafana-notification-policy.md](./grafana-notification-policy.md).
153
+ The remaining sequence is split into smaller documents to keep each below 200 lines.
317
154
 
318
- This completes the alerting-provisioning family this project set out to cover.
319
- - ⛔ **Same open gaps as `forgejo/*` and `netbox/*` above:** `read` never answers `Unowned`
320
- (H1), and credentials come from an explicit env var NAME at call time rather than an
321
- `alchemy/Auth` provider (S24) — here the house's own `grafanaCredentials(target)`, not the
322
- SDK's `CredentialsFromEnv`, because more than one Grafana instance exists on this estate
323
- (grafana.md's own Credentials section).
324
- - Measured, read-only, on `teslamate-grafana.service` (CT100, `teslamate/grafana:4.2.0`,
325
- Grafana 13.1.3): one datasource (file-provisioned, not API-managed — a live example this
326
- family COULD adopt), dashboards baked into the image (not API-managed either way), and no
327
- folders, alert rules or contact points configured — consistent with the gap being real
328
- rather than merely unexploited. No stack yet imports `@homeflare/alchemy/grafana`.
155
+ [Findings 7–14](./upstream-conformance-families.md) cover Proxmox, Talos, NetBox,
156
+ LiteLLM, list/JSDoc/test conventions and Discord. [Finding 15: Grafana](./upstream-conformance-grafana.md)
157
+ records the SDK migration, shipped resources and the scoped deprecated-API departure.
329
158
 
330
159
  ## Conforms
331
160
 
332
- - **`paperless/*` (`Tag`, `DocumentType`, `StoragePath`, `CustomField`)** — ✅ **S23 fixed
333
- 2026-09-24** (branch `claude2/distilled-paperless-family`, decision 43, mirroring the
334
- netbox/forgejo migrations above). Every call now goes through `@distilled.cloud/paperless-ngx`'s
335
- typed operations, `catchTag('NotFound', …)` replaced the status-carrying `PaperlessError`
336
- union, and `client.ts`/`errors.ts`/`credentials.ts` are deleted. The shared
337
- `matching.ts`/`matching-locate.ts` engine (locate-by-name before state, by `output.id` after —
338
- PR 163) is unchanged in shape. Not published upstream yet, aliased onto
339
- `@homeflare/distilled-paperless-ngx@0.3.0` — [distilled-interim.md](./distilled-interim.md).
340
- State did not move: props/attributes stay byte-identical, proven by `tag.test.ts` and the full
341
- PR 163 regression suite (`tag-identity.test.ts`) against a fake exercising the real distilled
342
- protocol. ⛔ **Still diverges on credentials (S24):** `PAPERLESS_URL`/`PAPERLESS_TOKEN` are read
343
- at call time, now through the SDK's `CredentialsFromEnv` — same divergence, unchanged by the
344
- transport swap.
345
- - **`github/RepositoryRuleset`** (added 2026-09-23) closes the hazard the row above names:
346
- probes by name (`read`/`reconcile` both), answers `Unowned` on a cold name match (H1),
347
- normalizes before comparing so a matching live ruleset is a true noop, and refuses
348
- (rather than silently drops) a live rule type it does not model. Two recorded
349
- divergences: the type string sits in upstream's own `GitHub` namespace (H14), and the
350
- client is Octokit rather than distilled because distilled's `S.Struct` request schemas
351
- have no field for `require_extra_approval_for_unattributed_changes` (H15) — see
352
- [repository-ruleset.md](./repository-ruleset.md). `declareRepoPolicy` is rewired onto
353
- it; the row below is now describing upstream `GitHub.Ruleset` itself, which this family
354
- does not touch and remains usable directly.
355
- - **`github/declareRepoPolicy`** composes upstream `GitHub.Repository` and
356
- `GitHub.Ruleset`. Its one hazard is upstream's own: `Ruleset.read` returns `undefined`
357
- without prior output, so a first deploy creates a duplicate
358
- ([repo-policy.md](./repo-policy.md)). The fix belongs upstream: read by name and answer
359
- `Unowned`. Since 2026-09-23 `declareRepoPolicy` no longer takes this path (see the
360
- `RepositoryRuleset` row above); this remains accurate for upstream `Ruleset` on its own.
361
- - **`cloudflare/website.ts`** sets house defaults over upstream `Website.Astro` and
362
- `Website.Vite`.
363
- - **`ownership/*`** is built only on `AdoptPolicy`, `Stack`, `State` and `Artifacts`. Its
364
- apply-time re-check (H2) exists because beta.79 skips the adoption probe while props hold
365
- an Output. That fix also belongs in the engine upstream.
366
- - **`verify/*`** (`hf-adopt-verify`) runs Alchemy's own planner. It exists because beta.79
367
- labels every cold adoption as an update (H6).
368
- - **`google-workspace/*`** (new 2026-09-24, `Group`, `GroupMember`, `DomainAlias`, `OrgUnit`)
369
- is built directly on `@distilled.cloud/google-workspace@1.0.0-rc.12`'s typed
370
- `unstable/admin_directory_v1` operations from the first commit — no hand-rolled client ever
371
- existed for it to migrate off (S23), so there is no `client.ts` deletion to record. Every
372
- `fetchLive` is get-by-key with its own `catchTag('NotFound', …)`, the same split forgejo's and
373
- netbox's engines use (resource.ts). Get-by-key rather than NetBox's list-then-disambiguate,
374
- since Directory addresses every object this family models by a stable key. ⛔ **Diverges on
375
- credentials, same shape as the rest of this ledger (S24):** `GOOGLE_ACCESS_TOKEN` is read at
376
- call time through the SDK's own `CredentialsFromEnv` — but here the SDK provides no way to
377
- MINT that token at all (no service-account/DWD support), so a Bun wrapper outside this
378
- package (H8, unwritten by this PR) is load-bearing in a way NetBox's/Forgejo's simple
379
- API-token env vars are not. SDK gaps (the `unstable/` service tree, no per-operation error
380
- beyond the shared 4xx set, no group-alias update) and the full credential/scope setup:
381
- [google-workspace.md](./google-workspace.md).
161
+ [Conforming building blocks](./upstream-conformance-conforms.md) records the measured
162
+ conformance and remaining exceptions for Paperless, GitHub, ownership, verify and
163
+ Google Workspace.
382
164
 
383
165
  ## The Bun line
384
166
 
385
- These are non-test files under `src/` that the standard (S42) says must be portable. "Exp"
386
- marks files exported from a subpath `index.ts`. `git grep` on `origin/main` finds 23 non-test
387
- files under `src/` that call `Bun.*` or import `node:*`/`bun:*`. Six of them are outside
388
- S42's scope:
389
-
390
- - four loopback fakes (`caddy/fake-caddy.ts`, `openbao/fake-bao.ts`, `proxmox/fake-pve.ts`,
391
- `proxmox/fake-pve-lxc.ts`), which are test-side under S44;
392
- - `proxmox/provision-cli-fake.ts`, which is used only by tests;
393
- - `verify/args.ts`, which belongs to the CLI and so is tooling under S43.
394
-
395
- That leaves 17 files. The table lists them, plus `provision-cli-fake.ts`, which should move
396
- out of `src/`. The six out-of-scope files still ship in the tarball's `src/`, but no export
397
- reaches them.
398
- ⚠️ The first version of this table said 16 files and left out `launchd/job-form.ts`.
399
- ⚠️ Corrected 2026-09-22: 14 of the 17 break upstream's rule. `launchd/job-form.ts`,
400
- `proxmox/write-only.ts` and `proxmox/pbs-notification-target-wire.ts` use only synchronous
401
- `node:crypto` or `Buffer`. Upstream allows those inside `Effect.sync`
402
- (`AGENTS.md@v2.0.0-beta.79#Workflow`), and 40 upstream `src/` files import `node:crypto`,
403
- among them `Fly/Secret.ts` and `Railway/Variable.ts`. They were listed under a blanket
404
- `node:*` ban that upstream does not have.
405
-
406
- | file | API | exp | portable replacement |
407
- | ---------------------------------------------------------- | -------------------------------------------- | --- | -------------------------------------------- |
408
- | `openbao/cloudflare-roles-config.ts` | `Bun.YAML` | yes | parse on the tooling side; pass data in |
409
- | `openbao/digest.ts` (14 importers) | `Bun.CryptoHasher` | — | `Crypto.digest` + hex; keep `canonical()` ⚠️ |
410
- | `openbao/cloudflare-parity-snapshot.ts` | `Bun.file`, `Bun.Glob` | — | `FileSystem` + `Path` |
411
- | `openbao/approle-login-form.ts` | `Bun.file` | — | `FileSystem.readFileString` |
412
- | `openbao/approle-login-result.ts` | `Bun.inspect` | — | a plain formatter |
413
- | `openbao/forgejo-bootstrap.ts` | `Bun.spawn` | — | unreferenced; `ChildProcessSpawner` or drop |
414
- | `talos/credentials.ts` | `Bun.write/file/env/randomUUIDv7`, `node:fs` | — | `FileSystem.makeTempFileScoped` |
415
- | `talos/kubeconfig.ts` | `Bun.file` | yes | `FileSystem` |
416
- | `talos/talos-machine-config.ts` | `Bun.file` | yes | `FileSystem` |
417
- | `talos/values.ts` | `Bun.YAML`, `node:crypto` | — | tooling-side parse; hash may stay |
418
- | `launchd/local-runner.ts` | `node:child_process`, `node:fs/promises` | yes | H3 above |
419
- | `launchd/sudo-stage.ts` | `node:fs/promises`, `node:os`, `node:path` | — | `FileSystem`, `Path` |
420
- | `launchd/job-form.ts` | `node:crypto` (`createHash`) | — | allowed in `Effect.sync` |
421
- | `linux/ssh-runner.ts` | `node:child_process`, `node:crypto` | yes | H3 above |
422
- | `caddy/caddy-http-client.ts` | `node:http`, `node:stream` (unix socket) | yes | inherent — an Effect `HttpClient`, not H3 ⚠️ |
423
- | `proxmox/write-only.ts`, `pbs-notification-target-wire.ts` | `node:crypto`, `node:buffer` | — | allowed in `Effect.sync` ⚠️ |
424
- | `proxmox/provision-cli-fake.ts` | `Bun.spawn`, `node:fs` | — | test-only; move out of `src/` |
425
-
426
- The ⚠️ rows:
427
-
428
- - `openbao/digest.ts`: every `Bao.*` family persists this digest in state, so a swap must
429
- hash the same bytes (`canonical()`, lowercase hex), or every row plans an update.
430
- `Crypto.digest` from `effect/Crypto` is in every stack's services and fails with a typed
431
- error; `alchemy/Util/sha256` wraps WebCrypto in `Effect.promise`.
432
- - `proxmox/write-only.ts`: a salted scrypt seal with `timingSafeEqual`. No Alchemy helper
433
- replaces it. A plain `sha256` would make a write-only secret cheap to brute-force from
434
- state, and it would change the persisted seal format.
435
- - `caddy/caddy-http-client.ts`: unlike `launchd/local-runner.ts` and `linux/ssh-runner.ts`
436
- (H3, plain `async` interfaces), this is already the Effect `HttpClient.HttpClient` the
437
- distilled SDK's own protocol runs on — `node:http` here is the same kind of unavoidable,
438
- bottom-of-the-stack platform adapter `@effect/platform-node`'s own `NodeHttpClient.ts` is,
439
- extended with `socketPath` dialing (that module has none) because a unix socket is the
440
- measured transport a local Caddy needs and neither `FetchHttpClient` (Bun-only `unix`
441
- option) nor stock `NodeHttpClient` (URL-only) speaks one on both Bun and Node. There is no
442
- more-portable replacement to point to; the file already conforms to S19 (no async/await,
443
- no raw `Promise` — `Effect.callback`/`Effect.tryPromise` throughout).
444
-
445
- Test runners: 51 files import `node:test`/`node:assert` (openbao 42, proxmox 7, forgejo 1,
446
- talos 1), and 126 import `bun:test`. S43 says `bun:test`.
167
+ [The Bun line inventory](./upstream-conformance-bun.md) preserves the runtime-boundary
168
+ measurements, portable replacements and state-format constraints.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.36.0",
3
+ "version": "0.37.1",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "default": "./dist/index.js"
30
30
  },
31
+ "./argocd": {
32
+ "types": "./dist/argocd/index.d.ts",
33
+ "default": "./dist/argocd/index.js"
34
+ },
31
35
  "./cloudflare": {
32
36
  "types": "./dist/cloudflare/index.d.ts",
33
37
  "default": "./dist/cloudflare/index.js"
@@ -120,11 +124,12 @@
120
124
  "@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
121
125
  "@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.2.0",
122
126
  "@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
123
- "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.2.0",
124
- "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.2.0",
127
+ "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.0",
128
+ "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.0",
125
129
  "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
126
130
  },
127
131
  "peerDependencies": {
132
+ "@distilled.cloud/argocd": "1.0.0-rc.12",
128
133
  "@distilled.cloud/cloudflare": "1.0.0-rc.12",
129
134
  "@distilled.cloud/discord": "1.0.0-rc.12",
130
135
  "@distilled.cloud/forgejo": "1.0.0-rc.12",
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `Argocd.Cluster`'s `spec` against a fake Argo CD — the same three doctrine-required shapes as
3
+ * `repository.test.ts`, plus the nested `config.tlsClientConfig` credential path.
4
+ */
5
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
6
+ import * as Effect from 'effect/Effect';
7
+ import { fakeArgocd, fakeArgocdLayer, fakeFailure } from './fake-argocd.ts';
8
+ import { argocdOperations } from './resource.ts';
9
+ import { spec } from './cluster.ts';
10
+
11
+ const SERVER = 'https://talos.example.com:6443';
12
+ const ENV_VAR = 'ARGOCD_CLUSTER_TALOS_BEARER_TOKEN';
13
+
14
+ const liveCluster = (overrides: Record<string, unknown> = {}) =>
15
+ Response.json({ name: 'talos', server: SERVER, ...overrides });
16
+
17
+ describe('Argocd.Cluster spec.fetchLive', () => {
18
+ test('a live cluster decodes into attributes, including nested tls config', async () => {
19
+ const fake = fakeArgocd((method) =>
20
+ method === 'GET'
21
+ ? liveCluster({
22
+ config: { tlsClientConfig: { insecure: false, serverName: 'talos.internal' } },
23
+ })
24
+ : fakeFailure(404, 'not found'),
25
+ );
26
+ const live = await Effect.runPromise(
27
+ spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
28
+ );
29
+ expect(live?.server).toBe(SERVER);
30
+ expect(live?.config?.tlsClientConfig?.serverName).toBe('talos.internal');
31
+ });
32
+
33
+ test('a 404 folds to undefined', async () => {
34
+ const fake = fakeArgocd(() => fakeFailure(404, 'cluster not found'));
35
+ const result = await Effect.runPromise(
36
+ spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
37
+ );
38
+ expect(result).toBeUndefined();
39
+ });
40
+
41
+ test('a 403 propagates — not folded to absent', async () => {
42
+ const fake = fakeArgocd(() => fakeFailure(403, 'forbidden'));
43
+ const failure = await Effect.runPromise(
44
+ Effect.flip(
45
+ spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
46
+ ),
47
+ );
48
+ expect(failure._tag).toBe('Forbidden');
49
+ });
50
+ });
51
+
52
+ describe('Argocd.Cluster destroy — idempotent (S11)', () => {
53
+ test('deleting an already-absent cluster makes no DELETE call', async () => {
54
+ const fake = fakeArgocd(() => fakeFailure(404, 'cluster not found'));
55
+ await Effect.runPromise(
56
+ argocdOperations(spec)
57
+ .destroy({ server: SERVER })
58
+ .pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
59
+ );
60
+ expect(fake.seen.every((call) => call.method !== 'DELETE')).toBe(true);
61
+ });
62
+
63
+ test('the DELETE call itself returning 404 (a race with an external removal) still succeeds', async () => {
64
+ // ⚠️ fetchLive's pre-check finds the cluster live, but something else deregisters it before
65
+ // this call's own DELETE lands — matching upstream's Hetzner/Certificate.ts `deleteById`.
66
+ const fake = fakeArgocd((method) =>
67
+ method === 'GET' ? liveCluster() : fakeFailure(404, 'cluster not found'),
68
+ );
69
+ await Effect.runPromise(
70
+ argocdOperations(spec)
71
+ .destroy({ server: SERVER })
72
+ .pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
73
+ );
74
+ expect(fake.seen.filter((call) => call.method === 'DELETE')).toHaveLength(1);
75
+ });
76
+ });
77
+
78
+ describe('Argocd.Cluster spec.upsert — write-only bearer token', () => {
79
+ beforeEach(() => {
80
+ delete process.env[ENV_VAR];
81
+ });
82
+ afterEach(() => {
83
+ delete process.env[ENV_VAR];
84
+ });
85
+
86
+ test('refuses with a typed domain error when the declared env var is unset', async () => {
87
+ const fake = fakeArgocd(() => fakeFailure(404, 'unused'));
88
+ const failure = await Effect.runPromise(
89
+ Effect.flip(
90
+ spec
91
+ .upsert({ credentials: { bearerToken: { fromEnv: ENV_VAR } }, server: SERVER })
92
+ .pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
93
+ ),
94
+ );
95
+ expect(failure._tag).toBe('ArgocdClusterSecretEnvUnsetError');
96
+ expect(fake.seen).toEqual([]);
97
+ });
98
+
99
+ test('resolves the bearer token into config, never the FromEnv reference itself', async () => {
100
+ process.env[ENV_VAR] = 'kube-bearer-abc123';
101
+ const fake = fakeArgocd((method) =>
102
+ method === 'POST' ? Response.json({ server: SERVER }) : fakeFailure(404, 'x'),
103
+ );
104
+ await Effect.runPromise(
105
+ spec
106
+ .upsert({
107
+ credentials: { bearerToken: { fromEnv: ENV_VAR } },
108
+ namespaces: ['argocd', 'apps'],
109
+ server: SERVER,
110
+ })
111
+ .pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
112
+ );
113
+ const [create] = fake.seen.filter((call) => call.method === 'POST');
114
+ expect(create?.path).toContain('upsert=true');
115
+ const body = create?.body as { config?: { bearerToken?: string }; namespaces?: string[] };
116
+ expect(body.config?.bearerToken).toBe('kube-bearer-abc123');
117
+ expect(body.namespaces).toEqual(['argocd', 'apps']);
118
+ expect(JSON.stringify(body)).not.toContain('fromEnv');
119
+ });
120
+ });