@homeflare/alchemy 0.27.4 → 0.28.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.
@@ -0,0 +1,88 @@
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
+ type UpstreamRules = NonNullable<RulesetProps['rules']>;
7
+ export interface RepositoryRulesetReviewerRule {
8
+ readonly filePatterns: readonly string[];
9
+ readonly minimumApprovals: number;
10
+ readonly reviewer: {
11
+ readonly actorId: number;
12
+ readonly actorType: 'Team';
13
+ };
14
+ }
15
+ export interface RepositoryRulesetPullRequestRule extends NonNullable<UpstreamRules['pullRequest']> {
16
+ /** @default every method GitHub allows — the house baseline always pins `['squash']`. */
17
+ readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
18
+ readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
19
+ /** `require_extra_approval_for_unattributed_changes` — see repository-ruleset.ts's H15. Only
20
+ * `false` is modeled: sending `true` has no house use, and GitHub already defaults absent to
21
+ * `true`. */
22
+ readonly extraApprovalForUnattributedChanges?: false;
23
+ }
24
+ export interface RepositoryRulesetStatusChecksRule extends NonNullable<UpstreamRules['requiredStatusChecks']> {
25
+ readonly doNotEnforceOnCreate?: boolean;
26
+ }
27
+ export interface RepositoryRulesetRules extends Omit<UpstreamRules, 'pullRequest' | 'requiredStatusChecks'> {
28
+ /** `undefined`: no opinion (refused if the live ruleset already has one and this is not an
29
+ * exact-adopt pass-through — see repository-ruleset-guards.ts's `undeclaredLiveRuleRefusal`).
30
+ * `false`: declare its ABSENCE — either "this repo never had one" (a create, or an adopt
31
+ * where live already has none, both no-ops) or a deliberate removal of a live rule, which
32
+ * `ruleNarrowingRefusal` gates behind `acknowledgeRuleNarrowing` (see below). Added 2026-09-23 for
33
+ * exact-adopt pass-through: `requiredStatusChecks` already had this `false` escape; `deletion`/
34
+ * `nonFastForward`/etc already have it implicitly (plain `boolean`, so `false` is already a
35
+ * value); `pullRequest` was the one modeled rule with NO way to express "not present at all" —
36
+ * a repo like `taslabs-net/taslabs-net` (measured live 2026-09-23: `rules = [deletion,
37
+ * non_fast_forward]`, no `pull_request`) could not be adopted through this resource without
38
+ * either refusing to omit the key (a type error) or silently declaring a brand-new rule. */
39
+ readonly pullRequest?: RepositoryRulesetPullRequestRule | false;
40
+ /** `undefined`: no opinion (refused if the live ruleset already has one — see
41
+ * repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
42
+ readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
43
+ }
44
+ export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
45
+ readonly rules?: RepositoryRulesetRules;
46
+ /**
47
+ * An explicit, reasoned sign-off that THIS declaration deliberately drops a live bypass actor
48
+ * (`bypassActors` is a strict subset of what is live). Without this, `reconcile` refuses the
49
+ * write rather than silently loosen a ruleset that already protects something — see
50
+ * `bypassNarrowingRefusal` in repository-ruleset-narrowing-guards.ts. `reason` is not logged or
51
+ * sent to GitHub; it exists so the declaration's own diff carries WHY, for the next reader
52
+ * (S21: a refusal names the fact it checked — this is the human-authored counterpart for the
53
+ * one case a fact alone cannot settle: "is this narrowing intentional?").
54
+ *
55
+ * ⛔ SEPARATE FROM `acknowledgeRuleNarrowing` ON PURPOSE (2026-09-23 review finding: a single
56
+ * shared `acknowledgeNarrowing` let one reason worded for a rule drop silently also rubber-
57
+ * stamp an unrelated bypass-actor drop in the SAME declaration — a `bypassActors: []` left
58
+ * over from an unrelated edit slipped through under a reason that never mentioned bypass at
59
+ * all). Each prop authorizes only the narrowing its own guard checks; a declaration that
60
+ * narrows both needs both, each with its own reason.
61
+ *
62
+ * ⛔ ALSO A STANDING PROP, NOT A ONE-TIME TOKEN — remove it from the declaration once the
63
+ * narrowing it describes has landed. Left in place, it keeps disabling the bypass-narrowing
64
+ * guard for every future `reconcile` of this resource, not only the change it was written
65
+ * for — the same way `rules: { deletion: false }` stays a standing "this rule stays absent"
66
+ * fact rather than a one-time action, only here the standing fact is "bypass narrowing needs
67
+ * no review", which is a fact you want true for as short a window as possible. A reviewer
68
+ * reading a diff that adds a SECOND, unrelated bypass drop under an OLD
69
+ * `acknowledgeBypassNarrowing` left over from a prior change is the failure mode this note
70
+ * exists to head off.
71
+ */
72
+ readonly acknowledgeBypassNarrowing?: {
73
+ readonly reason: string;
74
+ };
75
+ /**
76
+ * The `rules`-side counterpart to `acknowledgeBypassNarrowing` above — an explicit, reasoned
77
+ * sign-off that THIS declaration deliberately drops a live rule (a modeled rule type declared
78
+ * `false`/boolean-`false` while the live ruleset still has it). Without this, `reconcile`
79
+ * refuses the write — see `ruleNarrowingRefusal` in repository-ruleset-narrowing-guards.ts.
80
+ * Same standing-prop caution as `acknowledgeBypassNarrowing`: remove it once the rule drop it
81
+ * describes has landed, or it keeps excusing every later rule drop on this resource too.
82
+ */
83
+ readonly acknowledgeRuleNarrowing?: {
84
+ readonly reason: string;
85
+ };
86
+ }
87
+ export {};
88
+ //# sourceMappingURL=repository-ruleset-props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-ruleset-props.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-props.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,KAAK,aAAa,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AAExD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7E;AAED,MAAM,WAAW,gCAAiC,SAAQ,WAAW,CACnE,aAAa,CAAC,aAAa,CAAC,CAC7B;IACC,yFAAyF;IACzF,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACtE;;iBAEa;IACb,QAAQ,CAAC,mCAAmC,CAAC,EAAE,KAAK,CAAC;CACtD;AAED,MAAM,WAAW,iCAAkC,SAAQ,WAAW,CACpE,aAAa,CAAC,sBAAsB,CAAC,CACtC;IACC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAClD,aAAa,EACb,aAAa,GAAG,sBAAsB,CACvC;IACC;;;;;;;;;;gGAU4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,gCAAgC,GAAG,KAAK,CAAC;IAChE;2FACuF;IACvF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,iCAAiC,GAAG,KAAK,CAAC;CAC3E;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IACxC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,QAAQ,CAAC,0BAA0B,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE;;;;;;;OAOG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"repository-ruleset-reconcile.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-reconcile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEnG,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AAUxC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAC;AA+CvC;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,GAAG,KAAK,SAAS;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,KAAK,EAAE,sBAAsB,KAC1B,wBAAwB,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,2BAA2B,CAAC;CACrE,KAAG,MAAM,CAAC,MAAM,CAAC,2BAA2B,EAAE,sBAAsB,GAAG,KAAK,EAAE,CAAC,CAuF5E,CAAC"}
1
+ {"version":3,"file":"repository-ruleset-reconcile.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-reconcile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEnG,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AAgBxC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAC;AA+CvC;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,GAAG,KAAK,SAAS;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,KAAK,EAAE,sBAAsB,KAC1B,wBAAwB,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,2BAA2B,CAAC;CACrE,KAAG,MAAM,CAAC,MAAM,CAAC,2BAA2B,EAAE,sBAAsB,GAAG,KAAK,EAAE,CAAC,CAoH5E,CAAC"}
@@ -40,38 +40,11 @@
40
40
  * is not hand-rolled — only the choice of Octokit over distilled is a recorded divergence.
