@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,183 @@
1
+ # Alchemy changelog archive 12
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ 1. `R2BucketLock` uses `Effect.orDie` and `Effect.promise`. Its reconcile trusts `output`
6
+ rather than the live lock, and it sits on a second Cloudflare SDK where
7
+ `@distilled.cloud/cloudflare/r2` already has the lock operations.
8
+ 2. `forgejo/client.ts` is hand-rolled, while `@distilled.cloud/forgejo@1.0.0-rc.12` is
9
+ generated against Forgejo 16.0.3.
10
+ 3. `MeshNode` is a deliberate twin of `Cloudflare.Tunnel.WarpConnector`.
11
+ 4. Shipped provider code calls `Bun.*` or the `node:*` modules upstream bans (14 of 17
12
+ listed files; the other 3 use only synchronous `node:crypto` or `Buffer`, which upstream
13
+ allows inside `Effect.sync`), and 51 test files run on `node:test` instead of
14
+ `bun:test`.
15
+
16
+ What the ledger records is the gap for each finding. It changes nothing.
17
+
18
+ ## 0.19.1
19
+
20
+ ### Patch Changes
21
+
22
+ - [#128](https://github.com/taslabs-net/homeflare-kit/pull/128) [`ba55148`](https://github.com/taslabs-net/homeflare-kit/commit/ba5514820e32f9d546f1a5eb0f92c7f156f2a978) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Estate topology out of the constraint proofs. PR [#118](https://github.com/taslabs-net/homeflare-kit/issues/118)'s create-form proofs used the real
23
+ declarations verbatim, which put a metrics hostname, a cluster's `api-path-prefix` and three Ceph
24
+ pool names into `src` — and `src` ships in the npm tarball of a public repository, so they would
25
+ have stayed in the git history forever. `lxc-harness.ts` states the rule and these tests did not
26
+ follow it: a production-SHAPED declaration with placeholder values, because the proof is about
27
+ which keys the create form sends and which bounds they face, never about the strings.
28
+
29
+ No behaviour changes; the same forms are checked against the same tables.
30
+
31
+ - [#136](https://github.com/taslabs-net/homeflare-kit/pull/136) [`64d4c36`](https://github.com/taslabs-net/homeflare-kit/commit/64d4c36091054fa05b716f1d4029c1feea955289) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Check a systemd rename at plan time. A unit's name or directory change is a delete-first replace, and Alchemy deletes the old unit before reconciling the new one, so a masked name, a unit file someone else owns, or a runner that will not write the new path used to be noticed only after the old unit was already stopped. Those checks now run while planning, and again at apply when the new name was still an Output and the diff could not see the rename. A file byte-identical to this declaration's render stays exempt: it is a deploy that died between write and reload.
32
+
33
+ ## 0.19.0
34
+
35
+ ### Minor Changes
36
+
37
+ - [#120](https://github.com/taslabs-net/homeflare-kit/pull/120) [`512bf1a`](https://github.com/taslabs-net/homeflare-kit/commit/512bf1a6a973bdbd1c9688d295dcf8a067820a36) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Linux hosts on the existing HostRunner seam: `@homeflare/alchemy/linux`.
38
+
39
+ The kit could declare a guest and nothing inside it. This adds the families that gap
40
+ was missing, on the same seam the launchd subpath already drives a Mac through — so
41
+ `HostFile`'s ownership rules, `checkWrite` and the adoption doctrine come along unchanged.
42
+
43
+ - `sshRunner({ host })` — a Linux `HostRunner` over the operator's own ssh config.
44
+ ⛔ `BatchMode=yes` and host verification untouched; ⛔ every remote script reports its
45
+ status behind a per-runner nonce, so a dropped connection is an Error and never a
46
+ "nothing is there"; ⛔ `privileged: false` — nothing calls sudo.
47
+ - `HostDirectory` — because no file resource creates a parent. One directory, never a
48
+ chain; delete is `rmdir`, never recursive.
49
+ - `RemoteFile` — a whole file, or one MANAGED REGION (`BEGIN`/`END` markers) inside a
50
+ file this resource does not own. ⛔ Every byte outside the markers stays identical, the
51
+ file's own mode and owner are copied back, and a delete removes only the block.
52
+ - `SystemdUnit` / `SystemdTimer` — unit file, `daemon-reload`, enable/disable,
53
+ start/stop. ⛔ A deploy NEVER mass-restarts: a unit restarts only when its own file
54
+ changed, when state or systemd says the loaded copy is stale, or when a digest the
55
+ declaration listed in `restartOn` changed. An adopted unit that already matches is not
56
+ restarted, reloaded or started.
57
+
58
+ `systemctl` and `stat` shapes measured read-only on Debian 13 / systemd 257, 2026-09-22;
59
+ the write subcommands are reasoned and read back rather than assumed. Unit files render
60
+ verbatim — there is no machine-readable directive schema to generate from, so the kit
61
+ invents none. Guide: `docs/linux-host.md`.
62
+
63
+ ## 0.18.0
64
+
65
+ ### Minor Changes
66
+
67
+ - [#118](https://github.com/taslabs-net/homeflare-kit/pull/118) [`5ae4e91`](https://github.com/taslabs-net/homeflare-kit/commit/5ae4e91919a101da3d8605a069c65e0301d0cccc) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Every Proxmox family that writes to the vendor is now checked against the vendor's own schema —
68
+ 35 of 35, up from 19, covering 75 endpoints instead of 37. A family left unwired was a write this
69
+ package made with nothing between the declaration and the server's 400, which is the shape of the
70
+ 2026-09-22 `deploy:pbs` incident this feature exists for.
71
+
72
+ Newly wired: `Proxmox.ApiToken`, `CephDaemon`, `CephFlag`, `CephFs`, `CephOsd`, `CephPool`, `Lxc`,
73
+ `MetricServer`, `NetworkApply`, `NodeNetwork`, `NotificationTarget`, `SdnApply`, `SdnSubnet`, `Vm`,
74
+ `ZfsPool`, and `Pbs.NotificationTarget` — which was missing from the sweep list and carries the
75
+ incident's own rule, `comment: maxLength 128`, on all three of its creates. Families that write
76
+ their own handlers (`CephOsd`, `Lxc`, the two applies, `Pbs.NotificationTarget`) reach the same
77
+ check by name through `guardForm`, as `Pbs.Datastore` already did; families with a spec declare
78
+ `endpoint`. `tests/constraint-wiring.test.ts` derives the census from the ownership ledger, so a
79
+ family added without an endpoint fails there rather than on a deploy, and each newly wired family
80
+ has a proof test that runs its REAL create form through the vendor's create table and requires no
81
+ violations.
82
+
83
+ 🔴 **A live bug this found.** `Proxmox.NodeNetwork`'s create form never sent `iface`, which PVE
84
+ marks required on `POST /nodes/{node}/network` while `{node}` is its only path parameter. Every
85
+ interface create this package could have made would have 400ed; nothing caught it because the
86
+ estate's interfaces were all adopted, which takes the PUT path. `createBody` now sends it, and the
87
+ PUT still does not — there `iface` is the path.
88
+
89
+ ⛔ **Presence of a vendor-required parameter is now demanded only when a create is really about to
90
+ happen**, not whenever the create form is built. `Proxmox.NotificationTarget` cannot send gotify's
91
+ `token` or smtp's `password` — they are write-only secrets and props are persisted unencrypted — so
92
+ the documented workflow is to create the target out of band and then declare it. Under the old
93
+ unconditional check that adopt-then-update would have been refused forever; now it plans clean,
94
+ while asking to CREATE a gotify target fails at plan with PVE's own `token: required`. The guards
95
+ move into `resource-guard.ts` and are exported from `pveOperations`, so `CephPool`'s hand-written
96
+ reconcile gets them too.
97
+
98
+ An **action** endpoint with no form is wired as well (`PUT /cluster/sdn`, `PUT /nodes/{node}/network`):
99
+ the table is empty, but the key is resolved against the vendor schema at generation time, so a PVE
100
+ that moves or withdraws an apply fails `bun run check` instead of an `ifreload -a` on three nodes.
101
+
102
+ `PveSpec['endpoint']` now also admits a function of props, for the two families whose endpoint is
103
+ chosen by a prop — `NotificationTarget`'s four PVE types and `CephDaemon`'s mds/mgr/mon, each with
104
+ its own parameter schema. Every key it can return is still a literal in this package's source,
105
+ because the generator finds endpoints by scanning text.
106
+
107
+ Generator changes that came with the volume: `/cluster` and `/nodes/{node}` are split one level
108
+ further down, because they are routes rather than areas — PVE's own viewer expands them — so the
109
+ tables are now 18 files (`pve-cluster-sdn.ts`, `pve-nodes-ceph.ts`, …), all inside the 250-line
110
+ house cap. The generator deletes a file it no longer produces, `tests/schema-manifest.test.ts`
111
+ enumerates the directory instead of a hand-written list and checks every table is claimed by the
112
+ manifest entry it came from, and two PVE bounds published as JSON strings (`bwlimit`'s
113
+ `minimum: "0"`, `count`'s `maximum: "16777216"`) are parsed to numbers — a faithful reading of a
114
+ stated value; a bound that is not a number at all is still dropped rather than guessed at.
115
+
116
+ ## 0.17.0
117
+
118
+ ### Minor Changes
119
+
120
+ - [#114](https://github.com/taslabs-net/homeflare-kit/pull/114) [`3316006`](https://github.com/taslabs-net/homeflare-kit/commit/3316006513196622551d4cc041986089dc28ffc4) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The Proxmox API type generator, written from the vendor schemas, and the widening it removes.
121
+ `packages/alchemy/src/proxmox/generated/{pve,pbs}.ts` carried the header
122
+ `Run: bun codegen/generate.ts` from the day they were committed, and
123
+ `git log --oneline --all -- 'codegen/generate*'` is empty at every commit: that file existed
124
+ nowhere. The mapping was therefore readable only as its own 8,196 lines of output — nobody could
125
+ reproduce it, correct it, or say which schema version it described. Because nobody could read it,
126
+ nobody noticed what it did: it kept `type`, `enum` and `optional`, dropped every `maxLength`,
127
+ `minLength`, `minimum`, `maximum`, `pattern`, `format`, `typetext`, `default` and description, and
128
+ covered 407 of PVE's 678 endpoints and 46 of PBS's 367 with no record of which 407 or why.
129
+
130
+ `bun codegen/types.ts` is that generator, with `--check`, the same manifest and the same
131
+ sha256-as-identity rule as `codegen/constraints.ts`. It emits every endpoint both products
132
+ document — 678 PVE and 367 PBS, 1,619 exported types — split across 89 files by the vendor's own
133
+ path and packed back up so the split is no deeper than the 250-line house cap requires. Every file
134
+ names its manifest entry, the product version the host reported and the sha256 of the bytes it was
135
+ read from. `generated/pve.ts` and `generated/pbs.ts` stay as `export *` barrels, so no import in
136
+ this package or any consumer moves.
137
+
138
+ ⛔ **An integer request parameter is `` `${number}` ``, not `string`.** `pbs:POST /config/verify`'s
139
+ `max-depth` is `integer, minimum 0, maximum 7` in PBS's schema and was `'max-depth'?: string` in
140
+ the type, which accepts `'banana'`; 480 PVE and 151 PBS parameters were widened that way. They are
141
+ now the wire spelling of a number: still assignable to `PveForm`, still carried unchanged through
142
+ `violations`' bound check, and no longer satisfied by an arbitrary string. It is deliberately NOT
143
+ `number`: `client.ts` sends `application/x-www-form-urlencoded` and types the body
144
+ `Record<string, readonly string[] | string>`, so a `number` could not be handed to `pve()` at all,
145
+ and `constraints.ts` iterates a form value on `typeof value === 'string'`. ⚠️ `String(n)` does not
146
+ typecheck against it — write `` `${n}` ``. A boolean parameter stays `'0' | '1'`, which is the
147
+ encoding `values.ts`'s `flag()` already produces rather than a widening. Responses are JSON and
148
+ keep their real `number` and `boolean | 0 | 1`.
149
+
150
+ The old output is reproduced before it is changed, which is what makes the diff reviewable: run
151
+ against the same two schemas with integers left widened, the pipeline re-emits all 646 PVE and 69
152
+ PBS declarations identically, with two recorded exceptions — `NodesNodeLxcVmidConfigGetReturn`'s
153
+ `lxc` becomes `readonly (readonly string[])[]` rather than a readonly array of mutable ones, and 21
154
+ declarations break lines differently because `oxfmt` had reformatted the committed files before
155
+ every `generated` directory reached its ignore list. The naming is the old generator's, reproduced
156
+ rather than improved: `ClusterBackupIdIncluded_volumesGetReturn` keeps its underscore, because
157
+ renaming sixty exported types in the commit that changes what the types mean would hide the second
158
+ change inside the first.
159
+
160
+ ⛔ Parameter schemas wrapped in `allOf`/`oneOf` are read through `codegen/parameters.ts` (PR [#113](https://github.com/taslabs-net/homeflare-kit/issues/113)),
161
+ not asked for as `parameters.properties`. `POST /cluster/ha/rules` and `PUT /cluster/ha/rules/{rule}`
162
+ are the two PVE endpoints that need it; a reader that misses them emits a type with no fields, which
163
+ is indistinguishable from an endpoint that takes nothing. An unresolvable schema gets a doc comment
164
+ naming the construct and **no** `Params` type — neither product needs that on these versions.
165
+
166
+ ⛔ "Closed object" is spelled differently by the two products, and a test for one lies about the
167
+ other. Measured over both whole schemas: PVE writes numbers (`additionalProperties: 0` on 617
168
+ objects, `1` on 21, absent on 352), PBS writes booleans (`false` on 560, `true` on 36). An absent
169
+ `additionalProperties` is open — the vendor never promised the list was exhaustive.
170
+
171
+ ⛔ Eight PVE files are over the house cap and cannot be split. Each holds the endpoints of one
172
+ vendor path whose parameters carry enums of hundreds of members — `rootfs`, `mp0`…`mp255`,
173
+ `unused0`…`unused255` for the volume moves, the ACME DNS provider list, the QEMU CPU model list. A
174
+ single type declaration is the smallest unit there is; dropping the enum would widen the parameter
175
+ back to `string`. Each says so in its own header and `tests/schema-types.test.ts` pins the list.
176
+
177
+ `tests/schema-type-mapping.test.ts` holds the mapping to shapes lifted from the committed files and
178
+ needs no schema cache, so it runs on CI. `tests/schema-types.test.ts` checks provenance, the
179
+ barrel against the files on disk, the cap, the coverage counts and five endpoints the old generator
180
+ omitted, and runs `bun codegen/types.ts --check` when the cache is present — skipping with the
181
+ refresh command when it is not. `codegen/TYPES.md` carries the reasoning.
182
+
183
+ ## 0.16.1
@@ -0,0 +1,180 @@
1
+ # Alchemy changelog archive 13
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ ### Patch Changes
6
+
7
+ - [#113](https://github.com/taslabs-net/homeflare-kit/pull/113) [`4fe8cef`](https://github.com/taslabs-net/homeflare-kit/commit/4fe8cef3154ad0f373b291f413dcbd570602e6fa) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `Proxmox.HaRule`'s constraint table was empty and nothing said so.
8
+
9
+ MEASURED 2026-09-22: PVE spells `POST /cluster/ha/rules` as `parameters: {allOf: [{properties:
10
+ {rule}}, {oneOf: [node-affinity, resource-affinity]}]}` — a discriminated union. The apidoc reader
11
+ asked for `parameters.properties`, got `undefined`, and emitted `{}`. A wired family's plan-time
12
+ guard therefore checked **nothing**, and an empty table is indistinguishable from an endpoint whose
13
+ parameters happen to carry no rules. `comment` there has a `maxLength` of 4096 and `affinity` an
14
+ enum of two.
15
+
16
+ `codegen/parameters.ts` reads both combinators, and their logic is their meaning. `allOf` branches
17
+ all apply, so their properties MERGE — a key claimed by two branches would have to satisfy both,
18
+ which this does not compute, so it stops rather than picking one. `oneOf` branches are
19
+ ALTERNATIVES, so they INTERSECT: only what every branch states identically survives, because
20
+ enforcing a rule from one branch would refuse a legal declaration of the other kind. `nodes` and
21
+ `strict` exist only on node-affinity and are therefore not enforced. ⚠️ `optional` is intersected
22
+ toward optional rather than field-by-field: its ABSENCE means required, so dropping a disagreeing
23
+ `optional` would have read as required and refused every legal node-affinity rule, whose `affinity`
24
+ is optional where resource-affinity's is not.
25
+
26
+ ⛔ And a parameter schema this file cannot read is now recorded as `unresolved` and **stops the
27
+ generator** for any endpoint this package writes to, rather than producing the empty table that hid
28
+ the problem. `tests/schema-manifest.test.ts` covers the reader directly.
29
+
30
+ ⛔ `docs/api-coverage.*` had the identical blind spot from its own parser: it reported
31
+ `/cluster/ha/rules` as having **zero** parameters and zero gaps. `scripts/api-schema.ts` now reads
32
+ the combinators through the same resolver — 5 parameters, 2 unenforced, both `format` names.
33
+ ⚠️ Two parsers for one file format is the deeper defect; merging them is its own change.
34
+
35
+ - [#116](https://github.com/taslabs-net/homeflare-kit/pull/116) [`54479fc`](https://github.com/taslabs-net/homeflare-kit/commit/54479fc7792076fb0e718b55588cc815823818c2) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `Netbox.Prefix` no longer erases prose it did not declare.
36
+
37
+ 🔴 **The bug, found by review rather than by an incident.** The resource sent `description: ''`
38
+ whenever the prop was absent. On a create that is invisible — the field was empty anyway. ⛔ On an
39
+ **adopt** it is data loss: NetBox is the estate's record of DECISIONS, so a prefix's description is
40
+ usually the only written trace of why that range exists. The first deploy that adopted one would
41
+ have PATCHed it to empty, `matches` would have reported drift, the plan would have said `update`,
42
+ and the diff would have read as converging a declaration rather than deleting a sentence.
43
+
44
+ ★ **The tell was an inconsistency inside the same file, not a failure.** Optional foreign keys were
45
+ already omitted when undeclared, with a comment explaining that sending `null` would clear a tenant
46
+ somebody set in the UI. Free text had the identical hazard and the opposite treatment. Two fields,
47
+ one hazard, two answers — that gap is the defect.
48
+
49
+ ★ **The line is now drawn at what the vendor itself defaults.** `status`, `is_pool` and
50
+ `mark_utilized` have defaults in NetBox's schema, so omitting one genuinely means "the default" and
51
+ settling it says what NetBox would have done anyway. `description`, `comments` and the optional
52
+ foreign keys have no such default — the schema's `''` is the absence of a value, not a decision —
53
+ so they are omitted from the body and left uncompared until declared.
54
+
55
+ ⛔ **Whatever `matches` compares, `body` must send**, or the plan says `update` forever: the PATCH
56
+ omits the field, so the next read is unchanged. The two moved together here and
57
+ `prefix-form.test.ts` asserts the invariant.
58
+
59
+ ⚠️ **The cost, stated:** prose can no longer be cleared by omission. Clearing it is
60
+ `description: ''`, written on purpose — the readable way to say a destructive thing.
61
+
62
+ `body` and `matches` are extracted to `prefix-form.ts` so both are pure functions a test can call
63
+ with a literal, the way the Proxmox families keep their `*-form.ts` beside the resource.
64
+
65
+ ## 0.16.0
66
+
67
+ ### Minor Changes
68
+
69
+ - [#110](https://github.com/taslabs-net/homeflare-kit/pull/110) [`9fa5800`](https://github.com/taslabs-net/homeflare-kit/commit/9fa5800c8a1d2d3fb831d29ae64e9145e82fe48f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - A NetBox provider, generated from NetBox's own OpenAPI document — `@homeflare/alchemy/netbox`.
70
+
71
+ `Netbox.Prefix` declares one IP prefix and the decision recorded against it. It is the first object
72
+ class for three measurable reasons: `WritablePrefixRequest.required` is exactly `["prefix"]`, so it
73
+ is the only interesting NetBox object with **no foreign-key prerequisite** (a VLAN needs `vid` and
74
+ a group; a Device needs a role, a type and a site — four more Resources before the first one can be
75
+ declared); a prefix is the atom of what NetBox is for, the record of what the network was DECIDED
76
+ to be; and `status: 'deprecated'` is how a retired range stops being folklore in an SSH config
77
+ comment and becomes a line with a reviewable diff.
78
+
79
+ ⛔ **The constraint tables are generated, never hand-typed.** `bun codegen/netbox.ts` reads NetBox
80
+ 4.7.0's OpenAPI 3.0.3 document, verifies its sha256 against `codegen/manifest.json`, and emits the
81
+ committed tables plus `docs/netbox-coverage.md`. A key naming an endpoint the vendor does not have
82
+ stops the generator. This is the same pipeline that exists because a PBS deploy adopted ten objects
83
+ and then failed its one create on a `maxLength: 128` the generated type did not carry — NetBox gets
84
+ it **before** its first write rather than after.
85
+
86
+ ⛔ **Two of NetBox's seven regexes are not JavaScript in meaning, and both compile cleanly.**
87
+ Measured over the whole document: `^[-\w]+$` (`slug`) and `^[\w.@+-]+$` (`username`). Python's `\w`
88
+ is Unicode on a `str`, so Django accepts `zürich-core` and a verbatim JavaScript copy refuses it —
89
+ a plan blaming the operator for a legal value, which is worse than the server-side 400 the table
90
+ replaces. ⚠️ The `u` flag does not fix it. Both are dropped, recorded as `patternSource` with no
91
+ `pattern`, and the generated header says nothing enforces them.
92
+
93
+ ⚠️ **The document was pinned to the vendor's release tag, not read from an instance, and the
94
+ manifest says why.** The reference instance could not answer `/api/schema/`. The published document
95
+ was then cross-checked against a snapshot the estate took from its own instance while it was up:
96
+ 1256 operations on each side, `(method, path)` sets identical with zero difference. ⛔ That verifies
97
+ the path surface only — the snapshot discards request bodies, which is the half this generates —
98
+ so every constraint rests on the vendor document alone.
99
+
100
+ Also here:
101
+
102
+ - Adopt-first by construction: `reconcile` locates before it writes, and ⛔ an ambiguous identity
103
+ **fails** rather than binding to whichever row NetBox ordered first.
104
+ - 🔴 **One guessed filter shape was caught before it shipped, and the fix is structural.** The
105
+ prefix locate first narrowed server-side with `vrf_id=null`, the sentinel NetBox uses for "no
106
+ foreign key" — `FILTERS_NULL_CHOICE_VALUE = 'null'` is real. ⛔ But in the vendor's own source
107
+ at v4.7.0, `PrefixFilterSet.vrf_id` is a plain `ModelMultipleChoiceFilter` with no `null_value`:
108
+ it never opted in, so `'null'` fails queryset validation and NetBox answers **400 on every plan
109
+ for every global-table prefix**. `locate` now sends only filters the document declares and a new
110
+ `identifies` picks the row in this process, where the rule is readable and testable offline.
111
+ - `retain` on removal for every family, because deleting a NetBox row reparents children and
112
+ detaches IP assignments; the `delete` handler is fully implemented anyway.
113
+ - Read/write shape asymmetry handled in `values.ts` — `status` is written as `"active"` and read
114
+ back as `{value, label}`; a foreign key is written as `4` and read back as `{id, url, display}`.
115
+ Comparing those directly reports drift on every plan, forever.
116
+ - ⛔ No credential is ever a prop. `NETBOX_URL` and `NETBOX_TOKEN` are read at call time, and
117
+ `Authorization: Token`, not `Bearer` — a wrong scheme and a wrong credential look identical in
118
+ the response.
119
+ - ★ **`codegen/param-rules.ts` gains a dialect table, and `emit.ts` is untouched.** There are
120
+ exactly two vendor facts about a pattern — which dialect it is written in, and whether the
121
+ vendor anchors it — so they live together per product rather than as a string every function
122
+ switches on. ⛔ NetBox gets the Django translator and no anchoring: its own 7 patterns already
123
+ carry `^…$` and Django validates with `re.search`, so re-anchoring would invent a rule. PVE is
124
+ the opposite case and keeps its measured anchoring. Every existing Proxmox table regenerates
125
+ byte-identical.
126
+
127
+ ### Patch Changes
128
+
129
+ - [#108](https://github.com/taslabs-net/homeflare-kit/pull/108) [`6586b0d`](https://github.com/taslabs-net/homeflare-kit/commit/6586b0decb5201e57f9e086619d1f658e5bce94d) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Adversarial review of the vendor-constraint guard, same day it shipped: two rule kinds were passing
130
+ through it unchecked, and the coverage report did not know the guard existed.
131
+
132
+ ⛔ **PVE anchors every pattern and the tables did not.** MEASURED read-only on a cluster node,
133
+ `/usr/share/perl5/PVE/JSONSchema.pm:1636`: `if ($value !~ m/^$pattern$/)`. The published pattern is
134
+ the INSIDE of an anchored match — PVE ships `[A-Za-z][A-Za-z0-9\-\_]+` for a firewall alias name —
135
+ and `RegExp.test` is a search, so `ok name!` matched on its `ok`, planned clean and was rejected by
136
+ PVE with the 400 the guard exists to prevent. All eleven PVE patterns in the tables were toothless
137
+ this way. The anchoring is textual rather than `(?:…)`, because Perl's is: three of PVE's 72
138
+ patterns carry a top-level `|`, and `^a|b$` is not `^(?:a|b)$`. `\n?` before the `$` is Perl's `$`,
139
+ which matches before a final newline where JavaScript's does not — without it the guard would refuse
140
+ values PVE accepts, which is worse than the 400. PBS is untouched: all 37 of its patterns already
141
+ carry their own `^…$` and Rust's `is_match` is a search.
142
+
143
+ ⛔ **An array states its rules on `items`, and the emitter read only the parameter.** 30 tabled
144
+ parameters are arrays and 11 state real limits one level down — PBS `target` (2–32 chars, a name
145
+ pattern), `associated-key`, the `delete` enums, PVE `secondary-controllers` (max 64). `violations`
146
+ was already checking every element of a repeated key against a row that had no rules in it. Those
147
+ rules now merge into the row, which says `each: true`; `required` is never taken from `items`.
148
+
149
+ ⛔ **`patternFlags` is emitted instead of discarded.** `translatePattern` lifts PBS's leading `(?m)`
150
+ to a flag and the first generation returned it and threw it away, so a multi-line rule would have
151
+ been enforced with single-line semantics. No tabled endpoint uses one today; this is the guard for
152
+ the day one does.
153
+
154
+ ⛔ **`docs/api-coverage.md` called the 128-character comment unenforced.** It was generated from the
155
+ vendor schema alone, hours after the tables started enforcing 321 rows of it, so the gap column
156
+ counted every rule the guard had just closed — including the one the report opens by describing.
157
+ `POST /config/verify` now reads `unenforced: []`, PBS's owned gap falls 144 → 62 and PVE's 545 →
158
+ 490, and a `format` is still never subtracted because the tables record the name and check nothing.
159
+
160
+ ⛔ **The two manifests named two different PVE schemas.** `codegen/manifest.json` said 9.2.11 and
161
+ `schemas/manifest.json` said 9.2.4 — both true of this genuinely mixed-version cluster, differing by
162
+ two write endpoints, and nothing said so. Both now name the same bytes and the same versioned cache
163
+ filename, and `tests/schema-manifest.test.ts` fails if they ever diverge again.
164
+
165
+ New tests: `constraints-dialect.test.ts` holds a mutant for each newly enforced kind, and
166
+ `constraints-live.test.ts` runs all ten objects of the live PBS inventory — the real ids, stores,
167
+ schedules and retention values, comment text replaced by same-length filler because this package is
168
+ public — through their create AND update tables expecting zero violations, which is the false
169
+ positive this feature could itself cause. `constraints.test.ts` is split at the 250-line house cap.
170
+
171
+ ## 0.15.0
172
+
173
+ ### Minor Changes
174
+
175
+ - [#106](https://github.com/taslabs-net/homeflare-kit/pull/106) [`fc026e7`](https://github.com/taslabs-net/homeflare-kit/commit/fc026e7b81d937f614515abe8b2a2d7bb2a2d019) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Forward every `Bao.*` family's `Props` and `Attributes` types from `@homeflare/alchemy/openbao`.
176
+
177
+ Each family already re-exported its own types from its module, but the barrel forwarded only the
178
+ VALUES for seven of them — `BaoAuthMethod`, `BaoCloudflareRole`, `BaoMount`, `BaoPkiRole`,
179
+ `BaoPolicy`, `BaoProxmoxRole` and `BaoSshRole` — plus `BaoAuthRoleAttributes`,
180
+ `BaoPluginAttributes`, `BaoJwtRoleAttributes` and `BaoJwtCallbackMode`.
@@ -0,0 +1,178 @@
1
+ # Alchemy changelog archive 14
2
+
3
+ [Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
4
+
5
+ ⛔ This is a bug only a consumer could see, and only one that obeys the rules. A stack may use
6
+ Resources the package entry exports and must not deep-import, so it could be handed
7
+ `BaoProxmoxRole` and still be unable to name its props — leaving it to write the shape out and
8
+ hope it stayed in step. MEASURED 2026-09-22 in homeflare-openbao, which did exactly that for
9
+ `BaoProxmoxRole` while declaring the VPS proxmox engine. For that family the cost is highest:
10
+ `mount` + `name` + `mintUser` + `ttl` + `maxTtl` IS the whole role, so a hand-written copy
11
+ duplicates the entire server-side state of a family whose `mintUser` is its security boundary.
12
+
13
+ Additive and type-only: no value, signature or runtime behaviour changes, and nothing that was
14
+ importable stops being importable. `src/openbao/index-types.test.ts` pins the surface with a
15
+ type-only test — `tsc --noEmit` is the assertion, so a family whose props stop being reachable
16
+ from the package entry fails here instead of in another repo's next consumer.
17
+
18
+ - [#107](https://github.com/taslabs-net/homeflare-kit/pull/107) [`734ef60`](https://github.com/taslabs-net/homeflare-kit/commit/734ef6067e504be20c65ce7ec9c221c54554d7e7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The provisioning baseline takes a comment per object, so it can describe a cluster that
19
+ already exists.
20
+
21
+ `provisionBaseline` and `provisionBootstrap` carried ONE `comment` for the mint group and
22
+ both mint users. That can only describe a cluster this baseline made. The common case is
23
+ the other one: a cluster that already has its mint group and its read user, each with its
24
+ own live comment, both of them already declared at those values by the stack that adopted
25
+ them. A single comment made the generated script modify all three, and the next deploy of
26
+ that stack wrote them back — a loop that reads like drift and is not. Measured on an
27
+ estate cluster on 2026-09-22, where the mint group had no comment at all and the read user
28
+ named its own mount.
29
+
30
+ `ProvisionNames` now adds `groupComment`, `provisionComment` and `readComment`, each
31
+ defaulting to `comment`, so the generic case is still one string and an override changes
32
+ exactly one object:
33
+
34
+ ```ts
35
+ provisionBootstrap({
36
+ role: 'LXCProvisioner',
37
+ groupComment: '', // live: no comment at all
38
+ readComment: 'mint target: read (ops)', // live: its own wording
39
+ provisionComment: 'mint target: provision (ops)', // the one new object
40
+ });
41
+ ```
42
+
43
+ The script then prints `group hf-mint: ok` and `user hf-read@pve: ok` and its only writes
44
+ are the role, the new user and its grant. A test runs exactly that against the CLI fake,
45
+ with the one-shared-comment run beside it as a negative control.
46
+
47
+ Also:
48
+
49
+ - Each comment is checked like `comment` was, and a problem is **named by where the value
50
+ came from** — a bad shared `comment` is still one problem called `comment`, not three
51
+ called after overrides the caller never passed.
52
+ - `readComment` goes with its lane: a `null` `readUser` drops the user, so the field is
53
+ neither used nor checked.
54
+ - `:` joins the characters a comment may hold. It is special in neither `sh` nor a Perl
55
+ `q{}`, and it is how real mint-user comments are written (`mint target: read`).
56
+ - `CoreProvisionNames` is the six names `PROVISION_DEFAULTS` resolves, split out so that
57
+ type keeps its exact shape; `ProvisionNames` extends it. `ProvisionComments` is the
58
+ resolved comment per object. Both are exported.
59
+
60
+ - [#102](https://github.com/taslabs-net/homeflare-kit/pull/102) [`4fb005e`](https://github.com/taslabs-net/homeflare-kit/commit/4fb005e4892f34bbcef01c5227d464ae36e80299) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Vendor schema constraints, generated and enforced at plan time. `homeflare-proxmox`'s first
61
+ `deploy:pbs` adopted ten objects and then failed its one create on `PVE POST config/verify -> 400:
62
+ parameter verification failed - comment: value may only be 128 characters long`. Nothing local
63
+ caught it, because the only place the number 128 existed was PBS's published schema: the generated
64
+ types keep `type`, `enum` and `optional` and drop every `maxLength`, `minLength`, `minimum`,
65
+ `maximum` and `pattern` the vendor states.
66
+
67
+ A new `codegen/` reads the cluster's own `apidoc.js` and emits machine-readable constraint tables
68
+ (`packages/alchemy/src/proxmox/generated/constraints/`, one file per vendor area, all inside the
69
+ 250-line house cap). `constraints.ts` is a pure validator over a form and its endpoint's table, and
70
+ `resource.ts`'s shared `pveHandlers` runs it on both the create form and the update form — so every
71
+ family that declares an `endpoint` gets it, with no per-resource copy. `Pbs.Datastore`, which writes
72
+ its own handlers, gets the same check through `pbs-datastore-endpoint.ts`. Nineteen families are
73
+ wired, covering 37 endpoints: PBS datastore/prune/sync/verify/matchers, PVE acl, groups, roles,
74
+ users, backup, firewall aliases, HA resources and rules, matchers, replication, SDN vnets and zones,
75
+ pools and storage. Presence of a vendor-required parameter is checked on CREATE only — an update
76
+ form is partial by design.
77
+
78
+ Provenance is committed with it. `codegen/manifest.json` records each schema's vendor, product,
79
+ version as the host reports it (pve-manager 9.2.11, proxmox-backup-server 4.2.6-1), source host
80
+ ROLE and absolute path, sha256, byte size and fetch time; every generated header names its manifest
81
+ entry, version and sha256 prefix. The raw 5.8 MB blobs stay out of git in a documented cache
82
+ directory, and the generator refuses to run when a cached file's sha256 does not match.
83
+ `tests/schema-manifest.test.ts` recomputes the tables' digest on every run and, when the cache is
84
+ present, runs `bun codegen/constraints.ts --check` so a stale generation fails with the exact
85
+ refresh command. The two UniFi OpenAPI documents (Network 10.4.57, Site Manager 1.0.0) are recorded
86
+ as available and consumed by nothing — there is no UniFi provider family yet.
87
+
88
+ ⛔ Patterns are translated through a whitelist, not copied. Measured over both whole schemas: PBS
89
+ prints its Rust regex through `Display`, so every PBS pattern arrives wrapped in slashes, and it
90
+ uses POSIX classes — `new RegExp` accepts `/^[[:^cntrl:]]*$/` and `[[:^cntrl:]]` SILENTLY and means
91
+ something else in both cases, which would have refused every legal comment. PVE's `(?^:…)` throws.
92
+ Anything the whitelist cannot carry over faithfully is recorded verbatim as `patternSource` and left
93
+ unenforced, including PVE's 216 server-side `format` validators and PBS `schedule`, which publishes
94
+ no pattern at all.
95
+
96
+ `resource.ts` is split: the `PveSpec` shape and its argument move to `resource-spec.ts` (re-exported,
97
+ so no importer changes) to keep both files inside the house cap.
98
+
99
+ ### Patch Changes
100
+
101
+ - [#103](https://github.com/taslabs-net/homeflare-kit/pull/103) [`173b736`](https://github.com/taslabs-net/homeflare-kit/commit/173b7365742921bfde6f3a3114fca22ff7978c46) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `Proxmox.Lxc`'s guide now says what a container's **inside** is, and pins it with a test.
102
+
103
+ `Proxmox.Lxc` declares the keys in `/nodes/{node}/lxc/{vmid}/config` and nothing within the guest's
104
+ filesystem. Every consumer that meets that limit goes looking for the resource that must surely
105
+ exist — an exec, a file write, a cloud-init. **For containers it does not exist in PVE's API at
106
+ all.** QEMU VMs have `POST …/qemu/{vmid}/agent/exec`, `…/agent/file-write`, `…/agent/file-read` and
107
+ a `cloudinit` subtree; the complete `/nodes/{node}/lxc/{vmid}/…` endpoint set has no counterpart.
108
+ The only reach inside is `termproxy` / `vncwebsocket`, an interactive console for a person.
109
+
110
+ - `docs/proxmox-lxc.md` gains that table under **Gaps**, and says plainly that a generic,
111
+ vendor-API-based Resource for a container's interior cannot be written: there is nothing to
112
+ wrap. What is left, in the order that keeps a change declarative — bake it into the template
113
+ (⚠️ `ostemplate` is create-only, so changing it REPLACES the guest), a first-boot artifact, or a
114
+ recorded one-time human step named as undeclared.
115
+ - ⛔ It also says why an exec-over-SSH resource is not option zero: it needs a credential and a
116
+ network path _to the guest_, so when the guest is what provides credentials, naming or reach to
117
+ others, it inverts the bootstrap — the new system's first boot depends on its own output. The
118
+ `HostRunner` seam in `@homeflare/alchemy/launchd` is where such a runner plugs in, and the kit
119
+ ships only `localRunner()` and `sudoRunner()` on purpose.
120
+ - ⚠️ **Sibling families are not at parity.** QEMU and LXC sit under the same `/nodes/{node}/…` tree
121
+ with completely different reach; the guide now says not to infer one from the other.
122
+ - `src/proxmox/lxc-interior.test.ts` checks this against the generated schema on every run, so it
123
+ fails the day PVE adds such an endpoint — which is exactly when the kit would want to wrap it.
124
+ Its positive control asserts QEMU's three are present, so a change to the generated file's shape
125
+ fails the test instead of making every absence assertion pass for free.
126
+
127
+ Docs and a test only. No resource, type or behaviour changed.
128
+
129
+ - [#105](https://github.com/taslabs-net/homeflare-kit/pull/105) [`b1e4af3`](https://github.com/taslabs-net/homeflare-kit/commit/b1e4af38b2bf71986bb8b85974aa432e3b30ec99) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Generated Proxmox API coverage report, with provenance.
130
+
131
+ `docs/api-coverage.md` and `docs/api-coverage.json` map every PVE and PBS endpoint that can
132
+ create, update or delete state to the Resource that owns it, or to nothing — derived from the
133
+ vendor schemas named in `schemas/manifest.json` (product version, source host role, sha256, size,
134
+ fetch time), not hand-counted. PVE `pve-manager/9.2.4/5e5ae681198514d4`: 88 of 335 write
135
+ endpoints owned. PBS `proxmox-backup-server 4.2.6-1`: 27 of 182.
136
+
137
+ Each row also names the parameters carrying a vendor `maxLength`, `minLength`, `minimum`,
138
+ `maximum`, `pattern` or `format` that nothing local enforces — 545 on the PVE endpoints we own and
139
+ 144 on the PBS ones, including the `comment` on `POST /config/verify` whose 128-character limit
140
+ failed a `Pbs.VerifyJob` create at apply time.
141
+
142
+ Regenerate with `bun run api:coverage`; `--check` exits 1 when the committed report is stale.
143
+ `tests/api-coverage.test.ts` fails if a Resource claims an endpoint the schema no longer has, if a
144
+ path the source calls unreachable turns out to exist, or if the report has been hand-edited. No
145
+ package code changed.
146
+
147
+ ## 0.14.0
148
+
149
+ ### Minor Changes
150
+
151
+ - [#98](https://github.com/taslabs-net/homeflare-kit/pull/98) [`89ce4fd`](https://github.com/taslabs-net/homeflare-kit/commit/89ce4fdf606d6cc6e635164f0413b4ca6187379f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - **New subpath: `@homeflare/alchemy/github` — one repository's merge policy in one call.**
152
+
153
+ - `declareRepoPolicy(id, options)` declares a `GitHub.Repository` and a `GitHub.Ruleset` over its
154
+ default branch: squash-only merges, auto-merge, head branches deleted on merge, no branch
155
+ deletion, no force pushes, and the status-check contexts you name required with
156
+ `strict_required_status_checks_policy` off. Both resources retain; `adopt` is piped only when
157
+ asked. Generic and parameterized — `rulesetName`, `include`/`exclude`, `bypassActors`,
158
+ `enforcement`, `baseUrl` (applied to both resources or to neither), and a `settings` bag for
159
+ everything that is not merge policy, merged underneath so it cannot re-open a merge method.
160
+ - `repoPolicy(options)` is the same policy as two plain prop objects, pure and type-only, for a
161
+ test or a stack that wants to declare the resources itself.
162
+
163
+ What it refuses, because each of these failures is silent:
164
+
165
+ - ⛔ **Auto-merge with nothing to wait for merges the pull request immediately.** Auto-merge is
166
+ a queue only while something is outstanding, and three inputs produce "nothing
167
+ outstanding": no `checks` and no `requiredApprovals`; an `enforcement` that is not `active`
168
+ (the rules are listed and none of them block); and an explicitly empty `include` (the ruleset
169
+ matches no ref while GitHub still shows it as active). A required review counts as outstanding,
170
+ so `requiredApprovals` with an empty `checks` is allowed. `checks: []` alone is accepted only
171
+ alongside `autoMerge: false` — the honest description of a repo with no green run to require yet.
172
+ - ⛔ **A blank check context** is refused: GitHub stores it and no job ever reports it, so every
173
+ pull request waits on a check that cannot come.
174
+ - ⛔ **`requiredApprovals: 0` is refused rather than treated as "no reviews".** Zero approvals is
175
+ the solo-maintainer shape and needs `require_extra_approval_for_unattributed_changes: false`,
176
+ which `alchemy@2.0.0-beta.79`'s `Ruleset` cannot send and GitHub defaults to `true`. Omitting
177
+ `requiredApprovals` declares no `pull_request` rule at all, which is a different and honest
178
+ thing.