@homeflare/alchemy 0.27.4 → 0.29.0

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 (217) hide show
  1. package/README.md +2 -0
  2. package/dist/caddy/admin-address.d.ts +27 -0
  3. package/dist/caddy/admin-address.d.ts.map +1 -0
  4. package/dist/caddy/admin-calls.d.ts +25 -28
  5. package/dist/caddy/admin-calls.d.ts.map +1 -1
  6. package/dist/caddy/admin.d.ts +28 -38
  7. package/dist/caddy/admin.d.ts.map +1 -1
  8. package/dist/caddy/caddy-http-client.d.ts +25 -0
  9. package/dist/caddy/caddy-http-client.d.ts.map +1 -0
  10. package/dist/caddy/config-errors.d.ts +24 -0
  11. package/dist/caddy/config-errors.d.ts.map +1 -0
  12. package/dist/caddy/config-lifecycle.d.ts +20 -49
  13. package/dist/caddy/config-lifecycle.d.ts.map +1 -1
  14. package/dist/caddy/config-reconcile.d.ts +45 -0
  15. package/dist/caddy/config-reconcile.d.ts.map +1 -0
  16. package/dist/caddy/config.d.ts +4 -3
  17. package/dist/caddy/config.d.ts.map +1 -1
  18. package/dist/caddy/fake-caddy.d.ts +10 -2
  19. package/dist/caddy/fake-caddy.d.ts.map +1 -1
  20. package/dist/caddy/index.d.ts +3 -3
  21. package/dist/caddy/index.d.ts.map +1 -1
  22. package/dist/caddy/index.js +487 -444
  23. package/dist/caddy/index.js.map +16 -12
  24. package/dist/caddy/local-admin.d.ts +20 -34
  25. package/dist/caddy/local-admin.d.ts.map +1 -1
  26. package/dist/caddy/providers.d.ts +19 -3
  27. package/dist/caddy/providers.d.ts.map +1 -1
  28. package/dist/cloudflare/mesh-node-token.d.ts +1 -1
  29. package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
  30. package/dist/forgejo/branch-protection.d.ts +4 -4
  31. package/dist/forgejo/org-actions-secrets.d.ts +4 -4
  32. package/dist/forgejo/org-label.d.ts +4 -4
  33. package/dist/forgejo/org-team.d.ts +4 -4
  34. package/dist/forgejo/repo-webhook.d.ts +4 -4
  35. package/dist/forgejo/repository.d.ts +4 -4
  36. package/dist/forgejo/team-member.d.ts +4 -4
  37. package/dist/github/declare-repo-baseline.d.ts +1 -1
  38. package/dist/github/declare-repo-baseline.d.ts.map +1 -1
  39. package/dist/github/index.d.ts +9 -5
  40. package/dist/github/index.d.ts.map +1 -1
  41. package/dist/github/index.js +161 -25
  42. package/dist/github/index.js.map +15 -13
  43. package/dist/github/repo-baseline-data.d.ts +10 -1
  44. package/dist/github/repo-baseline-data.d.ts.map +1 -1
  45. package/dist/github/repo-policy.d.ts +1 -1
  46. package/dist/github/repo-policy.d.ts.map +1 -1
  47. package/dist/github/repository-ruleset-errors.d.ts +37 -3
  48. package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
  49. package/dist/github/repository-ruleset-form.d.ts.map +1 -1
  50. package/dist/github/repository-ruleset-guards.d.ts +15 -2
  51. package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
  52. package/dist/github/repository-ruleset-narrowing-guards.d.ts +39 -0
  53. package/dist/github/repository-ruleset-narrowing-guards.d.ts.map +1 -0
  54. package/dist/github/repository-ruleset-octokit.d.ts +49 -0
  55. package/dist/github/repository-ruleset-octokit.d.ts.map +1 -1
  56. package/dist/github/repository-ruleset-probe.d.ts +2 -1
  57. package/dist/github/repository-ruleset-probe.d.ts.map +1 -1
  58. package/dist/github/repository-ruleset-props.d.ts +91 -0
  59. package/dist/github/repository-ruleset-props.d.ts.map +1 -0
  60. package/dist/github/repository-ruleset-providers.d.ts +42 -0
  61. package/dist/github/repository-ruleset-providers.d.ts.map +1 -0
  62. package/dist/github/repository-ruleset-reconcile.d.ts.map +1 -1
  63. package/dist/github/repository-ruleset.d.ts +28 -34
  64. package/dist/github/repository-ruleset.d.ts.map +1 -1
  65. package/dist/litellm/fake-litellm.d.ts +27 -16
  66. package/dist/litellm/fake-litellm.d.ts.map +1 -1
  67. package/dist/litellm/fake-stack.d.ts +9 -4
  68. package/dist/litellm/fake-stack.d.ts.map +1 -1
  69. package/dist/litellm/index.d.ts +7 -5
  70. package/dist/litellm/index.d.ts.map +1 -1
  71. package/dist/litellm/index.js +53 -123
  72. package/dist/litellm/index.js.map +8 -8
  73. package/dist/litellm/operations.d.ts +60 -0
  74. package/dist/litellm/operations.d.ts.map +1 -0
  75. package/dist/litellm/pass-through-endpoint.d.ts +10 -28
  76. package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
  77. package/dist/litellm/pass-through-errors.d.ts +37 -0
  78. package/dist/litellm/pass-through-errors.d.ts.map +1 -0
  79. package/dist/litellm/pass-through-form.d.ts +11 -4
  80. package/dist/litellm/pass-through-form.d.ts.map +1 -1
  81. package/dist/litellm/providers.d.ts +7 -5
  82. package/dist/litellm/providers.d.ts.map +1 -1
  83. package/dist/netbox/prefix.d.ts +4 -4
  84. package/dist/paperless/custom-field.d.ts +21 -21
  85. package/dist/paperless/custom-field.d.ts.map +1 -1
  86. package/dist/paperless/document-type.d.ts +34 -4
  87. package/dist/paperless/document-type.d.ts.map +1 -1
  88. package/dist/paperless/fake-paperless.d.ts +36 -19
  89. package/dist/paperless/fake-paperless.d.ts.map +1 -1
  90. package/dist/paperless/index.d.ts +7 -2
  91. package/dist/paperless/index.d.ts.map +1 -1
  92. package/dist/paperless/index.js +164 -228
  93. package/dist/paperless/index.js.map +10 -13
  94. package/dist/paperless/matching-locate.d.ts +42 -20
  95. package/dist/paperless/matching-locate.d.ts.map +1 -1
  96. package/dist/paperless/matching-types.d.ts +47 -18
  97. package/dist/paperless/matching-types.d.ts.map +1 -1
  98. package/dist/paperless/matching.d.ts +30 -27
  99. package/dist/paperless/matching.d.ts.map +1 -1
  100. package/dist/paperless/providers.d.ts +13 -7
  101. package/dist/paperless/providers.d.ts.map +1 -1
  102. package/dist/paperless/storage-path.d.ts +34 -4
  103. package/dist/paperless/storage-path.d.ts.map +1 -1
  104. package/dist/paperless/tag.d.ts +43 -8
  105. package/dist/paperless/tag.d.ts.map +1 -1
  106. package/dist/proxmox/acl.d.ts +30 -23
  107. package/dist/proxmox/acl.d.ts.map +1 -1
  108. package/dist/proxmox/distilled-pve.d.ts +89 -0
  109. package/dist/proxmox/distilled-pve.d.ts.map +1 -0
  110. package/dist/proxmox/index.js +663 -609
  111. package/dist/proxmox/index.js.map +12 -11
  112. package/dist/proxmox/members.d.ts +29 -10
  113. package/dist/proxmox/members.d.ts.map +1 -1
  114. package/docs/caddy-admin.md +1 -1
  115. package/docs/caddy.md +3 -2
  116. package/docs/credentials.md +16 -0
  117. package/docs/litellm.md +21 -13
  118. package/docs/paperless.md +12 -0
  119. package/docs/peers.md +5 -0
  120. package/docs/repo-policy.md +16 -1
  121. package/docs/repository-ruleset.md +53 -13
  122. package/docs/upstream-conformance.md +126 -10
  123. package/package.json +9 -2
  124. package/src/caddy/admin-address.ts +71 -0
  125. package/src/caddy/admin-calls.ts +48 -100
  126. package/src/caddy/admin.ts +37 -41
  127. package/src/caddy/adopt-scope.test.ts +1 -1
  128. package/src/caddy/caddy-http-client.ts +229 -0
  129. package/src/caddy/config-adopt-provider.test.ts +139 -0
  130. package/src/caddy/config-adopt.test.ts +60 -126
  131. package/src/caddy/config-errors.ts +47 -0
  132. package/src/caddy/config-lifecycle.test.ts +55 -42
  133. package/src/caddy/config-lifecycle.ts +72 -162
  134. package/src/caddy/config-reconcile.ts +138 -0
  135. package/src/caddy/config.ts +36 -42
  136. package/src/caddy/fake-caddy.ts +20 -2
  137. package/src/caddy/index.ts +3 -8
  138. package/src/caddy/local-admin.test.ts +35 -21
  139. package/src/caddy/local-admin.ts +60 -167
  140. package/src/caddy/providers.test.ts +14 -6
  141. package/src/caddy/providers.ts +20 -4
  142. package/src/github/declare-repo-baseline.ts +6 -6
  143. package/src/github/index.ts +10 -4
  144. package/src/github/repo-baseline-data.test.ts +13 -0
  145. package/src/github/repo-baseline-data.ts +11 -2
  146. package/src/github/repo-policy-stack.test.ts +59 -0
  147. package/src/github/repo-policy.ts +3 -2
  148. package/src/github/repository-ruleset-errors.ts +51 -4
  149. package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
  150. package/src/github/repository-ruleset-exact-adopt.test.ts +184 -0
  151. package/src/github/repository-ruleset-form.ts +7 -3
  152. package/src/github/repository-ruleset-guards.ts +11 -4
  153. package/src/github/repository-ruleset-narrowing-guards.ts +141 -0
  154. package/src/github/repository-ruleset-narrowing.test.ts +210 -0
  155. package/src/github/repository-ruleset-octokit.test.ts +141 -0
  156. package/src/github/repository-ruleset-octokit.ts +121 -30
  157. package/src/github/repository-ruleset-probe.ts +2 -1
  158. package/src/github/repository-ruleset-props.ts +95 -0
  159. package/src/github/repository-ruleset-providers.ts +46 -0
  160. package/src/github/repository-ruleset-reconcile.ts +35 -0
  161. package/src/github/repository-ruleset-wire.test.ts +38 -27
  162. package/src/github/repository-ruleset-write-refusals.test.ts +6 -26
  163. package/src/github/repository-ruleset.ts +53 -43
  164. package/src/litellm/fake-litellm.ts +83 -78
  165. package/src/litellm/fake-stack.ts +13 -5
  166. package/src/litellm/index.ts +7 -21
  167. package/src/litellm/operations.ts +149 -0
  168. package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +17 -20
  169. package/src/litellm/pass-through-endpoint-replace.test.ts +3 -4
  170. package/src/litellm/pass-through-endpoint.test.ts +8 -8
  171. package/src/litellm/pass-through-endpoint.ts +21 -31
  172. package/src/litellm/pass-through-errors.ts +37 -0
  173. package/src/litellm/pass-through-form.ts +20 -10
  174. package/src/litellm/providers.ts +8 -7
  175. package/src/paperless/custom-field.test.ts +49 -40
  176. package/src/paperless/custom-field.ts +57 -33
  177. package/src/paperless/document-type.test.ts +48 -0
  178. package/src/paperless/document-type.ts +51 -22
  179. package/src/paperless/fake-paperless.ts +63 -72
  180. package/src/paperless/index.ts +7 -13
  181. package/src/paperless/matching-locate.test.ts +102 -0
  182. package/src/paperless/matching-locate.ts +80 -86
  183. package/src/paperless/matching-types.ts +49 -16
  184. package/src/paperless/matching.ts +77 -90
  185. package/src/paperless/providers.ts +14 -10
  186. package/src/paperless/storage-path.test.ts +56 -0
  187. package/src/paperless/storage-path.ts +53 -24
  188. package/src/paperless/tag-identity.test.ts +127 -0
  189. package/src/paperless/tag.test.ts +100 -0
  190. package/src/paperless/tag.ts +62 -32
  191. package/src/proxmox/acl.test.ts +145 -0
  192. package/src/proxmox/acl.ts +128 -150
  193. package/src/proxmox/distilled-pve.test.ts +231 -0
  194. package/src/proxmox/distilled-pve.ts +147 -0
  195. package/src/proxmox/members-timeout.test.ts +161 -0
  196. package/src/proxmox/members.test.ts +9 -0
  197. package/src/proxmox/members.ts +36 -2
  198. package/dist/litellm/client.d.ts +0 -76
  199. package/dist/litellm/client.d.ts.map +0 -1
  200. package/dist/litellm/credentials.d.ts +0 -48
  201. package/dist/litellm/credentials.d.ts.map +0 -1
  202. package/dist/litellm/generated/pass-through.d.ts +0 -71
  203. package/dist/litellm/generated/pass-through.d.ts.map +0 -1
  204. package/dist/paperless/client.d.ts +0 -37
  205. package/dist/paperless/client.d.ts.map +0 -1
  206. package/dist/paperless/credentials.d.ts +0 -40
  207. package/dist/paperless/credentials.d.ts.map +0 -1
  208. package/dist/paperless/errors.d.ts +0 -49
  209. package/dist/paperless/errors.d.ts.map +0 -1
  210. package/src/litellm/client.ts +0 -195
  211. package/src/litellm/credentials.ts +0 -73
  212. package/src/litellm/generated/pass-through.ts +0 -73
  213. package/src/paperless/client.ts +0 -88
  214. package/src/paperless/credentials.ts +0 -69
  215. package/src/paperless/errors.ts +0 -62
  216. package/src/paperless/matching-identity.test.ts +0 -194
  217. package/src/paperless/matching.test.ts +0 -128
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `GitHub.RepositoryRuleset`'s declared shape — split out of repository-ruleset.ts purely to
3
+ * keep that file (the provider wiring) under the 250-line cap; nothing here is provider logic.
4
+ */
5
+ import type { RulesetProps } from 'alchemy/GitHub';
6
+
7
+ type UpstreamRules = NonNullable<RulesetProps['rules']>;
8
+
9
+ export interface RepositoryRulesetReviewerRule {
10
+ readonly filePatterns: readonly string[];
11
+ readonly minimumApprovals: number;
12
+ readonly reviewer: { readonly actorId: number; readonly actorType: 'Team' };
13
+ }
14
+
15
+ export interface RepositoryRulesetPullRequestRule extends NonNullable<
16
+ UpstreamRules['pullRequest']
17
+ > {
18
+ /** @default every method GitHub allows — the house baseline always pins `['squash']`. */
19
+ readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
20
+ readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
21
+ /** `require_extra_approval_for_unattributed_changes` — see repository-ruleset.ts's H15.
22
+ * Both values are modeled (K1, 2026-09-23): 8 live rulesets carry `true` (aop, cloudflareforms,
23
+ * doesthishelp-workeropen, homeflare-anyauth, homeflare-desktop, loggarr, magictransit,
24
+ * proxmox-tb4 — re-read live via `gh api repos/taslabs-net/<repo>/rulesets/<id>`,
25
+ * 2026-09-23), and this resource declares exactly what is live or it cannot adopt them —
26
+ * `undefined` stays "no opinion" (GitHub defaults an absent key to `true`). */
27
+ readonly extraApprovalForUnattributedChanges?: boolean;
28
+ }
29
+
30
+ export interface RepositoryRulesetStatusChecksRule extends NonNullable<
31
+ UpstreamRules['requiredStatusChecks']
32
+ > {
33
+ readonly doNotEnforceOnCreate?: boolean;
34
+ }
35
+
36
+ export interface RepositoryRulesetRules extends Omit<
37
+ UpstreamRules,
38
+ 'pullRequest' | 'requiredStatusChecks'
39
+ > {
40
+ /** `undefined`: no opinion (refused if the live ruleset already has one and this is not an
41
+ * exact-adopt pass-through — see repository-ruleset-guards.ts's `undeclaredLiveRuleRefusal`).
42
+ * `false`: declare its ABSENCE — either "this repo never had one" (a create, or an adopt
43
+ * where live already has none, both no-ops) or a deliberate removal of a live rule, which
44
+ * `ruleNarrowingRefusal` gates behind `acknowledgeRuleNarrowing` (see below). Added 2026-09-23 for
45
+ * exact-adopt pass-through: `requiredStatusChecks` already had this `false` escape; `deletion`/
46
+ * `nonFastForward`/etc already have it implicitly (plain `boolean`, so `false` is already a
47
+ * value); `pullRequest` was the one modeled rule with NO way to express "not present at all" —
48
+ * a repo like `taslabs-net/taslabs-net` (measured live 2026-09-23: `rules = [deletion,
49
+ * non_fast_forward]`, no `pull_request`) could not be adopted through this resource without
50
+ * either refusing to omit the key (a type error) or silently declaring a brand-new rule. */
51
+ readonly pullRequest?: RepositoryRulesetPullRequestRule | false;
52
+ /** `undefined`: no opinion (refused if the live ruleset already has one — see
53
+ * repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
54
+ readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
55
+ }
56
+
57
+ export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
58
+ readonly rules?: RepositoryRulesetRules;
59
+ /**
60
+ * An explicit, reasoned sign-off that THIS declaration deliberately drops a live bypass actor
61
+ * (`bypassActors` is a strict subset of what is live). Without this, `reconcile` refuses the
62
+ * write rather than silently loosen a ruleset that already protects something — see
63
+ * `bypassNarrowingRefusal` in repository-ruleset-narrowing-guards.ts. `reason` is not logged or
64
+ * sent to GitHub; it exists so the declaration's own diff carries WHY, for the next reader
65
+ * (S21: a refusal names the fact it checked — this is the human-authored counterpart for the
66
+ * one case a fact alone cannot settle: "is this narrowing intentional?").
67
+ *
68
+ * ⛔ SEPARATE FROM `acknowledgeRuleNarrowing` ON PURPOSE (2026-09-23 review finding: a single
69
+ * shared `acknowledgeNarrowing` let one reason worded for a rule drop silently also rubber-
70
+ * stamp an unrelated bypass-actor drop in the SAME declaration — a `bypassActors: []` left
71
+ * over from an unrelated edit slipped through under a reason that never mentioned bypass at
72
+ * all). Each prop authorizes only the narrowing its own guard checks; a declaration that
73
+ * narrows both needs both, each with its own reason.
74
+ *
75
+ * ⛔ ALSO A STANDING PROP, NOT A ONE-TIME TOKEN — remove it from the declaration once the
76
+ * narrowing it describes has landed. Left in place, it keeps disabling the bypass-narrowing
77
+ * guard for every future `reconcile` of this resource, not only the change it was written
78
+ * for — the same way `rules: { deletion: false }` stays a standing "this rule stays absent"
79
+ * fact rather than a one-time action, only here the standing fact is "bypass narrowing needs
80
+ * no review", which is a fact you want true for as short a window as possible. A reviewer
81
+ * reading a diff that adds a SECOND, unrelated bypass drop under an OLD
82
+ * `acknowledgeBypassNarrowing` left over from a prior change is the failure mode this note
83
+ * exists to head off.
84
+ */
85
+ readonly acknowledgeBypassNarrowing?: { readonly reason: string };
86
+ /**
87
+ * The `rules`-side counterpart to `acknowledgeBypassNarrowing` above — an explicit, reasoned
88
+ * sign-off that THIS declaration deliberately drops a live rule (a modeled rule type declared
89
+ * `false`/boolean-`false` while the live ruleset still has it). Without this, `reconcile`
90
+ * refuses the write — see `ruleNarrowingRefusal` in repository-ruleset-narrowing-guards.ts.
91
+ * Same standing-prop caution as `acknowledgeBypassNarrowing`: remove it once the rule drop it
92
+ * describes has landed, or it keeps excusing every later rule drop on this resource too.
93
+ */
94
+ readonly acknowledgeRuleNarrowing?: { readonly reason: string };
95
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `repoPolicyProviders` — the documented stack wiring for `RepositoryRuleset` (directly, or
3
+ * through `declareRepoPolicy` / `declareRepoBaseline`): `GitHub.providers()` and
4
+ * `RepositoryRulesetProvider()`, composed so the whole thing typechecks and needs nothing
5
+ * further from the stack.
6
+ *
7
+ * providers: repoPolicyProviders()
8
+ *
9
+ * ⛔ WHY THIS EXISTS — MEASURED 2026-09-24 (homeflare-builds bump PR 6, tsc). Writing the two
10
+ * providers side by side, `Layer.mergeAll(GitHub.providers(), RepositoryRulesetProvider())`,
11
+ * type-errors: `RepositoryRulesetProvider()`'s handlers call `octokitFor`, which needs
12
+ * `GitHubCredentials` — a requirement of the PROVIDER's layer, correctly inferred by
13
+ * `Provider.succeed` from `repositoryRulesetHandlers` in repository-ruleset.ts. Merging two
14
+ * layers does not thread one's output into the other's requirement; `RepositoryRulesetProvider()`
15
+ * still needs `GitHubCredentials` fed in. `GitHub.providers()` is the only thing here that
16
+ * resolves credentials (env, stored PAT, `gh` CLI, or OAuth — see `alchemy/src/GitHub/
17
+ * Providers.ts`), and — because it chains `Layer.provideMerge(Credentials.fromAuthProvider(…))`
18
+ * internally rather than `Layer.provide` — its own OUTPUT includes `GitHubCredentials`
19
+ * alongside `GitHub.Providers`. `Layer.provideMerge(github)` on `RepositoryRulesetProvider()`
20
+ * both supplies that requirement AND keeps `github`'s own output merged in for any
21
+ * `GitHub.Repository`/`GitHub.Ruleset` the caller also declares. `github` is built once and
22
+ * referenced twice below — Effect's layer memoization (by reference) builds it once, not
23
+ * twice, not twice against the auth provider either.
24
+ *
25
+ * ★ THIS IS ONLY HALF OF 2026-09-24's FIX. The other half — `RepositoryRuleset`'s own `Resource<>`
26
+ * declaration no longer naming `GitHubCredentials` in its 5th (`Providers`) slot — lives in
27
+ * repository-ruleset.ts's file header; that half is what let a stack BODY calling
28
+ * `declareRepoPolicy`/`declareRepoBaseline` typecheck at all. Composing the providers
29
+ * correctly, as this function does, does not by itself fix a body that still requires
30
+ * `GitHubCredentials` directly.
31
+ *
32
+ * ⚠️ DON'T ALSO MERGE A SEPARATE `GitHub.providers()` CALL IN. `github` here is one Layer
33
+ * object referenced twice, which Effect's layer memoization (by reference, ADVERSARIAL
34
+ * REVIEW 2026-09-24: confirmed against `effect@4.0.0-rc.115`'s own `MemoMap`) builds once. A
35
+ * stack that ALSO writes its own `GitHub.providers()` alongside this function's result is a
36
+ * second, distinct Layer object — a genuine second credential resolution, not shared with
37
+ * this one. Get every GitHub resource's providers from this one call.
38
+ */
39
+ import * as GitHub from 'alchemy/GitHub';
40
+ import * as Layer from 'effect/Layer';
41
+ import { RepositoryRulesetProvider } from './repository-ruleset.ts';
42
+
43
+ export const repoPolicyProviders = (options?: GitHub.ProvidersOptions) => {
44
+ const github = GitHub.providers(options);
45
+ return Layer.mergeAll(github, RepositoryRulesetProvider().pipe(Layer.provideMerge(github)));
46
+ };
@@ -20,6 +20,12 @@ import {
20
20
  requiredChecksOmissionRefusal,
21
21
  undeclaredLiveRuleRefusal,
22
22
  } from './repository-ruleset-guards.ts';
23
+ import {
24
+ bypassNarrowingRefusal,
25
+ isBypassNarrowingAcknowledged,
26
+ isRuleNarrowingAcknowledged,
27
+ ruleNarrowingRefusal,
28
+ } from './repository-ruleset-narrowing-guards.ts';
23
29
  import {
24
30
  type RulesetOctokit,
25
31
  type RulesetRecord,
@@ -104,6 +110,12 @@ export const reconcileRuleset = <R = never>(input: {
104
110
  observed = yield* probeByName(octokit, { owner, repo, name: news.name, target });
105
111
  }
106
112
 
113
+ // Deliberately TWO separate reads, not one shared flag — each guard below is authorized
114
+ // only by the acknowledgement scoped to what it checks (see the file header on
115
+ // repository-ruleset-narrowing-guards.ts for the 2026-09-23 finding this fixes).
116
+ const bypassAcknowledged = isBypassNarrowingAcknowledged(news);
117
+ const ruleAcknowledged = isRuleNarrowingAcknowledged(news);
118
+
107
119
  const bypassRefusal =
108
120
  news.bypassActors === undefined
109
121
  ? undefined
@@ -116,6 +128,19 @@ export const reconcileRuleset = <R = never>(input: {
116
128
  });
117
129
  if (bypassRefusal !== undefined) return yield* Effect.fail(bypassRefusal);
118
130
 
131
+ const bypassNarrowRefusal =
132
+ news.bypassActors === undefined
133
+ ? undefined
134
+ : bypassNarrowingRefusal({
135
+ owner,
136
+ repository: repo,
137
+ name: news.name,
138
+ declared: news.bypassActors,
139
+ live: observed,
140
+ acknowledged: bypassAcknowledged,
141
+ });
142
+ if (bypassNarrowRefusal !== undefined) return yield* Effect.fail(bypassNarrowRefusal);
143
+
119
144
  const ruleRefusal = undeclaredLiveRuleRefusal({
120
145
  owner,
121
146
  repository: repo,
@@ -125,6 +150,16 @@ export const reconcileRuleset = <R = never>(input: {
125
150
  });
126
151
  if (ruleRefusal !== undefined) return yield* Effect.fail(ruleRefusal);
127
152
 
153
+ const ruleNarrowRefusal = ruleNarrowingRefusal({
154
+ owner,
155
+ repository: repo,
156
+ name: news.name,
157
+ declared: news.rules,
158
+ live: observed,
159
+ acknowledged: ruleAcknowledged,
160
+ });
161
+ if (ruleNarrowRefusal !== undefined) return yield* Effect.fail(ruleNarrowRefusal);
162
+
128
163
  const checksRefusal = requiredChecksOmissionRefusal({
129
164
  owner,
130
165
  repository: repo,
@@ -32,35 +32,46 @@ function makeCapturingFetch(captured: CapturedRequest[]): typeof fetch {
32
32
  }
33
33
 
34
34
  describe('does Octokit forward an untyped extra body field to the wire?', () => {
35
- test('require_extra_approval_for_unattributed_changes survives createRepoRuleset', async () => {
36
- const captured: CapturedRequest[] = [];
37
- const octokit = new Octokit({
38
- auth: 'fake-token-not-a-real-credential',
39
- request: { fetch: makeCapturingFetch(captured) },
40
- });
35
+ // K1 (2026-09-23): both values are modeled now, not only `false` — 8 live rulesets (aop,
36
+ // cloudflareforms, doesthishelp-workeropen, homeflare-anyauth, homeflare-desktop, loggarr,
37
+ // magictransit, proxmox-tb4; re-read live 2026-09-23) carry `true`, so this measurement needs
38
+ // both, not just the one value this resource used to be able to send.
39
+ test.each([false, true])(
40
+ 'require_extra_approval_for_unattributed_changes: %p survives createRepoRuleset',
41
+ async (value) => {
42
+ const captured: CapturedRequest[] = [];
43
+ const octokit = new Octokit({
44
+ auth: 'fake-token-not-a-real-credential',
45
+ request: { fetch: makeCapturingFetch(captured) },
46
+ });
41
47
 
42
- const body = desiredWireRuleset({
43
- owner: 'taslabs-net',
44
- repository: 'widgets',
45
- name: 'main',
46
- rules: {
47
- pullRequest: {
48
- requiredApprovingReviewCount: 0,
49
- allowedMergeMethods: ['squash'],
50
- extraApprovalForUnattributedChanges: false,
48
+ const body = desiredWireRuleset({
49
+ owner: 'taslabs-net',
50
+ repository: 'widgets',
51
+ name: 'main',
52
+ rules: {
53
+ pullRequest: {
54
+ requiredApprovingReviewCount: 0,
55
+ allowedMergeMethods: ['squash'],
56
+ extraApprovalForUnattributedChanges: value,
57
+ },
51
58
  },
52
- },
53
- });
59
+ });
54
60
 
55
- await octokit.rest.repos.createRepoRuleset({ owner: 'taslabs-net', repo: 'widgets', ...body });
61
+ await octokit.rest.repos.createRepoRuleset({
62
+ owner: 'taslabs-net',
63
+ repo: 'widgets',
64
+ ...body,
65
+ });
56
66
 
57
- expect(captured).toHaveLength(1);
58
- const sent = captured[0]?.body as {
59
- rules?: { type: string; parameters?: Record<string, unknown> }[];
60
- };
61
- const pr = sent?.rules?.find((r) => r.type === 'pull_request');
62
- // MEASURED: this is what actually crossed the wire, not what Octokit's types say is legal.
63
- expect(pr?.parameters?.require_extra_approval_for_unattributed_changes).toBe(false);
64
- expect(pr?.parameters?.allowed_merge_methods).toEqual(['squash']);
65
- });
67
+ expect(captured).toHaveLength(1);
68
+ const sent = captured[0]?.body as {
69
+ rules?: { type: string; parameters?: Record<string, unknown> }[];
70
+ };
71
+ const pr = sent?.rules?.find((r) => r.type === 'pull_request');
72
+ // MEASURED: this is what actually crossed the wire, not what Octokit's types say is legal.
73
+ expect(pr?.parameters?.require_extra_approval_for_unattributed_changes).toBe(value);
74
+ expect(pr?.parameters?.allowed_merge_methods).toEqual(['squash']);
75
+ },
76
+ );
66
77
  });
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * (ii) The write-recording fake, refusals: duplicate names, bypass widening, an undeclared live
3
3
  * rule, required checks omitted, and a never-reported context each make ZERO writes; and a
4
- * readback mismatch fails even though the write itself "succeeded" (S10). The happy paths
5
- * (create, noop, update) are repository-ruleset-write.test.ts.
4
+ * readback mismatch fails even though the write itself "succeeded" (S10). Bypass/rule narrowing
5
+ * without the matching `acknowledge{Bypass,Rule}Narrowing` are their own file,
6
+ * repository-ruleset-narrowing.test.ts (split out purely for the 250-line cap). The happy paths
7
+ * (create, noop, update) are repository-ruleset-write.test.ts; exact-adopt pass-through's own
8
+ * dedicated fixtures (all 5 live non-empty-bypass shapes) are
9
+ * repository-ruleset-exact-adopt.test.ts.
6
10
  */
7
11
  import { describe, expect, test } from 'bun:test';
8
12
  import * as Effect from 'effect/Effect';
@@ -94,30 +98,6 @@ describe('refusals make zero writes', () => {
94
98
  expect(tagOf(error)).toBe('UndeclaredLiveRule');
95
99
  });
96
100
 
97
- test('the SAME live ruleset with deletion explicitly declared false is a deliberate removal, not refused', async () => {
98
- const live = {
99
- id: 1,
100
- name: 'main',
101
- enforcement: 'active',
102
- bypass_actors: [],
103
- conditions: {},
104
- rules: [
105
- { type: 'deletion' },
106
- { type: 'pull_request', parameters: { required_approving_review_count: 0 } },
107
- ],
108
- };
109
- const fake = makeRecordingFake({ widgets: live });
110
- fake.reportedContexts.add('ci');
111
- const declaration = props({
112
- rules: { deletion: false, pullRequest: BASE_PULL_REQUEST },
113
- });
114
- await reconcileAgainst(fake, declaration, { rulesetId: 1 }).pipe(Effect.runPromise);
115
- expect(fake.writes).toHaveLength(1);
116
- expect(fake.writes[0]?.op).toBe('update');
117
- const sentTypes = fake.writes[0]?.body?.rules?.map((r) => r.type);
118
- expect(sentTypes).not.toContain('deletion');
119
- });
120
-
121
101
  test('required checks omitted while the live ruleset has them', async () => {
122
102
  const live = {
123
103
  id: 1,
@@ -38,11 +38,33 @@
38
38
  * meant to allow. `octokitFor`/`GitHubCredentials` are upstream's own EXPORTED building
39
39
  * blocks (S23's second clause: reuse Alchemy's own client construction), so the client itself
40
40
  * is not hand-rolled — only the choice of Octokit over distilled is a recorded divergence.
41
+ *
42
+ * ⛔ CREDENTIALS BELONG TO THE PROVIDER'S LAYER, NOT THE RESOURCE'S DECLARATION — MEASURED
43
+ * 2026-09-24 (homeflare-builds bump PR 6, tsc TS2345). The `Resource<Type, Props, Attributes,
44
+ * Binding, Providers>` 5th type parameter had `GitHubCredentials` here; that puts the
45
+ * credential requirement on every `yield* RepositoryRuleset(...)` CALL SITE (see
46
+ * `alchemy/src/Resource.ts#ResourceClass`: `Req = R["Providers"] extends undefined ?
47
+ * Provider<R> : R["Providers"]`), so a STACK BODY that merely declares the resource would
48
+ * itself need `GitHubCredentials` in its own effect context — which a stack body cannot
49
+ * produce (`Alchemy.Stack`'s own type, `alchemy/src/Stack.ts#ProviderServices`, is a closed
50
+ * union of tagged marker interfaces — `ProviderCollectionLike | Provider<any> |
51
+ * EnvironmentLike | CredentialsLike | DockerLike` — and `GitHubCredentials` matches none of
52
+ * them structurally). VERIFIED against upstream `Ruleset` (`node_modules/alchemy/src/GitHub/
53
+ * Ruleset.ts`): its 5th slot is `GitHub.Providers`, the ProviderCollection tag `GitHub.
54
+ * providers()` OUTPUTS — never the raw `GitHubCredentials` service upstream's OWN provider
55
+ * handlers pull in via `octokitFor`. `GitHubCredentials` stays confined to the PROVIDER's
56
+ * layer requirement (`Provider.succeed`'s inferred `ReadReq | DiffReq | ReconcileReq |
57
+ * DeleteReq`, below), never the declaration's. Omitting the 5th parameter here defaults it to
58
+ * `undefined`, so the declaration's own requirement becomes `Provider<RepositoryRuleset>` —
59
+ * itself a `Provider<any>`, a real `ProviderServices` member — matching the pattern a
60
+ * standalone (non-collection) resource actually needs. See `repoPolicyProviders()` in
61
+ * repository-ruleset-providers.ts for the layer composition this now requires (the provider
62
+ * still needs `GitHubCredentials` fed in — that half of the fix was already correct, per
63
+ * `docs/repository-ruleset.md`).
41
64
  */
42
65
  import { Resource } from 'alchemy';
43
66
  import { Unowned } from 'alchemy/AdoptPolicy';
44
67
  import { isResolved } from 'alchemy/Diff';
45
- import type { GitHubCredentials, RulesetProps } from 'alchemy/GitHub';
46
68
  import * as Provider from 'alchemy/Provider';
47
69
  import * as Effect from 'effect/Effect';
48
70
  import {
@@ -52,46 +74,16 @@ import {
52
74
  import { RulesetConstraintRefused } from './repository-ruleset-errors.ts';
53
75
  import { makeRulesetOctokit } from './repository-ruleset-octokit.ts';
54
76
  import { type RulesetRecord, probeByName } from './repository-ruleset-probe.ts';
77
+ import type { RepositoryRulesetProps } from './repository-ruleset-props.ts';
55
78
  import { reconcileRuleset } from './repository-ruleset-reconcile.ts';
56
79
 
57
- type UpstreamRules = NonNullable<RulesetProps['rules']>;
58
-
59
- export interface RepositoryRulesetReviewerRule {
60
- readonly filePatterns: readonly string[];
61
- readonly minimumApprovals: number;
62
- readonly reviewer: { readonly actorId: number; readonly actorType: 'Team' };
63
- }
64
-
65
- export interface RepositoryRulesetPullRequestRule extends NonNullable<
66
- UpstreamRules['pullRequest']
67
- > {
68
- /** @default every method GitHub allows — the house baseline always pins `['squash']`. */
69
- readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
70
- readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
71
- /** `require_extra_approval_for_unattributed_changes` — see H15 above. Only `false` is
72
- * modeled: sending `true` has no house use, and GitHub already defaults absent to `true`. */
73
- readonly extraApprovalForUnattributedChanges?: false;
74
- }
75
-
76
- export interface RepositoryRulesetStatusChecksRule extends NonNullable<
77
- UpstreamRules['requiredStatusChecks']
78
- > {
79
- readonly doNotEnforceOnCreate?: boolean;
80
- }
81
-
82
- export interface RepositoryRulesetRules extends Omit<
83
- UpstreamRules,
84
- 'pullRequest' | 'requiredStatusChecks'
85
- > {
86
- readonly pullRequest?: RepositoryRulesetPullRequestRule;
87
- /** `undefined`: no opinion (refused if the live ruleset already has one — see
88
- * repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
89
- readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
90
- }
91
-
92
- export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
93
- readonly rules?: RepositoryRulesetRules;
94
- }
80
+ export type {
81
+ RepositoryRulesetProps,
82
+ RepositoryRulesetPullRequestRule,
83
+ RepositoryRulesetReviewerRule,
84
+ RepositoryRulesetRules,
85
+ RepositoryRulesetStatusChecksRule,
86
+ } from './repository-ruleset-props.ts';
95
87
 
96
88
  export interface RepositoryRulesetAttributes {
97
89
  readonly rulesetId: number;
@@ -105,8 +97,10 @@ export interface RepositoryRuleset extends Resource<
105
97
  'GitHub.RepositoryRuleset',
106
98
  RepositoryRulesetProps,
107
99
  RepositoryRulesetAttributes,
108
- never,
109
- GitHubCredentials
100
+ never
101
+ // ⛔ NO 5th (`Providers`) ARGUMENT — see the file header. Defaulting it to `undefined` makes
102
+ // a `yield* RepositoryRuleset(...)` call site require `Provider<RepositoryRuleset>`, not
103
+ // `GitHubCredentials`; the credential requirement lives on the PROVIDER below instead.
110
104
  > {}
111
105
 
112
106
  export const RepositoryRuleset = Resource<RepositoryRuleset>('GitHub.RepositoryRuleset', {
@@ -124,7 +118,11 @@ export const attrsOf = (r: RulesetRecord): RepositoryRulesetAttributes => ({
124
118
  /** Plan-time-only refusals — no live data needed, so these run before any read (schema-codegen's
125
119
  * rule: check the form before the "no previous state" branch, never after it). */
126
120
  function constraintRefusal(props: RepositoryRulesetProps): RulesetConstraintRefused | undefined {
127
- const approvals = props.rules?.pullRequest?.requiredApprovingReviewCount;
121
+ // `=== false` narrows `pullRequest` out of the `| false` half added for exact-adopt
122
+ // pass-through (see `RepositoryRulesetRules` above) — a declared absence has no fields to
123
+ // constrain.
124
+ const pr = props.rules?.pullRequest === false ? undefined : props.rules?.pullRequest;
125
+ const approvals = pr?.requiredApprovingReviewCount;
128
126
  if (approvals !== undefined) {
129
127
  const reason = approvalCountRefusal(approvals);
130
128
  if (reason !== undefined) {
@@ -134,7 +132,7 @@ function constraintRefusal(props: RepositoryRulesetProps): RulesetConstraintRefu
134
132
  });
135
133
  }
136
134
  }
137
- const methods = props.rules?.pullRequest?.allowedMergeMethods;
135
+ const methods = pr?.allowedMergeMethods;
138
136
  if (methods !== undefined) {
139
137
  const reason = allowedMergeMethodsRefusal(methods);
140
138
  if (reason !== undefined) {
@@ -144,6 +142,18 @@ function constraintRefusal(props: RepositoryRulesetProps): RulesetConstraintRefu
144
142
  });
145
143
  }
146
144
  }
145
+ // ⚠️ NO SEPARATE PLAN-TIME CHECK FOR A BLANK `acknowledge{Bypass,Rule}Narrowing.reason` HERE.
146
+ // A blank reason is already indistinguishable from "not acknowledged at all" —
147
+ // `isBypassNarrowingAcknowledged`/`isRuleNarrowingAcknowledged`
148
+ // (repository-ruleset-narrowing-guards.ts) each require a non-empty, trimmed `reason` on
149
+ // their OWN prop, and the matching narrowing guard reads that, not presence alone. Adding a
150
+ // SECOND check here would refuse with `RulesetConstraintRefused` instead of the more
151
+ // specific `BypassActorNarrowed`/`RuleNarrowed` the caller actually needs to act on — and,
152
+ // unlike those two (exercised directly through `reconcileRuleset`, injected-octokit fakes
153
+ // included), this function is wired only through the real `RepositoryRuleset.Provider`
154
+ // (`makeRulesetOctokit` needs live `GitHubCredentials`), which this family's own test
155
+ // helpers do not exercise (see `repository-ruleset-test-helpers.ts`'s stub
156
+ // `constraintRefusal`) — a check that lives only here would ship untested.
147
157
  return undefined;
148
158
  }
149
159