41
41
  */
42
42
  import { Resource } from 'alchemy';
43
- import type { GitHubCredentials, RulesetProps } from 'alchemy/GitHub';
43
+ import type { GitHubCredentials } from 'alchemy/GitHub';
44
44
  import * as Provider from 'alchemy/Provider';
45
45
  import { type RulesetRecord } from './repository-ruleset-probe.ts';
46
- type UpstreamRules = NonNullable<RulesetProps['rules']>;
47
- export interface RepositoryRulesetReviewerRule {
48
- readonly filePatterns: readonly string[];
49
- readonly minimumApprovals: number;
50
- readonly reviewer: {
51
- readonly actorId: number;
52
- readonly actorType: 'Team';
53
- };
54
- }
55
- export interface RepositoryRulesetPullRequestRule extends NonNullable<UpstreamRules['pullRequest']> {
56
- /** @default every method GitHub allows — the house baseline always pins `['squash']`. */
57
- readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
58
- readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
59
- /** `require_extra_approval_for_unattributed_changes` — see H15 above. Only `false` is
60
- * modeled: sending `true` has no house use, and GitHub already defaults absent to `true`. */
61
- readonly extraApprovalForUnattributedChanges?: false;
62
- }
63
- export interface RepositoryRulesetStatusChecksRule extends NonNullable<UpstreamRules['requiredStatusChecks']> {
64
- readonly doNotEnforceOnCreate?: boolean;
65
- }
66
- export interface RepositoryRulesetRules extends Omit<UpstreamRules, 'pullRequest' | 'requiredStatusChecks'> {
67
- readonly pullRequest?: RepositoryRulesetPullRequestRule;
68
- /** `undefined`: no opinion (refused if the live ruleset already has one — see
69
- * repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
70
- readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
71
- }
72
- export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
73
- readonly rules?: RepositoryRulesetRules;
74
- }
46
+ import type { RepositoryRulesetProps } from './repository-ruleset-props.ts';
47
+ export type { RepositoryRulesetProps, RepositoryRulesetPullRequestRule, RepositoryRulesetReviewerRule, RepositoryRulesetRules, RepositoryRulesetStatusChecksRule, } from './repository-ruleset-props.ts';
75
48
  export interface RepositoryRulesetAttributes {
76
49
  readonly rulesetId: number;
77
50
  readonly nodeId: string | undefined;
@@ -85,5 +58,4 @@ export declare const RepositoryRuleset: import("alchemy").ResourceClass<Reposito
85
58
  export declare const attrsOf: (r: RulesetRecord) => RepositoryRulesetAttributes;
86
59
  export declare const repositoryRulesetHandlers: Provider.ProviderService<RepositoryRuleset, GitHubCredentials, never, never, GitHubCredentials, GitHubCredentials, never, never, never>;
87
60
  export declare const RepositoryRulesetProvider: () => import("effect/Layer").Layer<Provider.Provider<RepositoryRuleset>, never, GitHubCredentials>;
88
- export {};
89
61
  //# sourceMappingURL=repository-ruleset.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repository-ruleset.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAQ7C,OAAO,EAAE,KAAK,aAAa,EAAe,MAAM,+BAA+B,CAAC;AAGhF,KAAK,aAAa,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AAExD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7E;AAED,MAAM,WAAW,gCAAiC,SAAQ,WAAW,CACnE,aAAa,CAAC,aAAa,CAAC,CAC7B;IACC,yFAAyF;IACzF,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACtE;iGAC6F;IAC7F,QAAQ,CAAC,mCAAmC,CAAC,EAAE,KAAK,CAAC;CACtD;AAED,MAAM,WAAW,iCAAkC,SAAQ,WAAW,CACpE,aAAa,CAAC,sBAAsB,CAAC,CACtC;IACC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAClD,aAAa,EACb,aAAa,GAAG,sBAAsB,CACvC;IACC,QAAQ,CAAC,WAAW,CAAC,EAAE,gCAAgC,CAAC;IACxD;2FACuF;IACvF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,iCAAiC,GAAG,KAAK,CAAC;CAC3E;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;CACzC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,0BAA0B,EAC1B,sBAAsB,EACtB,2BAA2B,EAC3B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAEH,eAAO,MAAM,OAAO,MAAO,aAAa,KAAG,2BAMzC,CAAC;AA4BH,eAAO,MAAM,yBAAyB,yIAkEpC,CAAC;AAEH,eAAO,MAAM,yBAAyB,oGAC0B,CAAC"}
1
+ {"version":3,"file":"repository-ruleset.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAQ7C,OAAO,EAAE,KAAK,aAAa,EAAe,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAG5E,YAAY,EACV,sBAAsB,EACtB,gCAAgC,EAChC,6BAA6B,EAC7B,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,0BAA0B,EAC1B,sBAAsB,EACtB,2BAA2B,EAC3B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAEH,eAAO,MAAM,OAAO,MAAO,aAAa,KAAG,2BAMzC,CAAC;AA4CH,eAAO,MAAM,yBAAyB,yIAkEpC,CAAC;AAEH,eAAO,MAAM,yBAAyB,oGAC0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.27.4",
3
+ "version": "0.28.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -94,8 +94,12 @@
94
94
  "access": "public"
95
95
  },
96
96
  "dependencies": {
97
+ "@distilled.cloud/caddy": "npm:@homeflare/distilled-caddy@0.2.0",
98
+ "@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.2.0",
97
99
  "@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
98
- "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.2.0"
100
+ "@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
101
+ "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.2.0",
102
+ "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.2.0"
99
103
  },
100
104
  "peerDependencies": {
101
105
  "@distilled.cloud/cloudflare": "1.0.0-rc.12",
@@ -44,11 +44,13 @@ export {
44
44
  } from './repository-ruleset.ts';
45
45
  export { RULE_TYPE_COVERAGE, type RepositoryRuleType } from './repository-ruleset-constraints.ts';
46
46
  export {
47
+ BypassActorNarrowed,
47
48
  BypassActorWidened,
48
49
  DuplicateRuleset,
49
50
  NeverReportedContext,
50
51
  ReadbackMismatch,
51
52
  RequiredChecksOmitted,
53
+ RuleNarrowed,
52
54
  RulesetConstraintRefused,
53
55
  UndeclaredLiveRule,
54
56
  type RepositoryRulesetError,
@@ -43,6 +43,49 @@ export class BypassActorWidened extends Data.TaggedError('BypassActorWidened')<{
43
43
  }
44
44
  }
45
45
 
46
+ /** A declared `bypassActors` drops an actor the live ruleset has, with no
47
+ * `acknowledgeBypassNarrowing` on the declaration. Unlike widening, narrowing is not always
48
+ * wrong — an exact-adopt declaration that matches live exactly narrows nothing — so this is
49
+ * refused only when live genuinely has more than the declaration keeps, and it is escapable
50
+ * (`BypassActorWidened` is not): see `bypassNarrowingRefusal` in
51
+ * repository-ruleset-narrowing-guards.ts. */
52
+ export class BypassActorNarrowed extends Data.TaggedError('BypassActorNarrowed')<{
53
+ readonly owner: string;
54
+ readonly repository: string;
55
+ readonly name: string;
56
+ readonly narrowed: readonly string[];
57
+ }> {
58
+ override get message(): string {
59
+ return (
60
+ `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the ` +
61
+ `declaration drops a bypass actor the live ruleset has (${this.narrowed.join(', ')}), and ` +
62
+ 'the declaration carries no `acknowledgeBypassNarrowing`. Add one with a reason once the ' +
63
+ 'removal is deliberate, or restore the actor to match live.'
64
+ );
65
+ }
66
+ }
67
+
68
+ /** A rule type the declaration explicitly marks absent (`false`) while the live ruleset still
69
+ * has it, with no `acknowledgeRuleNarrowing` on the declaration — see `ruleNarrowingRefusal` in
70
+ * repository-ruleset-narrowing-guards.ts. Distinct from `UndeclaredLiveRule`: that one fires on
71
+ * SILENCE (the declaration says nothing); this one fires on an EXPLICIT, but unacknowledged,
72
+ * removal. */
73
+ export class RuleNarrowed extends Data.TaggedError('RuleNarrowed')<{
74
+ readonly owner: string;
75
+ readonly repository: string;
76
+ readonly name: string;
77
+ readonly ruleType: string;
78
+ }> {
79
+ override get message(): string {
80
+ return (
81
+ `GitHub.RepositoryRuleset "${this.name}" on ${this.owner}/${this.repository}: the ` +
82
+ `declaration drops a live "${this.ruleType}" rule, and carries no ` +
83
+ '`acknowledgeRuleNarrowing`. Add one with a reason once the removal is deliberate, or ' +
84
+ 'declare the rule to match live.'
85
+ );
86
+ }
87
+ }
88
+
46
89
  /** A live rule's `type` is neither modeled nor explicitly refused by RULE_TYPE_COVERAGE, or is
47
90
  * refused and present live anyway. The PUT replaces `rules` wholesale, so silently omitting a
48
91
  * live rule type would drop it — this fails the plan by name instead. */
@@ -129,7 +172,9 @@ export class ReadbackMismatch extends Data.TaggedError('ReadbackMismatch')<{
129
172
  export type RepositoryRulesetError =
130
173
  | DuplicateRuleset
131
174
  | BypassActorWidened
175
+ | BypassActorNarrowed
132
176
  | UndeclaredLiveRule
177
+ | RuleNarrowed
133
178
  | RequiredChecksOmitted
134
179
  | NeverReportedContext
135
180
  | RulesetConstraintRefused
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Exact-adopt pass-through, proven against the 5 live rulesets the red team found this resource
3
+ * could not express before 2026-09-23 (design critique, "strongest_failure": `repoBaselineRuleset()`
4
+ * hardcodes `bypassActors: []` and an unconditional `pullRequest` rule; every one of these 5
5
+ * repos' live ruleset has a non-empty `bypass_actors`, and one — `taslabs-net/taslabs-net` — has
6
+ * no live `pull_request` rule at all). This file proves the underlying `RepositoryRuleset`
7
+ * resource (not the convenience `repoBaselineRuleset()`, which intentionally keeps its baseline
8
+ * defaults — see repo-baseline-data.ts) can express all 5 shapes, and that declaring one exactly
9
+ * is never treated as narrowing.
10
+ *
11
+ * ⚠️ RE-READ LIVE 2026-09-23 via `gh api repos/taslabs-net/<repo>/rulesets/<id>`, not copied
12
+ * from the design doc's own paraphrase — `bypass_mode` differs from the design critique's
13
+ * prose for `doesthishelp-workeropen` (measured: RepositoryRole 5 is `pull_request` mode and
14
+ * Integration 29110 is `always`, not the reverse) — see repository-ruleset-narrowing-guards.ts
15
+ * for the guard these fixtures exercise.
16
+ *
17
+ * `taslabs-net` is the only one of the 5 whose FULL ruleset (bypass AND rules) is provable as a
18
+ * true zero-write adopt today: its live `pull_request` rules (aop/magictransit/loggarr/
19
+ * doesthishelp-workeropen) all carry `require_extra_approval_for_unattributed_changes: true`,
20
+ * which this resource cannot yet declare (K1 — a separate, later PR; H15's comment on
21
+ * `extraApprovalForUnattributedChanges` explains why only `false` is modeled today). The other
22
+ * 4 fixtures below prove the narrower claim K1 does not gate: that this resource's `bypassActors`
23
+ * round-trips an arbitrary `actor_id`/`actor_type`/`bypass_mode` combination — single-actor by
24
+ * role, single-actor by integration, and multi-actor with mixed modes — onto the wire exactly as
25
+ * declared, with neither `BypassActorWidened` nor `BypassActorNarrowed` firing on a match.
26
+ */
27
+ import { describe, expect, test } from 'bun:test';
28
+ import { makeGetOnlyFake, makeRecordingFake } from './repository-ruleset-fake-octokit.ts';
29
+ import {
30
+ BASE_PULL_REQUEST,
31
+ props,
32
+ reconcileWithOctokit,
33
+ run,
34
+ } from './repository-ruleset-test-helpers.ts';
35
+
36
+ describe('exact-adopt pass-through — live shapes measured 2026-09-23', () => {
37
+ // The critical case: a non-empty bypass_actors AND an absent pull_request rule, both at once.
38
+ // `gh api repos/taslabs-net/taslabs-net/rulesets/12206080`.
39
+ test('taslabs-net/taslabs-net: non-empty bypass, no pull_request rule — zero-write adopt', async () => {
40
+ const live = {
41
+ id: 12206080,
42
+ name: 'main',
43
+ target: 'branch',
44
+ enforcement: 'active',
45
+ bypass_actors: [{ actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'always' }],
46
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
47
+ rules: [{ type: 'deletion' }, { type: 'non_fast_forward' }],
48
+ };
49
+ const fake = makeGetOnlyFake({ 'taslabs-net': live });
50
+ const declaration = props({
51
+ repository: 'taslabs-net',
52
+ bypassActors: [{ actorType: 'RepositoryRole', actorId: 5, bypassMode: 'always' }],
53
+ rules: { deletion: true, nonFastForward: true },
54
+ });
55
+ const attrs = await run(reconcileWithOctokit(fake, declaration, { rulesetId: 12206080 }));
56
+ expect(attrs.rulesetId).toBe(12206080);
57
+ });
58
+
59
+ // `gh api repos/taslabs-net/aop/rulesets/14572279` — single RepositoryRole actor, mode
60
+ // "always". Live `pull_request` also carries the K1 flag, so the write below is an `update`,
61
+ // not a noop — the point here is that `bypassActors` alone sends back exactly what was
62
+ // declared, and neither narrowing nor widening guard fires on a matching bypass list.
63
+ test('aop: single RepositoryRole actor round-trips exactly', async () => {
64
+ const live = {
65
+ id: 14572279,
66
+ name: 'main',
67
+ target: 'branch',
68
+ enforcement: 'active',
69
+ bypass_actors: [{ actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'always' }],
70
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
71
+ rules: [
72
+ { type: 'deletion' },
73
+ { type: 'non_fast_forward' },
74
+ { type: 'pull_request', parameters: { required_approving_review_count: 0 } },
75
+ ],
76
+ };
77
+ const fake = makeRecordingFake({ aop: live });
78
+ const declaration = props({
79
+ repository: 'aop',
80
+ bypassActors: [{ actorType: 'RepositoryRole', actorId: 5, bypassMode: 'always' }],
81
+ rules: { deletion: true, nonFastForward: true, pullRequest: BASE_PULL_REQUEST },
82
+ });
83
+ await run(reconcileWithOctokit(fake.octokit, declaration, { rulesetId: 14572279 }));
84
+ expect(fake.writes[0]?.body?.bypass_actors).toEqual([
85
+ { actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'always' },
86
+ ]);
87
+ });
88
+
89
+ // `gh api repos/taslabs-net/loggarr/rulesets/12323114` — a single Integration (Dependabot)
90
+ // actor with `bypass_mode: "pull_request"`, not `"always"`.
91
+ test('loggarr: single Integration actor, pull_request bypass mode, round-trips exactly', async () => {
92
+ const live = {
93
+ id: 12323114,
94
+ name: 'main',
95
+ target: 'branch',
96
+ enforcement: 'active',
97
+ bypass_actors: [{ actor_id: 29110, actor_type: 'Integration', bypass_mode: 'pull_request' }],
98
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
99
+ rules: [
100
+ { type: 'deletion' },
101
+ { type: 'non_fast_forward' },
102
+ { type: 'pull_request', parameters: { required_approving_review_count: 0 } },
103
+ ],
104
+ };
105
+ const fake = makeRecordingFake({ loggarr: live });
106
+ const declaration = props({
107
+ repository: 'loggarr',
108
+ bypassActors: [{ actorType: 'Integration', actorId: 29110, bypassMode: 'pull_request' }],
109
+ rules: { deletion: true, nonFastForward: true, pullRequest: BASE_PULL_REQUEST },
110
+ });
111
+ await run(reconcileWithOctokit(fake.octokit, declaration, { rulesetId: 12323114 }));
112
+ expect(fake.writes[0]?.body?.bypass_actors).toEqual([
113
+ { actor_id: 29110, actor_type: 'Integration', bypass_mode: 'pull_request' },
114
+ ]);
115
+ });
116
+
117
+ // `gh api repos/taslabs-net/doesthishelp-workeropen/rulesets/12504824` — TWO actors with
118
+ // DIFFERENT modes each (the design critique's own "(both)" shorthand for this repo).
119
+ test('doesthishelp-workeropen: two actors, mixed bypass modes, round-trip exactly', async () => {
120
+ const live = {
121
+ id: 12504824,
122
+ name: 'main',
123
+ target: 'branch',
124
+ enforcement: 'active',
125
+ bypass_actors: [
126
+ { actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'pull_request' },
127
+ { actor_id: 29110, actor_type: 'Integration', bypass_mode: 'always' },
128
+ ],
129
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
130
+ rules: [
131
+ { type: 'deletion' },
132
+ { type: 'non_fast_forward' },
133
+ { type: 'pull_request', parameters: { required_approving_review_count: 0 } },
134
+ ],
135
+ };
136
+ const fake = makeRecordingFake({ 'doesthishelp-workeropen': live });
137
+ const declaration = props({
138
+ repository: 'doesthishelp-workeropen',
139
+ bypassActors: [
140
+ { actorType: 'RepositoryRole', actorId: 5, bypassMode: 'pull_request' },
141
+ { actorType: 'Integration', actorId: 29110, bypassMode: 'always' },
142
+ ],
143
+ rules: { deletion: true, nonFastForward: true, pullRequest: BASE_PULL_REQUEST },
144
+ });
145
+ await run(reconcileWithOctokit(fake.octokit, declaration, { rulesetId: 12504824 }));
146
+ expect(fake.writes[0]?.body?.bypass_actors).toEqual([
147
+ { actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'pull_request' },
148
+ { actor_id: 29110, actor_type: 'Integration', bypass_mode: 'always' },
149
+ ]);
150
+ });
151
+
152
+ // `gh api repos/taslabs-net/magictransit/rulesets/10470003` — same single-actor shape as
153
+ // `aop`, kept as its own case because it is one of the 5 the design's own census named.
154
+ test('magictransit: single RepositoryRole actor round-trips exactly', async () => {
155
+ const live = {
156
+ id: 10470003,
157
+ name: 'main',
158
+ target: 'branch',
159
+ enforcement: 'active',
160
+ bypass_actors: [{ actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'always' }],
161
+ conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } },
162
+ rules: [
163
+ { type: 'deletion' },
164
+ { type: 'non_fast_forward' },
165
+ { type: 'pull_request', parameters: { required_approving_review_count: 0 } },
166
+ ],
167
+ };
168
+ const fake = makeRecordingFake({ magictransit: live });
169
+ const declaration = props({
170
+ repository: 'magictransit',
171
+ bypassActors: [{ actorType: 'RepositoryRole', actorId: 5, bypassMode: 'always' }],
172
+ rules: { deletion: true, nonFastForward: true, pullRequest: BASE_PULL_REQUEST },
173
+ });
174
+ await run(reconcileWithOctokit(fake.octokit, declaration, { rulesetId: 10470003 }));
175
+ expect(fake.writes[0]?.body?.bypass_actors).toEqual([
176
+ { actor_id: 5, actor_type: 'RepositoryRole', bypass_mode: 'always' },
177
+ ]);
178
+ });
179
+ });
@@ -14,7 +14,10 @@ import {
14
14
  } from './repository-ruleset-errors.ts';
