@savvy-web/silk-effects 5.7.2 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,11 @@ import { Filter } from "../utils/Filter.js";
5
5
  /**
6
6
  * Handler for TypeScript files.
7
7
  *
8
- * Runs type checking with tsgo or tsc.
8
+ * Runs type checking with tsc or tsgo.
9
9
  *
10
10
  * @remarks
11
11
  * Type checking runs on all staged TypeScript files using the configured
12
- * compiler (tsgo or tsc). The compiler is auto-detected at runtime using
12
+ * compiler (tsc or tsgo). The compiler is auto-detected at runtime using
13
13
  * `Command.findTool()`, which correctly handles pnpm catalogs, peer
14
14
  * dependencies, and hoisted/transitive deps.
15
15
  *
@@ -45,10 +45,16 @@ var TypeScript = class TypeScript {
45
45
  * Detect which TypeScript compiler to use.
46
46
  *
47
47
  * Uses `Command.findTool()` to check for available compilers:
48
- * 1. `tsgo` (native TypeScript) — checked first
49
- * 2. `tsc` (standard TypeScript) — fallback
48
+ * 1. `tsc` (standard TypeScript) — checked first
49
+ * 2. `tsgo` (native TypeScript) — fallback
50
50
  *
51
51
  * @remarks
52
+ * `tsc` is preferred so the pre-commit gate runs the same compiler as a
53
+ * repo's own `types:check` task. Preferring `tsgo` meant any repo with
54
+ * `\@typescript/native-preview` anywhere in its dependency graph — even as
55
+ * a hoisted or transitive dep — silently got a different compiler for its
56
+ * commit gate than for its typecheck task.
57
+ *
52
58
  * Unlike the previous implementation that parsed `package.json` dependencies,
53
59
  * this uses runtime tool detection which works correctly with pnpm catalogs,
54
60
  * peer dependencies, and hoisted/transitive deps.
@@ -58,14 +64,6 @@ var TypeScript = class TypeScript {
58
64
  */
