@homeflare/alchemy 0.37.0 → 0.37.2

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 (73) hide show
  1. package/dist/proxmox/ceph-daemon-wire.d.ts +5 -7
  2. package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -1
  3. package/dist/proxmox/ceph-daemon.d.ts +7 -8
  4. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  5. package/dist/proxmox/ceph-fs-distilled.d.ts +31 -5
  6. package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
  7. package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
  8. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  9. package/dist/proxmox/ceph-fs.d.ts +5 -6
  10. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  11. package/dist/proxmox/ceph-pool-settle.d.ts +9 -8
  12. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
  13. package/dist/proxmox/ceph-pool-wire.d.ts +11 -20
  14. package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -1
  15. package/dist/proxmox/group-wire.d.ts +8 -13
  16. package/dist/proxmox/group-wire.d.ts.map +1 -1
  17. package/dist/proxmox/index.js +296 -326
  18. package/dist/proxmox/index.js.map +14 -14
  19. package/dist/proxmox/node-network-wire.d.ts +1 -1
  20. package/dist/proxmox/node-network-wire.d.ts.map +1 -1
  21. package/dist/proxmox/storage-wire.d.ts +8 -11
  22. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  23. package/dist/proxmox/unreadable-read.d.ts +7 -7
  24. package/dist/proxmox/user-wire.d.ts +8 -11
  25. package/dist/proxmox/user-wire.d.ts.map +1 -1
  26. package/dist/proxmox/user.d.ts +5 -5
  27. package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
  28. package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
  29. package/docs/changelog/README.md +22 -0
  30. package/docs/changelog/archive-01.md +176 -0
  31. package/docs/changelog/archive-02.md +180 -0
  32. package/docs/changelog/archive-03.md +179 -0
  33. package/docs/changelog/archive-04.md +178 -0
  34. package/docs/changelog/archive-05.md +177 -0
  35. package/docs/changelog/archive-06.md +180 -0
  36. package/docs/changelog/archive-07.md +180 -0
  37. package/docs/changelog/archive-08.md +178 -0
  38. package/docs/changelog/archive-09.md +181 -0
  39. package/docs/changelog/archive-10.md +182 -0
  40. package/docs/changelog/archive-11.md +182 -0
  41. package/docs/changelog/archive-12.md +183 -0
  42. package/docs/changelog/archive-13.md +180 -0
  43. package/docs/changelog/archive-14.md +178 -0
  44. package/docs/changelog/archive-15.md +181 -0
  45. package/docs/changelog/archive-16.md +182 -0
  46. package/docs/changelog/archive-17.md +183 -0
  47. package/docs/changelog/archive-18.md +55 -0
  48. package/docs/distilled-interim.md +15 -3
  49. package/docs/provider-standard.md +24 -6
  50. package/docs/proxmox.md +26 -0
  51. package/docs/upstream-conformance-bun.md +67 -0
  52. package/docs/upstream-conformance-conforms.md +55 -0
  53. package/docs/upstream-conformance-families.md +95 -0
  54. package/docs/upstream-conformance-grafana.md +106 -0
  55. package/docs/upstream-conformance.md +20 -298
  56. package/package.json +3 -3
  57. package/src/proxmox/ceph-daemon-wire.ts +4 -7
  58. package/src/proxmox/ceph-daemon.ts +7 -8
  59. package/src/proxmox/ceph-fs-delete.test.ts +5 -8
  60. package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
  61. package/src/proxmox/ceph-fs-distilled.ts +45 -19
  62. package/src/proxmox/ceph-fs-wire.ts +5 -93
  63. package/src/proxmox/ceph-fs.ts +13 -15
  64. package/src/proxmox/ceph-pool-settle.ts +11 -10
  65. package/src/proxmox/ceph-pool-wire.ts +11 -20
  66. package/src/proxmox/ceph-pool-write.test.ts +9 -8
  67. package/src/proxmox/group-wire.ts +7 -12
  68. package/src/proxmox/read-failure-propagation.test.ts +5 -5
  69. package/src/proxmox/storage-wire.ts +9 -10
  70. package/src/proxmox/typed-absence.test.ts +146 -0
  71. package/src/proxmox/unreadable-read.ts +7 -7
  72. package/src/proxmox/user-wire.ts +7 -10
  73. package/src/proxmox/user.ts +5 -5