15
15
  import type { RulesetOctokit, RulesetRecord } from './repository-ruleset-probe.ts';
16
16
 
17
- const bypassKey = (a: { actor_type: unknown; actor_id: unknown; bypass_mode?: unknown }) =>
17
+ /** Exported: repository-ruleset-narrowing-guards.ts reuses the identical key so a widened and a
18
+ * narrowed actor are never computed by two independently-drifting definitions of "the same
19
+ * actor". */
20
+ export const bypassKey = (a: { actor_type: unknown; actor_id: unknown; bypass_mode?: unknown }) =>
18
21
  `${String(a.actor_type)}:${String(a.actor_id ?? '')}:${String(a.bypass_mode ?? 'always')}`;
19
22
 
20
23
  /**
@@ -56,7 +59,10 @@ export function bypassWideningRefusal(input: {
56
59
  });
57
60
  }
58
61
 
59
- const liveRuleTypes = (live: RulesetRecord | undefined): Set<string> =>
62
+ /** Exported for the same reason as `bypassKey` — repository-ruleset-narrowing-guards.ts's
63
+ * `ruleNarrowingRefusal` needs the identical live-type set `undeclaredLiveRuleRefusal` computes,
64
+ * not a second reading of `live.rules` that could disagree with this one. */
65
+ export const liveRuleTypes = (live: RulesetRecord | undefined): Set<string> =>
60
66
  new Set((live?.rules ?? []).map((r) => String(r.type)));