59
65
  static detectCompiler(_cwd) {
60
66
  if (TypeScript.cachedCompilerResult !== null) return TypeScript.cachedCompilerResult.compiler;
61
- const tsgo = Command.findTool("tsgo");
62
- if (tsgo.available) {
63
- TypeScript.cachedCompilerResult = {
64
- compiler: "tsgo",
65
- tool: tsgo
66
- };
67
- return "tsgo";
68
- }
69
67
  const tsc = Command.findTool("tsc");
70
68
  if (tsc.available) {
71
69
  TypeScript.cachedCompilerResult = {
@@ -74,6 +72,14 @@ var TypeScript = class TypeScript {
74
72
  };
75
73
  return "tsc";
76
74
  }
75
+ const tsgo = Command.findTool("tsgo");
76
+ if (tsgo.available) {
77
+ TypeScript.cachedCompilerResult = {
78
+ compiler: "tsgo",
79
+ tool: tsgo
80
+ };
81
+ return "tsgo";
82
+ }
77
83
  }
78
84
  /**
79
85
  * Check if a TypeScript compiler is available.
@@ -90,7 +96,7 @@ var TypeScript = class TypeScript {
90
96
  * Uses the cached `ToolSearchResult` from `detectCompiler()` to build
91
97
  * the command string, avoiding a separate package manager detection step.
92
98
  *
93
- * @returns Command string like `pnpm exec tsgo --noEmit` or `tsgo --noEmit`
99
+ * @returns Command string like `pnpm exec tsc --noEmit` or `tsc --noEmit`
94
100
  * @throws Error if no TypeScript compiler is available
95
101
  */
96
102
  static getDefaultTypecheckCommand() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "5.7.2",
3
+ "version": "5.8.1",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -34,13 +34,13 @@
34
34
  "@changesets/get-github-info": "^1.0.0",
35
35
  "@changesets/get-release-plan": "^5.0.0",
36
36
  "@effected/commands": "^0.4.0",
37
- "@effected/git": "^0.7.0",
37
+ "@effected/git": "^0.8.0",
38
38
  "@effected/glob": "^0.3.0",
39
39
  "@effected/jsonc": "^0.6.0",
40
- "@effected/package-json": "^0.8.0",
40
+ "@effected/package-json": "^0.9.0",
41
41
  "@effected/templates": "^0.2.0",
42
42
  "@effected/walker": "^0.4.0",
43
- "@effected/workspaces": "^0.12.0",
43
+ "@effected/workspaces": "^0.13.0",
44
44
  "@effected/yaml": "^0.8.0",
45
45
  "@manypkg/get-packages": "^3.1.0",
46
46
  "mdast-util-heading-range": "^4.0.0",
@@ -0,0 +1,145 @@
1
+ import { Region } from "./region.js";
2
+ import { Markers } from "./markers.js";
3
+ import { ClosingReferences, OwnedAttribute } from "./references.js";
4
+
5
+ //#region src/pr-body/body.ts
6
+ const FENCE = "```";
7
+ /**
8
+ * The marker-delimited reference region, merging this run's references with
9
+ * any an agent added.
10
+ *
11
+ * @remarks
12
+ * Module-private: the region is always emitted, empty or not, so an agent
13
+ * always has a target to write into — the same reservation the summary
14
+ * region makes.
15
+ */
16
+ const buildReferencesRegion = (args) => {
17
+ const known = new Set(args.linkedIssues.map((issue) => issue.number));
18
+ const ownedNow = ClosingReferences.fromIssues(args.linkedIssues).dedupe();
19
+ const agentAdded = [...new Set(ClosingReferences.parseBare(args.carriedReferences))].filter((id) => !args.previouslyOwned.has(id) && !known.has(id)).sort((a, b) => a - b);
20
+ const lines = ClosingReferences.make({ ids: [...ownedNow.ids, ...agentAdded] }).renderBareLines();
21
+ return `${`${Markers.REFERENCES_START_PREFIX} ${OwnedAttribute.render(ownedNow.ids)} -->`}\n${lines === "" ? "" : `${lines}\n`}${Markers.REFERENCES_END}`;
22
+ };
23
+ /**
24
+ * The proposed squash-commit message, fenced for an AI integration to
25
+ * rewrite.
26
+ *
27
+ * @remarks
28
+ * Module-private. Carries its own `Closes` references because the squash
29
+ * commit needs them too — a reference inside a fenced block is inert to
30
+ * GitHub's linker, which is precisely why the plain-text lines outside it
31
+ * are separate rather than shared.
32
+ */
33
+ const buildSquashBlock = (args) => {
34
+ const closing = ClosingReferences.fromIssues(args.linkedIssues).renderTrailer();
35
+ const message = [
36
+ args.subject,
37
+ closing,
38
+ args.signoff
39
+ ].filter((part) => part !== "").join("\n\n");
40
+ return `${FENCE}${Markers.SQUASH_FENCE_LANGUAGE}\n${message}\n${FENCE}`;
41
+ };
42
+ /**
43
+ * The shared managed-PR-body renderer and its carry-through readers — the
44
+ * single implementation of the contract `silk-release-action` dogfooded at
45
+ * `src/utils/pr-body.ts` (savvy-web/systems#419).
46
+ *
47
+ * @remarks
48
+ * Every operation is pure and total: markers absent, regions broken, or
49
+ * attributes malformed all degrade to the documented fail-safe result
50
+ * (preserve too much rather than delete someone's work) instead of failing —
51
+ * a regenerating action must still produce a body when the prior one is
52
+ * malformed. Use `PrBodyDiagnostic.scan` where a writer wants to be told
53
+ * about a broken pair instead of tolerating it.
54
+ *
55
+ * @public
56
+ */
57
+ var ManagedPrBody = class ManagedPrBody {
58
+ constructor() {}
59
+ /**
60
+ * Build the region of the PR description the generating run owns.
61
+ *
62
+ * @remarks
63
+ * Delimited by `Markers.MANAGED_START` / `Markers.MANAGED_END` so
64
+ * {@link ManagedPrBody.upsert} can regenerate it without touching
65
+ * anything a human wrote around it. Layout, in order: the reserved
66
+ * summary region (nothing may sit above it — a reader meets the prose
67
+ * before the machinery), the proposed-squash-commit fence, and the
68
+ * bare-reference region. No preamble, no file listing, no linked-issues
69
+ * list, no run attribution — each said something already on the page.
70
+ *
71
+ * @public
72
+ */
73
+ static build(options) {
74
+ const parts = [];
75
+ parts.push(`${Markers.SUMMARY_START}\n${options.summary === "" ? "" : `${options.summary}\n`}${Markers.SUMMARY_END}`);
76
+ parts.push(buildSquashBlock({
77
+ subject: options.subject,
78
+ linkedIssues: options.linkedIssues,
79
+ signoff: options.signoff
80
+ }));
81
+ const priorBody = options.priorBody ?? "";
82
+ parts.push(buildReferencesRegion({
83
+ linkedIssues: options.linkedIssues,
84
+ carriedReferences: ManagedPrBody.extractReferences(priorBody),
85
+ previouslyOwned: OwnedAttribute.parse(priorBody)
86
+ }));
87
+ return `${Markers.MANAGED_START}\n${parts.join("\n\n")}\n${Markers.MANAGED_END}`;
88
+ }
89
+ /**
90
+ * Put `managed` (a full {@link ManagedPrBody.build} result) into
91
+ * `existing`, replacing a previous managed region and leaving everything
92
+ * else alone.
93
+ *
94
+ * @remarks
95
+ * Human edits outside the markers survive; a body with no markers keeps
96
+ * its content and gains the region below it. See `Region.upsert` for the
97
+ * splice semantics.
98
+ *
99
+ * @public
100
+ */
101
+ static upsert(existing, managed) {
102
+ return Region.upsert(existing, "silk-release", managed);
103
+ }
104
+ /**
105
+ * The summary region's current content, or `""` when it is empty or
106
+ * absent.
107
+ *
108
+ * @remarks
109
+ * Extraction exists because the managed region is REGENERATED on every
110
+ * run. Re-emitting the region empty would delete a summary the moment
111
+ * any commit landed — destructive and silent, with no signal back to the
112
+ * summariser whose work was discarded. Feed the result to
113
+ * {@link ManagedPrBody.build}'s `summary` option.
114
+ *
115
+ * @public
116
+ */
117
+ static extractSummary(existing) {
118
+ const from = existing.indexOf(Markers.SUMMARY_START);
119
+ const to = existing.indexOf(Markers.SUMMARY_END);
120
+ if (from === -1 || to === -1 || to < from) return "";
121
+ return existing.slice(from + Markers.SUMMARY_START.length, to).trim();
122
+ }
123
+ /**
124
+ * The reference region's current content, or `""` when it is empty or
125
+ * absent.
126
+ *
127
+ * @remarks
128
+ * Symmetric to {@link ManagedPrBody.extractSummary}, and for the same
129
+ * reason. Located by `Markers.REFERENCES_START_PREFIX` — never the plain
130
+ * opening constant — because a generating run emits the attributed form.
131
+ *
132
+ * @public
133
+ */
134
+ static extractReferences(existing) {
135
+ const from = existing.indexOf(Markers.REFERENCES_START_PREFIX);
136
+ const to = existing.indexOf(Markers.REFERENCES_END);
137
+ if (from === -1 || to === -1 || to < from) return "";
138
+ const openEnd = existing.indexOf("-->", from);
139
+ if (openEnd === -1 || openEnd > to) return "";
140
+ return existing.slice(openEnd + 3, to).trim();
141
+ }
142
+ };
143
+
144
+ //#endregion
145
+ export { ManagedPrBody };
@@ -0,0 +1,104 @@
1
+ import { Region } from "./region.js";
2
+ import { Markers } from "./markers.js";
3
+ import { Schema } from "effect";
4
+
5
+ //#region src/pr-body/diagnostics.ts
6
+ /**
7
+ * The problems {@link PrBodyDiagnostic.scan} can report about a body's
8
+ * markers.
9
+ *
10
+ * @public
11
+ */
12
+ const PrBodyDiagnosticCode = Schema.Literals(["unpairedMarker", "duplicateMarker"]);
13
+ /**
14
+ * One problem with a body's silk-release markers.
15
+ *
16
+ * @remarks
17
+ * Diagnostics are advisory VALUES, not a typed error channel: every parse and
18
+ * render operation in this namespace is deliberately total (a regenerating
19
+ * action must still produce a body when the prior one is malformed, and the
20
+ * fail-safe direction is to preserve too much rather than delete someone's
21
+ * work). `scan` exists for the writer that wants to be told about a broken
22
+ * pair before editing — the `pr-body` skill instructs an agent that finds a
23
+ * region missing its pair to stop and report rather than guess, and this is
24
+ * the check that instruction points at. A misplaced marker pair is worse than
25
+ * none: it makes the next regeneration rewrite content it does not own.
26
+ *
27
+ * @public
28
+ */
29
+ var PrBodyDiagnostic = class PrBodyDiagnostic extends Schema.Class("PrBodyDiagnostic")({
30
+ code: PrBodyDiagnosticCode,
31
+ /** The region token the problem is about, e.g. `silk-release:summary`. */
32
+ token: Schema.String
33
+ }) {
34
+ /**
35
+ * A human-readable description, derived from the structured fields.
36
+ *
37
+ * @public
38
+ */
39
+ get message() {
40
+ return this.code === "unpairedMarker" ? `region "${this.token}" has an unpaired or out-of-order marker — its :start and :end must both be present, in order` : `region "${this.token}" appears more than once — each silk-release region must be a single pair`;
41
+ }
42
+ /**
43
+ * Every marker problem in `body`, or an empty array when the markers are
44
+ * well-formed (including entirely absent — an unmanaged body is not a
45
+ * defect).
46
+ *
47
+ * @remarks
48
+ * The references region is located by its attributed opening prefix, so a
49
+ * marker carrying an `owned="…"` attribute counts as present.
50
+ *
51
+ * @public
52
+ */
53
+ static scan(body) {
54
+ const diagnostics = [];
55
+ const tokens = [
56
+ ["silk-release", Markers.MANAGED_START],
57
+ ["silk-release:summary", Markers.SUMMARY_START],
58
+ ["silk-release:references", Markers.REFERENCES_START_PREFIX]
59
+ ];
60
+ for (const [token, openMarker] of tokens) {
61
+ const opens = countOccurrences(body, openMarker);
62
+ const closes = countOccurrences(body, Region.end(token));
63
+ if (opens === 0 && closes === 0) continue;
64
+ if (opens !== closes) {
65
+ diagnostics.push(PrBodyDiagnostic.make({
66
+ code: "unpairedMarker",
67
+ token
68
+ }));
69
+ continue;
70
+ }
71
+ if (opens > 1) {
72
+ diagnostics.push(PrBodyDiagnostic.make({
73
+ code: "duplicateMarker",
74
+ token
75
+ }));
76
+ continue;
77
+ }
78
+ if (body.indexOf(Region.end(token)) < body.indexOf(openMarker)) diagnostics.push(PrBodyDiagnostic.make({
79
+ code: "unpairedMarker",
80
+ token
81
+ }));
82
+ }
83
+ return diagnostics;
84
+ }
85
+ };
86
+ /**
87
+ * Non-overlapping occurrences of `needle` in `haystack`.
88
+ *
89
+ * @remarks
90
+ * Module-private on purpose — it exists only to keep `scan` readable.
91
+ */
92
+ const countOccurrences = (haystack, needle) => {
93
+ let count = 0;
94
+ let from = 0;
95
+ for (;;) {
96
+ const at = haystack.indexOf(needle, from);
97
+ if (at === -1) return count;
98
+ count += 1;
99
+ from = at + needle.length;
100
+ }
101
+ };
102
+
103
+ //#endregion
104
+ export { PrBodyDiagnostic, PrBodyDiagnosticCode };
@@ -0,0 +1,22 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { Region } from "./region.js";
3
+ import { Markers } from "./markers.js";
4
+ import { LinkedIssueRef } from "./linked-issue.js";
5
+ import { ClosingReferences, OwnedAttribute } from "./references.js";
6
+ import { ManagedPrBody } from "./body.js";
7
+ import { PrBodyDiagnostic, PrBodyDiagnosticCode } from "./diagnostics.js";
8
+
9
+ //#region src/pr-body/index.ts
10
+ var pr_body_exports = /* @__PURE__ */ __exportAll({
11
+ ClosingReferences: () => ClosingReferences,
12
+ LinkedIssueRef: () => LinkedIssueRef,
13
+ ManagedPrBody: () => ManagedPrBody,
14
+ Markers: () => Markers,
15
+ OwnedAttribute: () => OwnedAttribute,
16
+ PrBodyDiagnostic: () => PrBodyDiagnostic,
17
+ PrBodyDiagnosticCode: () => PrBodyDiagnosticCode,
18
+ Region: () => Region
19
+ });
20
+
21
+ //#endregion
22
+ export { ClosingReferences, LinkedIssueRef, ManagedPrBody, Markers, OwnedAttribute, PrBodyDiagnostic, PrBodyDiagnosticCode, Region, pr_body_exports };
@@ -0,0 +1,44 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/pr-body/linked-issue.ts
4
+ /**
5
+ * The minimum an issue must carry to appear in a managed PR body.
6
+ *
7
+ * @remarks
8
+ * `state` is deliberately a tolerant `Schema.String` rather than a literal
9
+ * union: GitHub's REST API reports `"open"`/`"closed"` while GraphQL reports
10
+ * `"OPEN"`/`"CLOSED"`, and both actions pass their existing issue shapes
11
+ * through unchanged. **`LinkedIssueRef.isClosed` is the ONLY sanctioned way
12
+ * to test closedness** — it lowercases before comparing, so both spellings
13
+ * classify correctly. A hand-written `issue.state === "closed"` comparison
14
+ * silently misclassifies GraphQL's `"CLOSED"` as open, which re-links (and on
15
+ * merge auto-closes) an issue the release deliberately dropped.
16
+ *
17
+ * The class carries no instance members, so a plain
18
+ * `{ number, title, state }` literal satisfies the type structurally — both
19
+ * actions' existing `LinkedIssue` shapes are accepted without mapping.
20
+ *
21
+ * @public
22
+ */
23
+ var LinkedIssueRef = class extends Schema.Class("LinkedIssueRef")({
24
+ number: Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)),
25
+ title: Schema.String,
26
+ state: Schema.String
27
+ }) {
28
+ /**
29
+ * Whether an issue is closed, case-insensitively.
30
+ *
31
+ * @remarks
32
+ * The only sanctioned closedness test — see the class remarks for why a
33
+ * bare `state === "closed"` comparison is a silent bug against GraphQL
34
+ * payloads.
35
+ *
36
+ * @public
37
+ */
38
+ static isClosed(issue) {
39
+ return issue.state.toLowerCase() === "closed";
40
+ }
41
+ };
42
+
43
+ //#endregion
44
+ export { LinkedIssueRef };
@@ -0,0 +1,103 @@
1
+ import { Region } from "./region.js";
2
+
3
+ //#region src/pr-body/markers.ts
4
+ /**
5
+ * The frozen `silk-release` marker vocabulary — the wire format of the shared
6
+ * PR-body contract.
7
+ *
8
+ * @remarks
9
+ * **The `silk-release:` token is frozen and names the CONTRACT, not the
10
+ * emitting action.** `silk-update-action` PRs carry the same markers as
11
+ * release PRs, deliberately: every live document, the `pr-body` plugin skill,
12
+ * and every agent that edits a managed PR description key on these exact
13
+ * byte sequences. Do not parameterize the token per action and do not rename
14
+ * it — either forks the wire format for zero gain and orphans every open PR
15
+ * (ruled in savvy-web/systems#419).
16
+ *
17
+ * These constants are the single source of truth for the marker grammar.
18
+ * The agent-facing documentation in the silk plugin (`pr-body` and
19
+ * `commit-create` skills) duplicates the literals for readability; a drift
20
+ * lint in this package's test suite asserts the copies stay in sync.
21
+ */
22
+ /**
23
+ * The marker constants of the `silk-release` PR-body contract.
24
+ *
25
+ * @public
26
+ */
27
+ var Markers = class {
28
+ constructor() {}
29
+ /**
30
+ * Opening marker of the whole managed region.
31
+ *
32
+ * @remarks
33
+ * Everything between this and {@link Markers.MANAGED_END} that is not
34
+ * inside the summary or references region is regenerated wholesale on
35
+ * every run; everything outside the pair is human territory and survives
36
+ * every regeneration.
37
+ *
38
+ * @public
39
+ */
40
+ static MANAGED_START = Region.start("silk-release");
41
+ /**
42
+ * Closing marker of the whole managed region.
43
+ *
44
+ * @public
45
+ */
46
+ static MANAGED_END = Region.end("silk-release");
47
+ /**
48
+ * Opening marker of the region an AI summariser owns.
49
+ *
50
+ * @remarks
51
+ * The generating action never writes into this region — it only reserves
52
+ * it and carries its content through on regeneration.
53
+ *
54
+ * @public
55
+ */
56
+ static SUMMARY_START = Region.start("silk-release:summary");
57
+ /**
58
+ * Closing marker of the summariser's region.
59
+ *
60
+ * @public
61
+ */
62
+ static SUMMARY_END = Region.end("silk-release:summary");
63
+ /**
64
+ * The PLAIN opening marker of the closing-reference region — the form an
65
+ * author writes by hand.
66
+ *
67
+ * @remarks
68
+ * A generating run emits the ATTRIBUTED form instead (the plain prefix
69
+ * plus an `owned="…"` attribute). Never locate the region by matching
70
+ * this constant — match {@link Markers.REFERENCES_START_PREFIX}, or a
71
+ * region a run wrote will not be found.
72
+ *
73
+ * @public
74
+ */
75
+ static REFERENCES_START = Region.start("silk-release:references");
76
+ /**
77
+ * Closing marker of the closing-reference region.
78
+ *
79
+ * @public
80
+ */
81
+ static REFERENCES_END = Region.end("silk-release:references");
82
+ /**
83
+ * The references opening marker up to its attributes, for locating a
84
+ * region whose `owned` list is unknown.
85
+ *
86
+ * @public
87
+ */
88
+ static REFERENCES_START_PREFIX = "<!-- silk-release:references:start";
89
+ /**
90
+ * The fence language for the proposed squash-commit block.
91
+ *
92
+ * @remarks
93
+ * Not a GFM language and apparently undocumented, but GitHub renders it.
94
+ * It is a target for AI integrations to read and rewrite into the
95
+ * eventual squash-commit message. **Do not "correct" it to `text`.**
96
+ *
97
+ * @public
98
+ */
99
+ static SQUASH_FENCE_LANGUAGE = "proposed-squash-commit";
100
+ };
101
+
102
+ //#endregion
103
+ export { Markers };
@@ -0,0 +1,159 @@
1
+ import { Markers } from "./markers.js";
2
+ import { LinkedIssueRef } from "./linked-issue.js";
3
+ import { Schema } from "effect";
4
+
5
+ //#region src/pr-body/references.ts
6
+ /**
7
+ * An ordered list of issue ids destined for closing references, with the two
8
+ * renderers whose difference is the whole point of this module.
9
+ *
10
+ * @remarks
11
+ * The same issues appear twice in a managed PR body, spelled differently, and
12
+ * **neither consumer accepts the other's spelling**:
13
+ *
14
+ * - commitlint reads ONE comma-joined trailer (`Closes #1, #2`) inside the
15
+ * proposed-squash-commit fence — {@link ClosingReferences.renderTrailer};
16
+ * - GitHub's linker reads one bare `Closes #N` line each, OUTSIDE every
17
+ * fence — {@link ClosingReferences.renderBareLines}. A reference inside a
18
+ * fenced block is inert to GitHub.
19
+ *
20
+ * The duplication is load-bearing. Never "simplify" a body by emitting one
21
+ * form in both places: comma-joined bare lines link nothing (verified by hand
22
+ * against live pull requests — `savvy-web/silk-integration` #242/#232 with no
23
+ * bare line reported `closingIssuesReferences: []`, #243 with one reported
24
+ * `[168]`), and per-line trailers inside the fence break the commit contract.
25
+ *
26
+ * Ids are stored exactly as given — construction neither deduplicates nor
27
+ * sorts. Call {@link ClosingReferences.dedupe} where uniqueness is wanted;
28
+ * the split exists because the squash trailer historically renders duplicates
29
+ * as-given while the references region deduplicates, and byte-compatibility
30
+ * with live PR bodies pins that behavior.
31
+ *
32
+ * @public
33
+ */
34
+ var ClosingReferences = class ClosingReferences extends Schema.Class("ClosingReferences")({ ids: Schema.Array(Schema.Number.check(Schema.isInt())) }) {
35
+ /**
36
+ * A closing keyword and its issue reference, anchored per line.
37
+ *
38
+ * @remarks
39
+ * Anchored so a number mentioned in passing is not mistaken for a closing
40
+ * reference — matching what GitHub itself links on. Every keyword GitHub
41
+ * accepts is matched, not just the present-tense plural this contract
42
+ * emits: `close`/`closed`, `fix`/`fixed`, `resolve`/`resolved` and an
43
+ * optional colon are all valid, and a reference the parser fails to
44
+ * recognise is one the next regeneration silently deletes.
45
+ */
46
+ static BARE_LINE_PATTERN = /^(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+#(\d+)$/i;
47
+ /**
48
+ * The open issues' ids, in input order, duplicates preserved.
49
+ *
50
+ * @remarks
51
+ * Closedness is decided by `LinkedIssueRef.isClosed` — the only
52
+ * sanctioned test, case-insensitive so REST (`closed`) and GraphQL
53
+ * (`CLOSED`) payloads classify identically.
54
+ *
55
+ * @public
56
+ */
57
+ static fromIssues(issues) {
58
+ return ClosingReferences.make({ ids: issues.filter((issue) => !LinkedIssueRef.isClosed(issue)).map((issue) => issue.number) });
59
+ }
60
+ /**
61
+ * Issue ids carried by a region's bare closing lines.
62
+ *
63
+ * @public
64
+ */
65
+ static parseBare(region) {
66
+ return region.split("\n").map((line) => ClosingReferences.BARE_LINE_PATTERN.exec(line.trim())?.[1]).filter((id) => id !== void 0).map(Number);
67
+ }
68
+ /**
69
+ * A copy with duplicate ids removed, first occurrence winning.
70
+ *
71
+ * @public
72
+ */
73
+ dedupe() {
74
+ return ClosingReferences.make({ ids: [...new Set(this.ids)] });
75
+ }
76
+ /**
77
+ * The comma-joined trailer the squash-commit message carries, or `""`
78
+ * when there is nothing to close.
79
+ *
80
+ * @remarks
81
+ * `Closes #1, #2` on ONE line — the spelling commitlint reads and
82
+ * GitHub's linker ignores. See the class remarks before changing either
83
+ * renderer.
84
+ *
85
+ * @public
86
+ */
87
+ renderTrailer() {
88
+ if (this.ids.length === 0) return "";
89
+ return `Closes ${this.ids.map((id) => `#${id}`).join(", ")}`;
90
+ }
91
+ /**
92
+ * One bare `Closes #N` line per id, or `""` when empty.
93
+ *
94
+ * @remarks
95
+ * The spelling GitHub's linker reads — each line must sit OUTSIDE every
96
+ * fenced block to link. See the class remarks before changing either
97
+ * renderer.
98
+ *
99
+ * @public
100
+ */
101
+ renderBareLines() {
102
+ return this.ids.map((id) => `Closes #${id}`).join("\n");
103
+ }
104
+ };
105
+ /**
106
+ * The `owned="…"` attribute on the references region's opening marker.
107
+ *
108
+ * @remarks
109
+ * Records the issue ids a generating run emitted itself, so the next run can
110
+ * tell its own references from ones an agent or human added. "Not in this
111
+ * run's linked set" is NOT enough: a reference the previous run emitted also
112
+ * disappears from the linked set when the release stops tracking that issue,
113
+ * and treating it as agent-authored would preserve it forever — re-linking,
114
+ * and on merge auto-closing, an issue the release deliberately dropped.
115
+ *
116
+ * **Never hand-edit the attribute.** A wrong value makes the next run delete
117
+ * a real reference or resurrect a dropped one.
118
+ *
119
+ * @public
120
+ */
121
+ var OwnedAttribute = class {
122
+ constructor() {}
123
+ /**
124
+ * The attribute as emitted on the opening marker.
125
+ *
126
+ * @public
127
+ */
128
+ static render(ids) {
129
+ return `owned="${ids.join(",")}"`;
130
+ }
131
+ /**
132
+ * The ids the prior body's opening marker claims as the previous run's
133
+ * own.
134
+ *
135
+ * @remarks
136
+ * An absent or malformed attribute reads as "none", which degrades to
137
+ * treating every reference in the region as agent-authored. That
138
+ * preserves too much rather than deleting someone's work — the safe
139
+ * direction to fail. The match is anchored to an attribute boundary: an
140
+ * unanchored match also finds `data-owned="…"` and `unowned="…"`, which
141
+ * would let an unrelated attribute claim an agent's reference and get it
142
+ * dropped on the next run.
143
+ *
144
+ * @public
145
+ */
146
+ static parse(priorBody) {
147
+ const from = priorBody.indexOf(Markers.REFERENCES_START_PREFIX);
148
+ if (from === -1) return /* @__PURE__ */ new Set();
149
+ const openEnd = priorBody.indexOf("-->", from);
150
+ if (openEnd === -1) return /* @__PURE__ */ new Set();
151
+ const attributes = priorBody.slice(from + Markers.REFERENCES_START_PREFIX.length, openEnd);
152
+ const owned = /(?:^|\s)owned="([\d,\s]*)"/.exec(attributes)?.[1];
153
+ if (owned === void 0) return /* @__PURE__ */ new Set();
154
+ return new Set(owned.split(",").map((part) => part.trim()).filter((part) => part !== "").map(Number));
155
+ }
156
+ };
157
+
158
+ //#endregion
159
+ export { ClosingReferences, OwnedAttribute };