@@ -0,0 +1,180 @@
1
+ # Alchemy changelog archive 6
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ **Clears this family's own cries-wolf false updates**, MEASURED live 2026-09-24: on the agent
6
+ (`read`-role) lane, `bun run plan` showed all 5 storages (`cephfs-tb4`, `cephtb4`, `local`,
7
+ `local-zfs`, `pbs`) as `update` with no warning, because this family reads with `provision`
8
+ (`Datastore.Allocate` on `/storage` — `Datastore.Audit` is not enough) and the pre-migration
9
+ `pveOperations.read` folded a REFUSED `provision` mint into "absent" exactly like every family
10
+ before PR 231/239's fix — `unreadable-read.ts`'s own header already named this family as one of
11
+ the two the fix was written for. Now wired the same way: a refused mint reports `noop` with a
12
+ logged warning.
13
+
14
+ **A missing storage is a 500, not a 404** — MEASURED against the live cluster (TB4, admin lane,
15
+ read-only `GET /storage/hf-measure-nonexistent-probe`): `{"message":"storage '...' does not
16
+ exist\n"}` at HTTP 500, the same shape user.ts/group.ts measured for a missing user/group. So
17
+ `storage-wire.ts` carries the same dual-path read PR 239 established for those two families:
18
+ `readStorageOrFail` (non-folding, used by `diff`, so a genuine transient failure propagates and
19
+ fails the plan loudly) and `readStorage` (folding, used by `read`/`reconcile`'s create-detection).
20
+ `read`'s own provider hook also branches on `output` from the start (PR 239's own follow-up
21
+ finding, applied here proactively): `output === undefined` (Plan.ts's adoption probe, Apply.ts's
22
+ delete recovery — nothing confirmed exists yet) keeps the fold; `output !== undefined`
23
+ (`Drift.ts`'s already-confirmed row) uses the non-folding read instead, so `alchemy drift` never
24
+ reports a transient failure as a silent `{action: 'missing'}` either.
25
+
26
+ **Two measured gaps in `@distilled.cloud/proxmox`'s generated schema, handled rather than
27
+ papered over:**
28
+
29
+ - No `maxfiles` field at all (grepped across the whole package). Silently dropping a declared
30
+ value would be the exact class of bug this migration exists to close, so `StorageProps.maxfiles`
31
+ is now `never` — a compile error, the same treatment `StorageLocator`'s `password`/`keyring`/
32
+ `encryption-key` already get — rather than a silent no-op. None of this cluster's 5 live
33
+ storages set it; a consumer that genuinely needs it should file the gap upstream in the SDK's
34
+ generator, not work around it here.
35
+ - Every hyphenated PVE field name becomes an underscore in distilled's generated TypeScript
36
+ (`prune-backups` -> `prune_backups`, `fs-name` -> `fs_name`, and so on for every plugin field
37
+ `StorageLocator`'s free-form bag can carry — `cephfs-tb4`'s own live locator uses `fs-name`).
38
+ No other migrated family has a hyphenated field, so this is new here. `storage-form.ts`'s
39
+ `underscored` translates a form built with PVE's own names into distilled's shape before the
40
+ actual SDK call, never before the form the generated vendor-constraint tables check
41
+ (`guardWrite`, keyed by PVE's own hyphenated names), which would otherwise silently stop
42
+ inspecting the very fields it exists to catch. MEASURED both ways (a `storage-read-failure.test.ts`
43
+ case run once with the translation and once without it): the wire itself carries PVE's own
44
+ hyphenated name either way — distilled's own "unknown key" passthrough happens to re-encode an
45
+ untranslated key correctly for a body-bearing POST/PUT (unlike the GET case that caused PR 239's
46
+ regression, where no body should exist at all). The translation is still the right call: it
47
+ routes every field through distilled's own declared, typed property rather than its passthrough
48
+ fallback, so whatever per-field wire logic a future plugin field carries beyond a plain rename
49
+ runs correctly rather than silently not running.
50
+
51
+ `storage-read-failure.test.ts` pins all of this against real-shaped fixtures (live TB4 response
52
+ bodies, measured 2026-09-24, nothing secret): the cries-wolf fix, `diff`/`verify()` rejecting on a
53
+ transient failure instead of a false `update`, `alchemy drift` rejecting instead of a silent
54
+ `missing`, and — added after an adversarial review found every test above only ever adopted an
55
+ EXISTING storage, never exercising `reconcile`'s actual create path — a brand-new storage's create
56
+ translating a hyphenated locator field for the real SDK call, and a vendor-constraint violation on
57
+ a hyphenated locator field refused before any write. Each assertion confirmed to fail against the
58
+ pre-fix code (or, for the translation claim, against the untranslated code) before landing.
59
+ `verify/fake-engine.ts` gained a `drift` capability (driving the real `Alchemy.Drift.detect`) in
60
+ PR 239, reused here; that PR's `read-failure-propagation.test.ts` already established the
61
+ transient-failure/drift test pattern for User/Group.
62
+
63
+ **Expected after this releases and the consumer bumps:** on the agent lane, `bun run plan`'s
64
+ 5 storage rows return to `noop`; ACLs and users/groups (PR 231/239) stay unaffected.
65
+
66
+ ## 0.31.2
67
+
68
+ ### Patch Changes
69
+
70
+ - [#239](https://github.com/taslabs-net/homeflare-kit/pull/239) [`1e8985f`](https://github.com/taslabs-net/homeflare-kit/commit/1e8985fc8359304b2c6d03df0fba00781e5e32a0) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Fixes a live regression from kit 0.31.1 (PR 231, the `proxmox/access`
71
+ distilled migration): on the agent (`read`-role) lane, `bun run plan` showed
72
+ every `Proxmox.User`/`Proxmox.Group` row as `update` with no warning — a
73
+ silent false diff, not the loud "read was refused" case that migration was
74
+ meant to fix.
75
+
76
+ **Root cause, measured against the live cluster.** `user.ts`/`group.ts`
77
+ called `access.getAccessUser(props)`/`access.getAccessGroup(props)` with the
78
+ whole declared props object, not just the schema's own `{userid}`/
79
+ `{groupid}` label field. distilled's `buildRequest` treats any OTHER key on
80
+ that object (`target`, `comment`, every other prop this family carries) as
81
+ an "unknown key" and JSON-encodes it onto the request as a body — on what
82
+ must stay a bodyless GET. A permissive client tolerates this; Node/Bun's own
83
+ `fetch` refuses it outright
84
+ (`TypeError [ERR_INVALID_ARG_VALUE]: fetch() request with GET/HEAD method
85
+ cannot have body`), which `runPveWith` retried across every cluster member
86
+ and exhausted identically (`PveClusterExhausted`). The old
87
+ `Effect.orElseSucceed(() => undefined)` then folded that failure into
88
+ "absent", forcing a false `update` on every account and every group — the
89
+ same cries-wolf bug PR 231 had just fixed for a refused credential, now
90
+ triggered by a genuine transport failure instead. Fixed by calling both
91
+ operations with only their declared label field.
92
+
93
+ **The fold itself was too broad, independent of the request-shape bug.**
94
+ `orElseSucceed` turning ANY read failure into "absent" is unsound at `diff`
95
+ time: only a genuine not-found may mean absent, every other failure must
96
+ fail the plan loudly rather than silently forcing a write. `acl.ts` (already
97
+ migrated pre-231) and `role.ts` never needed a fold at all — an ACL/role's
98
+ absence is always a successful list read that just doesn't contain the row,
99
+ so any thrown failure was already a bug; the fold there is now removed
100
+ outright. `api-token.ts`'s absence signal is likewise a measured
101
+ success-path check (`expire`/`privsep` both undefined on a 200), so its
102
+ fold is also removed outright.
103
+
104
+ `user.ts`/`group.ts` are different: a genuinely missing user or group is a
105
+ thrown 500 on this cluster (`"no such user"`/`"no such group"`, not a clean
106
+ 404), MEASURED live — so `reconcile`'s create workflow still needs "absent"
107
+ derived from exactly that failure, or a brand-new declaration could never be
108
+ created. Each now has a dual-path read in its `*-wire.ts` sibling: a
109
+ non-folding `read...OrFail` (used by `diff`, so a genuine transient failure
110
+ propagates and fails the plan loudly) and a folding wrapper kept only for
111
+ `read`/`reconcile` (where folding costs at most a redundant, loudly-refused
112
+ create, never a silent wrong write).
113
+
114
+ **A second, deeper instance of the same bug class, found by adversarial
115
+ review of this fix rather than measured live.** The engine calls a
116
+ provider's `read` hook from four places — an adoption probe and
117
+ interrupted-create recovery in `Plan.ts`, delete recovery in `Apply.ts`, and
118
+ `Drift.ts` (`alchemy drift`/`sync`/`deploy --detect-drift`) — and only the
119
+ first three are "nothing confirmed exists yet" cases where `read`'s own
120
+ fold is safe. `Drift.ts` calls `read` on an ALREADY-CONFIRMED row (its own
121
+ persisted `output`), the same situation `diff` handles, and a folded
122
+ failure there is reported as `{action: 'missing'}` with no error anywhere —
123
+ worse than the original bug, since nothing even logs a warning. Fixed by
124
+ having `user.ts`/`group.ts`'s `read` hook branch on the input's own
125
+ `output` field (documented on `Provider.read`'s own type as "current state
126
+ -> synced state"): `output === undefined` still uses the folding read
127
+ (unchanged, for the three recovery/adoption cases), `output !== undefined`
128
+ now uses the same non-folding `read...OrFail` `diff` already calls.
129
+
130
+ `read-failure-propagation.test.ts` pins all three fixes end to end against
131
+ real-shaped PVE fixtures (live response bodies, nothing secret): a GET that
132
+ would carry a body now fails the assertion outright; a transient 500 on an
133
+ already-adopted `Proxmox.User`/`Proxmox.Group` now rejects `verify()`
134
+ instead of resolving with a false `update`; and (a new `drift` capability
135
+ on the shared `fake-engine.ts` test harness, driving the real
136
+ `Alchemy.Drift.detect`) the same transient failure now rejects `alchemy
137
+ drift` instead of silently reporting the resource missing — each of the
138
+ three new/changed tests was confirmed to fail against the pre-fix code
139
+ before this change landed.
140
+
141
+ **Expected after this releases and the consumer bumps:** on the agent
142
+ lane, `bun run plan`'s users/groups return to `noop`, ACLs stay
143
+ `noop`-with-warning, and the five pre-existing storage rows (a separate,
144
+ not-yet-migrated family) are unaffected.
145
+
146
+ - [#234](https://github.com/taslabs-net/homeflare-kit/pull/234) [`3357caa`](https://github.com/taslabs-net/homeflare-kit/commit/3357caa7c9a412365d3959bbb306628329eba4f8) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `@distilled.cloud/grafana` is not published upstream yet, so this package
147
+ now aliases it onto `@homeflare/distilled-grafana` (0.1.0 pre-release,
148
+ built the distilled way and shipped from this monorepo — see
149
+ `docs/distilled-interim.md`) as a plain `dependencies` entry instead of a
150
+ `1.0.0-rc.12` peer — nothing changes for a consumer's install (the peer
151
+ line is simply gone from the README and the smoke install, the same way
152
+ `/netbox` and `/litellm` already read). `Grafana.Datasource`'s props,
153
+ attributes and generated calls are unchanged — its existing tests pass
154
+ unmodified.
155
+
156
+ The alias also picks up `@distilled.cloud/grafana`'s newly-added folder,
157
+ dashboard and alerting-provisioning operations (see the
158
+ `@homeflare/distilled-grafana` changeset), which fixes the SDK-level gap
159
+ `docs/grafana.md` and `docs/upstream-conformance.md` recorded. No new house
160
+ `Resource` is added here — `Grafana.Folder`/`Dashboard`/`AlertRule`/etc. on
161
+ top of these operations is follow-up work, not part of this change.
162
+
163
+ ⚠️ **Deliberate deviation from `docs/distilled-interim.md`'s step 5.** That
164
+ doc has the interim package publish and get confirmed live
165
+ (`npm view @homeflare/distilled-grafana version`) in its own PR _before_ a
166
+ second PR adds the alias — exactly to dodge the propagation window
167
+ `scripts/publish.ts`'s own comments document twice (2026-09-15,
168
+ `kit@0.1.1`/`cloudflare@0.1.1`: "Your package is being processed and may
169
+ take a few minutes to become available"). This PR does both at once, on
170
+ purpose, so the next teams building `Grafana.Folder`/`Dashboard`/`AlertRule`
171
+ aren't blocked on a second release cycle. `scripts/publish.ts` has no
172
+ dependency-aware ordering, so **after this releases, confirm
173
+ `npm view @homeflare/distilled-grafana version` resolves before anyone
174
+ depends on the new `@homeflare/alchemy` version** — a few minutes' wait,
175
+ not a code change, and self-healing either way (the publish script is
176
+ idempotent and a stuck install just needs a retry).
177
+
178
+ ## 0.31.1
179
+
180
+ ### Patch Changes
@@ -0,0 +1,180 @@
1
+ # Alchemy changelog archive 7
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ - [#231](https://github.com/taslabs-net/homeflare-kit/pull/231) [`11705f2`](https://github.com/taslabs-net/homeflare-kit/commit/11705f2b88d19335be5a115e06f7f6ca883b63b3) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `access` sub-area (decision 43's serial proxmox
6
+ walk-down, PR 2 after `Proxmox.Acl` in PR 209/220) now calls
7
+ `@distilled.cloud/proxmox`'s typed `access.*` operations instead of
8
+ `client.ts`'s hand-rolled, generic `pve()` call: `Proxmox.User`, `Proxmox.Group`,
9
+ `Proxmox.Role` and `Proxmox.ApiToken`. Each gets its own hand-written
10
+ reconcile (read/diff/reconcile/delete), the way `acl.ts` established, rather
11
+ than the shared `pveHandlers`/`pveOperations` factory — a `*-wire.ts` sibling
12
+ per family holds the pure wire-shape functions (create/update forms,
13
+ attributes, `matches`), keeping every file under the house's 250-line cap.
14
+ `client.ts`/`pveOperations` are untouched and still serve every other PVE
15
+ family; nothing in `nodes`, `storage`, `notifications` or PBS has moved yet.
16
+
17
+ `Proxmox.Role` reads via `GET /access/roles` (the list), not the item
18
+ `GET /access/roles/{roleid}` this family used before: distilled's generated
19
+ schema for the item response enumerates a FIXED set of ~47 known privilege
20
+ field names, and a privilege outside that set would silently vanish from
21
+ state on every read. The list's `privs` field is the same plain comma string
22
+ PVE's item read also disagreed with the index about pre-migration, with no
23
+ fixed enumeration to fall behind — `role-wire.ts`'s `find` does the item
24
+ read's old client-side job. Every other family in this PR still reads the
25
+ item it read before.
26
+
27
+ A `distilled-guard.ts` module (`asForm`/`guardWrite`) is the generalized
28
+ form of `acl.ts`'s own local `guardWrite`, needed because distilled's
29
+ generated request interfaces have no index signature and TypeScript refuses
30
+ `Record<string, string | undefined>` for them directly — `body: object`
31
+ plus one internal cast is the fix, applied once here rather than at every
32
+ call site.
33
+
34
+ **The cries-wolf fix** (measured 2026-09-24: `bun run plan` on the agent lane
35
+ showed "19 to update" — 14 ACLs and 5 storages — because a REFUSED read
36
+ folded into "absent" and `diff` forced `update` without comparing a field).
37
+ `credentials.ts`'s `mint` now fails a 403 from OpenBao (a denied
38
+ role — "this identity's AppRole has no grant") with a new typed
39
+ `PveCredentialDenied` (`credential-errors.ts`), split out from the newly
40
+ `mint.ts`-housed `mint`/`authorization` to keep `credentials.ts` under the
41
+ line cap. A new `unreadable-read.ts` module's `readOrUnreadable` `catchTag`s
42
+ exactly that one tag — never any other read failure — turning it into an
43
+ `UNREADABLE` sentinel `diff` can tell apart from a genuine absence. Upstream's
44
+ `Diff` type is exactly `NoopDiff | UpdateDiff | ReplaceDiff`
45
+ (`packages/alchemy/src/Diff.ts@v2.0.0-beta.79`) with no fourth action, and
46
+ `Plan.ts`'s resource pass fails the WHOLE plan if even one resource's `diff`
47
+ throws — so `diff` now reports `{action: 'noop'}` plus a logged warning for
48
+ an unreadable row, never a failed plan and never a forced write. Wired into
49
+ `acl.ts` (the already-migrated family) and every family in this PR;
50
+ `unreadable-read.test.ts` proves it end to end through Alchemy's real Plan
51
+ and Apply, with a fake OpenBao that denies `provision` mid-test. `storage.ts`
52
+ and the other five false updates stay open until `nodes`/`storage` migrates.
53
+
54
+ Distilled's `{userid}`/`{tokenid}` label substitution percent-encodes them
55
+ (`iac@pve` -> `iac%40pve`), where `client.ts`'s plain string concatenation
56
+ sent the `@` literally — a real PVE decodes both the same way, so this is
57
+ not a behaviour change a stack observes, but every fake cluster with an `@`
58
+ in a userid (`user.test.ts`, `api-token-adopt.test.ts`,
59
+ `provision-declare.test.ts`) now decodes the path before matching it.
60
+
61
+ State did not move: every Props/Attributes interface is unchanged, and the
62
+ existing cross-family `adopt-noop.test.ts` cases (now extended with `Group`
63
+ and `User` rows for the new hand-written reconciles) and
64
+ `api-token-adopt.test.ts` pass with the same assertions the pre-migration
65
+ code made. New tests (`group.test.ts`, `user.test.ts`, `role.test.ts`)
66
+ drive a fake PVE server through the real distilled protocol, covering
67
+ create, drift-correction, delete, retain-by-default and the read-back guard.
68
+
69
+ `codegen/constraints.ts` regenerated: `Proxmox.ApiToken` no longer names its
70
+ (always-unreachable) create endpoint anywhere in source, so the generated
71
+ tables and the hand-written ownership ledger
72
+ (`scripts/proxmox-ownership-pve.ts`) both drop that one claim — 74 tabled
73
+ endpoints, not 75.
74
+
75
+ ## 0.31.0
76
+
77
+ ### Minor Changes
78
+
79
+ - [#228](https://github.com/taslabs-net/homeflare-kit/pull/228) [`25610e8`](https://github.com/taslabs-net/homeflare-kit/commit/25610e890ac360456c318dd8dbefd0d54ccf0d3e) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New `@homeflare/alchemy/discord` subpath, built directly on `@distilled.cloud/discord`
80
+ (1.0.0-rc.12) — no hand-rolled client ever existed for this vendor to retire.
81
+ `Discord.ApplicationCommand` (global) and `Discord.GuildApplicationCommand` (guild-scoped)
82
+ declare a slash/user/message command; `reconcile` is one upsert call, matching Discord's own
83
+ create-endpoint semantics (a command with the same name overwrites the old one), the same shape
84
+ `Cloudflare.Snippets.Snippet`'s `putSnippet` uses upstream. `read` follows that same upstream
85
+ reference for a marker-less API exactly: a cold match is `Unowned`, never a silent adopt, and
86
+ both convenience constructors (`applicationCommand`, `guildApplicationCommand`) pipe `adopt(true)`
87
+ by default. Rate limits are handled entirely by the SDK's own bounded default retry policy
88
+ (`Schedule.recurs(8)`, honoring a `429`'s `Retry-After`); this family adds no retry logic of its
89
+ own. Credentials are `DISCORD_BOT_TOKEN`/`DISCORD_TOKEN`, read at call time through the SDK's own
90
+ `CredentialsFromEnv`, never a prop.
91
+
92
+ Walked read-only against the live target, `hf-discord-halibut.service` on CT100: the bot
93
+ self-registers nine guild-scoped commands at every startup (Sapphire's `BulkOverwrite`), so
94
+ `docs/discord.md` documents the ownership conflict and the handover sequence rather than
95
+ declaring `Discord.GuildApplicationCommand` against Halibut's own guilds — that would fight the
96
+ bot's own registration, not replace it. Guild roles, channels and webhooks are not built: Halibut
97
+ uses none of them (measured on CT100), and the engine in `resource.ts` generalizes cleanly to a
98
+ future spec file if a stack ever needs one.
99
+
100
+ SDK gaps recorded in `docs/discord.md`: every generated operation's typed error union is narrower
101
+ than the protocol's own status map (`Forbidden`/`NotFound`/`BadRequest`/`Conflict` are built at
102
+ runtime but not in the exported type — measured via `tsc`, not assumed); `options` is passed
103
+ through opaquely rather than re-modeled from the ~40-type generated union; there is no vendor
104
+ constraint table (unlike NetBox/Paperless); global command propagation (up to an hour, Discord's
105
+ own docs) is not polled.
106
+
107
+ ## 0.30.0
108
+
109
+ ### Minor Changes
110
+
111
+ - [#229](https://github.com/taslabs-net/homeflare-kit/pull/229) [`b4d714e`](https://github.com/taslabs-net/homeflare-kit/commit/b4d714eff527c8c875afca2cb529d5f312bd1efa) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Added `caddy/formatCaddyfile(text)`: pipes a Caddyfile through the LOCAL
112
+ `caddy fmt -` binary (stdin in, formatted text out) — the same formatter the
113
+ `caddy fmt` CLI command runs. There is no admin API endpoint for this
114
+ (`cmd/commandfuncs.go` is CLI-only), so it shells out with
115
+ `ChildProcessSpawner` (S19) rather than going through `@distilled.cloud/caddy`,
116
+ and it never reimplements the formatter in TypeScript: a missing or failing
117
+ binary fails the Effect with a typed `CaddyFmtNotFound` / `CaddyFmtFailed`,
118
+ it never silently returns the input unformatted.
119
+
120
+ Also: `Caddy.Config`'s plan (`diffConfig`) and deploy (`CaddyConfigProvider`'s
121
+ `reconcile`) now surface the adapter's "Caddyfile input is not formatted"
122
+ warning as its own clear line naming the fix, separately from any other
123
+ adapter warnings — it used to be silent at plan time, and just another line
124
+ in the pile at deploy time. Nothing here changes what gets loaded onto Caddy:
125
+ formatting should never change the adapted JSON digest.ts compares (reasoned
126
+ from the Caddyfile grammar; not measured against a real Caddy in this
127
+ package — see docs/caddy-fmt.md).
128
+
129
+ - [#226](https://github.com/taslabs-net/homeflare-kit/pull/226) [`bfc71a3`](https://github.com/taslabs-net/homeflare-kit/commit/bfc71a31e811aab0ccdbbc5ce82ee6803c7ad277) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Add the Google Workspace provider family (`@homeflare/alchemy/google-workspace`): `Group`,
130
+ `GroupMember`, `DomainAlias` and `OrgUnit` over the Admin SDK Directory API, built on
131
+ `@distilled.cloud/google-workspace@1.0.0-rc.12`'s typed `admin_directory_v1` operations
132
+ (S23 — no hand-rolled client). Adopt-first by get-by-key, `retain` on removal for everything
133
+ but membership, and no `User` resource (Google's own `User` schema carries a `password` field).
134
+ Credential setup — domain-wide delegation, the least OAuth scopes each resource needs, and
135
+ where the service-account key lives in OpenBao — is in
136
+ `packages/alchemy/docs/google-workspace.md`.
137
+
138
+ - [#225](https://github.com/taslabs-net/homeflare-kit/pull/225) [`a68c021`](https://github.com/taslabs-net/homeflare-kit/commit/a68c0212f951d3492e5fde981f7f334f777d21fe) Thanks [@taslabs-net](https://github.com/taslabs-net)! - New family: `@homeflare/alchemy/grafana`. `Grafana.Datasource` declares one data source, keyed by
139
+ `uid`, calling `@distilled.cloud/grafana`'s typed `addDataSource`/`getDataSourceByUID`/
140
+ `updateDataSourceByUID`/`deleteDataSourceByUID` operations, `catchTag('NotFound', ...)` in place of
141
+ a status check. Credentials are a `GrafanaTarget` (instance origin + a token env var NAME, never a
142
+ literal value) resolved lazily per call, parameterized per instance rather than fixed like
143
+ Forgejo's — this estate runs more than one Grafana. `grafanaProviders(target)` composes the
144
+ provider with its credentials, mirroring `litellmProviders`.
145
+
146
+ Only `Datasource` ships: `@distilled.cloud/grafana@1.0.0-rc.12` has no create/update/delete
147
+ operations for folders, dashboards, alert rules or contact points, measured against its published
148
+ types — recorded in `docs/grafana.md` and `docs/upstream-conformance.md` rather than worked
149
+ around with a hand-rolled client for the missing pieces.
150
+
151
+ Built and measured, read-only, against the live target `teslamate-grafana.service` on CT100
152
+ (Grafana 13.1.3, `teslamate/grafana:4.2.0`): one file-provisioned datasource, image-baked
153
+ dashboards, no folders, alert rules or contact points. No stack yet imports this subpath.
154
+
155
+ ## 0.29.1
156
+
157
+ ### Patch Changes
158
+
159
+ - [#223](https://github.com/taslabs-net/homeflare-kit/pull/223) [`dc37600`](https://github.com/taslabs-net/homeflare-kit/commit/dc37600d0bb1c72ed2711351e498949af81271c6) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `declareRepoBaseline` and `declareRepoPolicy` declared `GitHub.Repository` and their
160
+ `RepositoryRuleset` as two independent resources with no dependency edge between them (K5,
161
+ kit PR 216). `repoBaselineSettings`/`repoPolicy` already gate `allowAutoMerge` on
162
+ `checks.length > 0`, but with nothing ordering the two resources, a deploy that moves a repo
163
+ from `checks: []` to a non-empty list could apply the repository (turning auto-merge on) before
164
+ the ruleset (adding the required check) — or the ruleset apply could fail outright, leaving
165
+ auto-merge on with nothing required. `gh pr merge --auto` merges a CLEAN pull request the instant
166
+ nothing is outstanding, so that window is exactly the fail-open state K5 exists to prevent.
167
+
168
+ Both callers now thread the ruleset's `rulesetId` through `allowAutoMerge`'s own value
169
+ (`repo-auto-merge-gate.ts`'s `gateAutoMergeOnRuleset`, via `alchemy/Output`'s `map`) whenever a
170
+ declaration turns auto-merge on, instead of writing it as a literal. Alchemy orders resources by
171
+ Output references in props (`Plan.ts`'s `Output.upstreamAny`, `Apply.ts`'s `waitForDeps` —
172
+ see the kit's own `alchemy-output-refs-order-resources` memory), so this makes the engine apply
173
+ the ruleset first, with no change to the value actually sent (`allowAutoMerge` is still exactly
174
+ `true`). A ruleset apply failure now leaves the repository's `reconcile` — and `allowAutoMerge`
175
+ — untouched, proved by a new engine-level ordering test and failure test for each caller.
176
+
177
+ The reverse transition (checks/approvals removed) needs no matching edge: neither caller ever
178
+ declares a rule's removal explicitly, only omits it, and `repository-ruleset-guards.ts`'s
179
+ `requiredChecksOmissionRefusal`/`undeclaredLiveRuleRefusal` already refuse — regardless of apply
180
+ order — to drop a still-live required rule by omission.
@@ -0,0 +1,178 @@
1
+ # Alchemy changelog archive 8
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ Known trade-off (found by adversarial review, 2026-09-24): forcing the ruleset first only helps
6
+ once the repository already exists. A repo and its ruleset created together for the FIRST time,
7
+ with `checks` non-empty from day one, now fails clearly instead of racing — GitHub's own ruleset
8
+ API 404s on a repository that does not exist yet, and the engine now guarantees that order rather
9
+ than leaving it to chance. Both `declareRepoBaseline` and `declareRepoPolicy` document this and a
10
+ new test proves the failure is explicit, not a silent fail-open; the fix is for an ALREADY-LIVE
11
+ repo moving from no checks to some, which is K5's own scenario and the documented use of both
12
+ functions.
13
+
14
+ ## 0.29.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [#222](https://github.com/taslabs-net/homeflare-kit/pull/222) [`1e6285f`](https://github.com/taslabs-net/homeflare-kit/commit/1e6285fb229dca7455afa5b52e1c79ca3286dc26) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Declare `@distilled.cloud/discord`, `@distilled.cloud/google-workspace` and `@distilled.cloud/grafana` (1.0.0-rc.12) as exact-pinned peers, ahead of the Discord, Google Workspace and Grafana provider families. Per the peer contract they are required, and they appear in the README install line and the smoke install.
19
+
20
+ - [#208](https://github.com/taslabs-net/homeflare-kit/pull/208) [`2d28264`](https://github.com/taslabs-net/homeflare-kit/commit/2d28264e71d9f141ff0771f57dab49d6daa79904) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `GitHub.RepositoryRuleset`'s `rules.pullRequest.extraApprovalForUnattributedChanges` now accepts
21
+ `true`, not only `false`. Re-read live 2026-09-23, 8 rulesets carry
22
+ `require_extra_approval_for_unattributed_changes: true` (aop, cloudflareforms,
23
+ doesthishelp-workeropen, homeflare-anyauth, homeflare-desktop, loggarr, magictransit,
24
+ proxmox-tb4), so this resource could not declare their exact shape before: it would either omit
25
+ the field (leaving it unmanaged, refused once a caller also carries a `bypassActors`/rule
26
+ declaration for the same ruleset) or send `false`, which is real drift against a live `true` on
27
+ every plan.
28
+
29
+ The wire builder now sends whatever is declared instead of hardcoding `false`; `undefined` still
30
+ means "no opinion" (GitHub defaults an absent key to `true`). `repoBaselineRuleset()` in
31
+ repo-baseline-data.ts is unchanged — the house baseline still pins `false` for the repos that
32
+ fit it; a caller with a live `true` declares `RepositoryRuleset` directly with the live value,
33
+ the same pattern kit PR 205 established for `bypassActors` and rule presence.
34
+
35
+ Tested against aop's full live shape (`gh api repos/taslabs-net/aop/rulesets/14572279`,
36
+ re-read 2026-09-23): declaring the live value (including `true`) is now a genuine zero-write
37
+ adopt, and declaring `false` against a live `true` is still real drift, never a silent noop.
38
+
39
+ - [#214](https://github.com/taslabs-net/homeflare-kit/pull/214) [`9af4441`](https://github.com/taslabs-net/homeflare-kit/commit/9af4441462743476da58ee06af877ec0b346aa1f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `GitHub.RepositoryRuleset`'s "never-reported context" guard (`refuseUnreportedContexts` /
40
+ `hasContextReportedSuccess`) checked only the default branch's current tip. The house's rendered
41
+ CI (`packages/config/src/repo-shape/ci.ts`) triggers on `pull_request` only, deliberately (no
42
+ `push: [main]` — a squash-merged commit re-testing an already-green PR was ~41% of the mini's CI
43
+ load, measured 2026-09-15..22), so `ci`, `secret scan` and `CodeQL` never report on the tip
44
+ itself. The guard refused adding any of them everywhere, including homeflare-builds' pending
45
+ first ruleset.
46
+
47
+ `hasContextReportedSuccess` now falls back to up to `RECENT_MERGED_PR_LIMIT` (10) recent merged
48
+ pull requests' head SHAs — one bounded list call, never the repo's full PR history — checked only
49
+ if the tip itself has no reported success. A context that has never reported success ANYWHERE
50
+ (not the tip, not any recent merged head) is still refused: the guard's whole purpose is
51
+ unchanged, only where it is willing to look for evidence widened.
52
+
53
+ The core logic (`contextReportedSuccess`, `hasRefReportedSuccess`, `recentMergedHeadShas`) is now
54
+ exported as plain functions over a real `OctokitClient`, not only reachable through the
55
+ `GitHubCredentials`-gated `RulesetOctokit` — the same pure-function seam `desiredWireRuleset`
56
+ already uses — so `repository-ruleset-octokit.test.ts` measures it against a real `@octokit/rest`
57
+ instance with a fetch shim (no network), mirroring the H15 wire test: accepting a context that
58
+ reports only on a recent merged head, refusing one that reports nowhere, and refusing one whose
59
+ only success is older than the bound (the limit is real, not decorative).
60
+
61
+ ### Patch Changes
62
+
63
+ - [#219](https://github.com/taslabs-net/homeflare-kit/pull/219) [`40c08eb`](https://github.com/taslabs-net/homeflare-kit/commit/40c08ebfc3ca04de7338deb95b217fbdce6c50d7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `caddy/*` family (`Caddy.Config`, `caddyWithFile`, `localCaddyAdmin`) now calls
64
+ `@distilled.cloud/caddy`'s typed `admin` operations (`adaptConfig`, `loadConfig`,
65
+ `getConfig`) instead of a hand-rolled promise-based `CaddyAdmin.request()` interface.
66
+ Every operation's error channel is `catchTag`-able (`Caddy.CaddyOpError`); `isUnreachable`
67
+ (caddy-http-client.ts) is a type guard over the SDK's own `HttpClientError`, replacing the
68
+ old `CaddyUnreachableError` class, and gates the same "plan without Caddy" escape hatch
69
+ config.ts always had.
70
+
71
+ The measured local transport survives unchanged in shape: loopback TCP or a unix socket,
72
+ Host/Origin headers as the Caddy CLI sends them, retrying only a connection nothing
73
+ accepted (`ECONNREFUSED`/`ENOENT`) — now built as an Effect `HttpClient.HttpClient` layer
74
+ (`caddy-http-client.ts`) the SDK's protocol runs over, over `node:http` (not
75
+ `FetchHttpClient`: only `node:http` dials a unix socket on both Bun and Node). The
76
+ `LoadRefused` 200-trap (a refused `/load` that still answers 200 with the error appended
77
+ after the adapter's warnings) is first-class in the SDK's own `protocol.ts`, not
78
+ re-detected here.
79
+
80
+ Two real gaps this migration found and fixed at the source, never worked around in the
81
+ resource:
82
+
83
+ - `@distilled.cloud/caddy`'s `AdaptConfig`/`LoadConfig` JSON-encoded the Caddyfile TEXT and
84
+ overwrote the caller's `Content-Type` — fixed in the distilled clone's `protocol.ts` and
85
+ shipped as `@homeflare/distilled-caddy@0.2.1` (sibling changeset).
86
+ - The SDK's own default retry policy treats any transport-level `HttpClientError` as
87
+ retryable, including a `POST /load` that was already accepted before the connection
88
+ reset — `local-admin.ts` now disables it (`Caddy.Retry.Retry`) so caddy-http-client.ts's
89
+ own narrower ECONNREFUSED/ENOENT-only retry is the only one in play.
90
+
91
+ `providers.ts`'s `caddyProviders()` (and this family's own tests) also fix a wiring bug the
92
+ migration surfaced: `CaddyConfigProvider().pipe(Layer.provide(caddyAdminLayer(...)))`
93
+ seals `caddyAdminLayer`'s services away from the provider's `read`/`diff`/`reconcile`
94
+ handlers once they are built, so the engine's later call to any of them died with "Service
95
+ not found". `Layer.provideMerge` keeps those services live for every later call.
96
+
97
+ State did not move: props and attributes stay byte-identical (`configSha256`, `endpoint`,
98
+ `sourceFile`) — proven by the family's existing tests (updated only where the typed-error
99
+ message text itself changed, from the old ad hoc `method path -> status` strings to the
100
+ SDK's own tagged errors) plus fake-caddy.ts, a real HTTP server, so every test already
101
+ drives the real distilled wire protocol, not a re-implementation of it.
102
+
103
+ - [#209](https://github.com/taslabs-net/homeflare-kit/pull/209) [`1eaf80f`](https://github.com/taslabs-net/homeflare-kit/commit/1eaf80f9ef2ad5e616f1a459ed6396819be15e6c) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `Proxmox.Acl` (the first resource in the PVE
104
+ `access/ACL` sub-area, per decision 43's serial proxmox walk-down) now calls
105
+ `@distilled.cloud/proxmox`'s typed `access.listAccessAcl`/`access.putAccessAcl`
106
+ operations instead of `client.ts`'s hand-rolled, generic `pve()` call. A new
107
+ `distilled-pve.ts` module (`runPve`/`runPveWith`) replaces `pve()`/`pveWith`
108
+ for a migrated family: the same OpenBao lease reuse (`lease-cache.ts`,
109
+ unchanged) and the same cluster-member failover safety rule (`members.ts`,
110
+ unchanged — a write may repeat only on a provably pre-send transport
111
+ failure, never on an HTTP answer or a post-connect timeout), now re-running
112
+ the whole typed operation per member instead of retrying one already-built
113
+ request. `client.ts`/`pveOperations` are untouched and still serve every
114
+ other PVE family; `Proxmox.Acl` is the first to move off them, chosen
115
+ because it was already the one family that didn't fit the generic
116
+ path+form shape (no create/delete verb — PVE has only GET/PUT on
117
+ `/access/acl`).
118
+
119
+ One dead branch is removed, not preserved: the old code's generic factory
120
+ POSTed on a "live read undefined" case that its own comments called
121
+ unreachable on a healthy cluster and documented as actively misleading (PVE
122
+ has no POST here, so the 501 it got back said nothing about the real
123
+ failure). `@distilled.cloud/proxmox` has no `createAccessAcl` at all — the
124
+ vendor schema has no POST for the generator to make one from — so there is
125
+ nothing to call that way any more. `reconcile` now always PUTs, the only
126
+ write PVE actually implements for this path, so a read that genuinely fails
127
+ surfaces its real cause instead of a confusing 501.
128
+
129
+ A trap found while writing the member-failover test: `@distilled.cloud/core`'s
130
+ default retry policy retries transport failures automatically (not just
131
+ 5xx answers), which could have resent a write to the same member several
132
+ times before this file's own cluster failover ever saw a result to
133
+ classify — the hand-rolled client never auto-retried anything. `distilled-pve.ts`
134
+ disables the SDK's retry (`Retry.none`) on every attempt, keeping
135
+ member failover the only place a request repeats, matching the original
136
+ behavior exactly.
137
+
138
+ State did not move: `AclProps`/`AclAttributes` are unchanged, and the
139
+ existing cross-family `adopt-noop.test.ts` case for `Proxmox.Acl` (an
140
+ already-matching grant is read-only) passes unmodified against the new
141
+ code. New tests (`acl.test.ts`, `distilled-pve.test.ts`) drive a fake PVE
142
+ server through the real distilled protocol — real path assembly, real
143
+ form-urlencoded PUT bodies, real `{"data": ...}` envelope — covering
144
+ create, drift-correction, delete, identity-replace, the read-back guard,
145
+ and cluster-member failover for both a read and a write.
146
+
147
+ This is PR 1 of a serial, sub-area-by-sub-area migration
148
+ (access/ACL, then nodes/storage, then notifications, then PBS, …) of the
149
+ ~98-resource PVE family plus the PBS resources onto
150
+ `@distilled.cloud/proxmox`/`@distilled.cloud/proxmox-backup`. The other PVE
151
+ and PBS resources still call `client.ts`'s `pve()`, unchanged; `client.ts`
152
+ is deleted only once nothing imports it.
153
+
154
+ - [#215](https://github.com/taslabs-net/homeflare-kit/pull/215) [`72c448d`](https://github.com/taslabs-net/homeflare-kit/commit/72c448dbaec8f2acb7b5109aa7387a4409185fe8) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Fix a review finding on the `litellm/*` distilled migration (`@homeflare/distilled-litellm`'s
155
+ `CredentialsFromEnv`, S20): a missing or misspelled `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY`
156
+ used to die as an unrecoverable Effect defect and crash the whole engine, instead of the typed
157
+ `ConfigError` `LitellmOpError` already declared — a regression this migration made newly
158
+ reachable from `LiteLLM.PassThroughEndpoint`'s `read`/`reconcile`/`delete` (the retired
159
+ hand-rolled `credentials.ts` failed typed). Fixed at the source, per house rule: the distilled
160
+ `litellm` package's own `credentials.ts` no longer ends in `Effect.orDie`, copied forward into
161
+ `@homeflare/distilled-litellm` unchanged. No prop, attribute or public API changed.
162
+
163
+ `netbox/*`'s `CredentialsFromEnv` has the identical shape and is not fixed by this changeset —
164
+ tracked separately (`docs/upstream-conformance.md` finding 9).
165
+
166
+ - [#210](https://github.com/taslabs-net/homeflare-kit/pull/210) [`2002713`](https://github.com/taslabs-net/homeflare-kit/commit/20027131b716058de035bd1d8423054f3e9dbff0) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `litellm/*` family (`LiteLLM.PassThroughEndpoint`) now calls `@distilled.cloud/litellm`'s
167
+ typed `misc` operations instead of a hand-rolled `Effect HttpClient` client. The old
168
+ status-carrying `LitellmBadRequestError`/`LitellmUnauthorizedError`/`LitellmHttpError`/
169
+ `LitellmTransportError` are gone: every failure the SDK's four operations declare (`BadRequest`,
170
+ `NotFound` on update, `UnprocessableEntity`, plus the shared default HTTP errors) is the SDK's own
171
+ typed error, `catchTag`'d. `deletePassThroughEndpoint`'s re-list-on-ambiguous-delete trick is
172
+ unchanged, now keyed on the `BadRequest` tag instead of a status code. The per-base-URL write
173
+ semaphore LiteLLM's whole-list storage forces is unchanged, moved into the new `operations.ts`.
174
+ `client.ts` and the kit's own hand-generated `generated/pass-through.ts` are both deleted — the
175
+ SDK's `misc.PassThroughGenericEndpoint` is the same shape, generated from the same LiteLLM 1.100.0
176
+ OpenAPI document. The `codegen/litellm.ts` generator and `tests/litellm-manifest.test.ts` that
177
+ kept that file current are retired with it; the `litellm-openapi` manifest entry stays, recorded
178
+ as consumed by nothing, the same pattern the two UniFi entries already establish.