61
67
 
62
68
  /**
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Narrowing refusals — the counterpart to `bypassWideningRefusal`/`undeclaredLiveRuleRefusal` in
3
+ * repository-ruleset-guards.ts, split into its own file purely to keep both under the 250-line
4
+ * cap (same reason repository-ruleset-props.ts exists).
5
+ *
6
+ * ⛔ WIDENING STAYS UNCONDITIONAL; NARROWING GETS AN ESCAPE HATCH. Widening bypass (granting a
7
+ * NEW actor) has no legitimate reason to happen through a deploy — "grant it by hand and adopt
8
+ * the change" (repository-ruleset-errors.ts's `BypassActorWidened` message) already covers it,
9
+ * and this file does not touch that guard. NARROWING (removing a live bypass actor, or
10
+ * dropping a live rule) is different: it is exactly what an EXACT-ADOPT declaration of a repo
11
+ * whose live ruleset is narrower than the house baseline needs to express on its very first
12
+ * deploy — a caller declaring `taslabs-net/taslabs-net`'s live shape (measured live via `gh
13
+ * api repos/taslabs-net/taslabs-net/rulesets/12206080`, 2026-09-23: `bypass_actors:
14
+ * [{actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "always"}]`, `rules: [deletion,
15
+ * non_fast_forward]`, no `pull_request` rule) is not narrowing anything by declaring exactly
16
+ * that shape — it is matching live exactly, and `reconcileRuleset`'s own noop check (comparing
17
+ * canonicalized wire forms) makes zero writes for it, same as any other exact match. What
18
+ * these guards refuse is a LATER declaration that removes something the CURRENT live state
19
+ * still has, without saying why. `acknowledgeBypassNarrowing`/`acknowledgeRuleNarrowing` are
20
+ * the "why": a reasoned, deliberate sign-off, not a rubber stamp — see their doc comments on
21
+ * `RepositoryRulesetProps` (repository-ruleset-props.ts).
22
+ *
23
+ * ⛔ THE TWO ACKNOWLEDGEMENTS ARE DELIBERATELY SEPARATE PROPS, NOT ONE SHARED FLAG (2026-09-23
24
+ * review finding). A single `acknowledgeNarrowing` computed once and handed to both guards let
25
+ * a reason written for one kind of narrowing silently also excuse the other — a declaration
26
+ * dropping BOTH a live rule and a live bypass actor at once needed only one acknowledgement,
27
+ * however narrowly worded, to pass both checks. Each guard below now reads its OWN prop.
28
+ */
29
+ import { bypassKey, liveRuleTypes } from './repository-ruleset-guards.ts';
30
+ import { BypassActorNarrowed, RuleNarrowed } from './repository-ruleset-errors.ts';
31
+ import type { RepositoryRulesetProps, RepositoryRulesetRules } from './repository-ruleset-props.ts';
32
+ import type { RulesetRecord } from './repository-ruleset-probe.ts';
33
+
34
+ /** `true` only for a non-empty, non-whitespace `reason` on `acknowledgeBypassNarrowing` — a bare
35
+ * `{}` or a blank string is not an acknowledgement (repository-ruleset.ts's `constraintRefusal`
36
+ * refuses the latter at plan time; this is the read `bypassNarrowingRefusal` shares). Deliberately
37
+ * ignorant of `acknowledgeRuleNarrowing` — see the file header. */
38
+ export const isBypassNarrowingAcknowledged = (props: RepositoryRulesetProps): boolean =>
39
+ props.acknowledgeBypassNarrowing !== undefined &&
40
+ props.acknowledgeBypassNarrowing.reason.trim().length > 0;
41
+
42
+ /** The `rules`-side counterpart to `isBypassNarrowingAcknowledged`, reading only
43
+ * `acknowledgeRuleNarrowing` — see the file header. */
44
+ export const isRuleNarrowingAcknowledged = (props: RepositoryRulesetProps): boolean =>
45
+ props.acknowledgeRuleNarrowing !== undefined &&
46
+ props.acknowledgeRuleNarrowing.reason.trim().length > 0;
47
+
48
+ /**
49
+ * A live bypass actor the declaration drops, unacknowledged. `undefined` `declared` never
50
+ * reaches here — same rule as `bypassWideningRefusal`: unmanaged means the live value is
51
+ * preserved unchanged (repository-ruleset-reconcile.ts), so there is nothing to narrow.
52
+ */
53
+ export function bypassNarrowingRefusal(input: {
54
+ readonly owner: string;
55
+ readonly repository: string;
56
+ readonly name: string;
57
+ readonly declared: RepositoryRulesetProps['bypassActors'];
58
+ readonly live: RulesetRecord | undefined;
59
+ readonly acknowledged: boolean;
60
+ }): BypassActorNarrowed | undefined {
61
+ if (input.acknowledged) return undefined;
62
+ if (input.live === undefined) return undefined;
63
+ const liveActors = (input.live.bypass_actors ?? []) as {
64
+ actor_type: unknown;
65
+ actor_id: unknown;
66
+ bypass_mode?: unknown;
67
+ }[];
68
+ const declaredKeys = new Set(
69
+ (input.declared ?? []).map((a) =>
70
+ bypassKey({
71
+ actor_type: a.actorType,
72
+ actor_id: a.actorId,
73
+ bypass_mode: a.bypassMode ?? 'always',
74
+ }),
75
+ ),
76
+ );
77
+ const narrowed = liveActors.map(bypassKey).filter((k) => !declaredKeys.has(k));
78
+ if (narrowed.length === 0) return undefined;
79
+ return new BypassActorNarrowed({
80
+ owner: input.owner,
81
+ repository: input.repository,
82
+ name: input.name,
83
+ narrowed,
84
+ });
85
+ }
86
+
87
+ /** The live rule `type` for the first boolean/`pullRequest` rule the declaration marks `false`
88
+ * while live still has it — `undefined` when none does. `requiredStatusChecks: false` is
89
+ * checked by the caller directly (its live-side read needs `required_status_checks`'s own
90
+ * parameters shape, not just presence, elsewhere in this family — kept out of this helper so it
91
+ * stays a plain type-to-type comparison). */
92
+ function droppedBooleanOrPullRequestType(
93
+ rules: RepositoryRulesetRules,
94
+ live: ReadonlySet<string>,
95
+ ): string | undefined {
96
+ const declaredFalse: readonly [boolean | undefined, string][] = [
97
+ [rules.creation, 'creation'],
98
+ [rules.update, 'update'],
99
+ [rules.deletion, 'deletion'],
100
+ [rules.requiredLinearHistory, 'required_linear_history'],
101
+ [rules.requiredSignatures, 'required_signatures'],
102
+ [rules.nonFastForward, 'non_fast_forward'],
103
+ ];
104
+ for (const [declared, type] of declaredFalse) {
105
+ if (declared === false && live.has(type)) return type;
106
+ }
107
+ if (rules.pullRequest === false && live.has('pull_request')) return 'pull_request';
108
+ return undefined;
109
+ }
110
+
111
+ /**
112
+ * A rule the declaration explicitly marks absent (a boolean rule or `pullRequest` declared
113
+ * `false`, or `requiredStatusChecks: false`) while the LIVE ruleset still has it, unacknowledged
114
+ * — the drop `undeclaredLiveRuleRefusal` (repository-ruleset-guards.ts) never sees, because an
115
+ * explicit `false` IS "declared" there (see its own `declaredRuleTypes`).
116
+ */
117
+ export function ruleNarrowingRefusal(input: {
118
+ readonly owner: string;
119
+ readonly repository: string;
120
+ readonly name: string;
121
+ readonly declared: RepositoryRulesetProps['rules'];
122
+ readonly live: RulesetRecord | undefined;
123
+ readonly acknowledged: boolean;
124
+ }): RuleNarrowed | undefined {
125
+ if (input.acknowledged) return undefined;
126
+ const rules = input.declared;
127
+ if (rules === undefined) return undefined;
128
+ const live = liveRuleTypes(input.live);
129
+ const ruleType =
130
+ droppedBooleanOrPullRequestType(rules, live) ??
131
+ (rules.requiredStatusChecks === false && live.has('required_status_checks')
132
+ ? 'required_status_checks'
133
+ : undefined);
134
+ if (ruleType === undefined) return undefined;
135
+ return new RuleNarrowed({
136
+ owner: input.owner,
137
+ repository: input.repository,
138
+ name: input.name,
139
+ ruleType,
140
+ });
141
+ }