@savvy-web/silk-core 0.1.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.
package/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { BiomeSyncError } from "./errors/BiomeSyncError.js";
2
+ import { ChangesetConfigError } from "./errors/ChangesetConfigError.js";
3
+ import { ConfigNotFoundError } from "./errors/ConfigNotFoundError.js";
4
+ import { PublishTargetBindingError } from "./errors/PublishTargetBindingError.js";
5
+ import { WorkspaceAnalysisError } from "./errors/WorkspaceAnalysisError.js";
6
+ import { pr_body_exports } from "./pr-body/index.js";
7
+ import { BiomeSyncOptions, BiomeSyncResult } from "./schemas/BiomeConfig.js";
8
+ import { ConfigDiscoveryOptions, ConfigLocation, ConfigSource } from "./schemas/ConfigDiscoverySchemas.js";
9
+ import { LIFECYCLE_SCRIPTS_CONFIG_KEY, SavvyInstallSection, publishesBuiltLinkDirectory, savvyInstallBlock, savvyInstallDeps } from "./schemas/SavvyInstallSection.js";
10
+ import { SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyToolSection, savvyToolchainCheck } from "./schemas/SavvySections.js";
11
+ import { ChangesetConfigFile, SilkChangesetConfigFile } from "./schemas/VersioningSchemas.js";
12
+ import { trimTrailingSlashes } from "./utils/TrailingSlash.js";
13
+ import { AnalyzedWorkspace, SilkPublishConfig, WorkspaceAnalysis } from "./schemas/WorkspaceAnalysisSchemas.js";
14
+
15
+ export { AnalyzedWorkspace, BiomeSyncError, BiomeSyncOptions, BiomeSyncResult, ChangesetConfigError, ChangesetConfigFile, ConfigDiscoveryOptions, ConfigLocation, ConfigNotFoundError, ConfigSource, LIFECYCLE_SCRIPTS_CONFIG_KEY, pr_body_exports as PrBody, PublishTargetBindingError, SavvyBaseSection, SavvyHooksSection, SavvyInstallSection, SavvyToolchainSection, SilkChangesetConfigFile, SilkPublishConfig, WorkspaceAnalysis, WorkspaceAnalysisError, publishesBuiltLinkDirectory, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyInstallDeps, savvyToolSection, savvyToolchainCheck, trimTrailingSlashes };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@savvy-web/silk-core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Platform-free domain core of the Silk Suite: schemas, tagged errors, and the PR-body contract",
6
+ "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-core",
7
+ "bugs": {
8
+ "url": "https://github.com/savvy-web/systems/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/savvy-web/systems.git",
13
+ "directory": "packages/silk-core"
14
+ },
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "C. Spencer Beggs",
18
+ "email": "spencer@savvyweb.systems",
19
+ "url": "https://savvyweb.systems"
20
+ },
21
+ "sideEffects": false,
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./index.d.ts",
26
+ "import": "./index.js",
27
+ "default": "./index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "dependencies": {
32
+ "@effected/github-references": "^0.3.0",
33
+ "@effected/templates": "^0.6.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@effected/workspaces": "^0.21.0",
37
+ "effect": "4.0.0-rc.115"
38
+ }
39
+ }
@@ -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,157 @@
1
+ import { Markers } from "./markers.js";
2
+ import { LinkedIssueRef } from "./linked-issue.js";
3
+ import { Schema } from "effect";
4
+ import { parseBareLines } from "@effected/github-references";
5
+
6
+ //#region src/pr-body/references.ts
7
+ /**
8
+ * An ordered list of issue ids destined for closing references, with the two
9
+ * renderers whose difference is the whole point of this module.
10
+ *
11
+ * @remarks
12
+ * The same issues appear twice in a managed PR body, spelled differently, and
13
+ * **neither consumer accepts the other's spelling**:
14
+ *
15
+ * - commitlint reads ONE comma-joined trailer (`Closes #1, #2`) inside the
16
+ * proposed-squash-commit fence — {@link ClosingReferences.renderTrailer};
17
+ * - GitHub's linker reads one bare `Closes #N` line each, OUTSIDE every
18
+ * fence — {@link ClosingReferences.renderBareLines}. A reference inside a
19
+ * fenced block is inert to GitHub.
20
+ *
21
+ * The duplication is load-bearing. Never "simplify" a body by emitting one
22
+ * form in both places: comma-joined bare lines link nothing (verified by hand
23
+ * against live pull requests — `savvy-web/silk-integration` #242/#232 with no
24
+ * bare line reported `closingIssuesReferences: []`, #243 with one reported
25
+ * `[168]`), and per-line trailers inside the fence break the commit contract.
26
+ *
27
+ * Ids are stored exactly as given — construction neither deduplicates nor
28
+ * sorts. Call {@link ClosingReferences.dedupe} where uniqueness is wanted;
29
+ * the split exists because the squash trailer historically renders duplicates
30
+ * as-given while the references region deduplicates, and byte-compatibility
31
+ * with live PR bodies pins that behavior.
32
+ *
33
+ * @public
34
+ */
35
+ var ClosingReferences = class ClosingReferences extends Schema.Class("ClosingReferences")({ ids: Schema.Array(Schema.Number.check(Schema.isInt())) }) {
36
+ /**
37
+ * The open issues' ids, in input order, duplicates preserved.
38
+ *
39
+ * @remarks
40
+ * Closedness is decided by `LinkedIssueRef.isClosed` — the only
41
+ * sanctioned test, case-insensitive so REST (`closed`) and GraphQL
42
+ * (`CLOSED`) payloads classify identically.
43
+ *
44
+ * @public
45
+ */
46
+ static fromIssues(issues) {
47
+ return ClosingReferences.make({ ids: issues.filter((issue) => !LinkedIssueRef.isClosed(issue)).map((issue) => issue.number) });
48
+ }
49
+ /**
50
+ * Issue ids carried by a region's bare closing lines.
51
+ *
52
+ * @remarks
53
+ * The region is read with `@effected/github-references`' `parseBareLines`
54
+ * — per line, the whole line, after trimming, must be
55
+ * `<keyword>[:] #<number>`, so a number mentioned in passing is never
56
+ * mistaken for a closing reference. Every keyword GitHub accepts counts,
57
+ * not just the present-tense plural this contract emits, because a
58
+ * reference the parser fails to recognise is one the next regeneration
59
+ * silently deletes.
60
+ *
61
+ * @public
62
+ */
63
+ static parseBare(region) {
64
+ return parseBareLines(region).map((ref) => ref.issueNumber);
65
+ }
66
+ /**
67
+ * A copy with duplicate ids removed, first occurrence winning.
68
+ *
69
+ * @public
70
+ */
71
+ dedupe() {
72
+ return ClosingReferences.make({ ids: [...new Set(this.ids)] });
73
+ }
74
+ /**
75
+ * The comma-joined trailer the squash-commit message carries, or `""`
76
+ * when there is nothing to close.
77
+ *
78
+ * @remarks
79
+ * `Closes #1, #2` on ONE line — the spelling commitlint reads and
80
+ * GitHub's linker ignores. See the class remarks before changing either
81
+ * renderer.
82
+ *
83
+ * @public
84
+ */
85
+ renderTrailer() {
86
+ if (this.ids.length === 0) return "";
87
+ return `Closes ${this.ids.map((id) => `#${id}`).join(", ")}`;
88
+ }
89
+ /**
90
+ * One bare `Closes #N` line per id, or `""` when empty.
91
+ *
92
+ * @remarks
93
+ * The spelling GitHub's linker reads — each line must sit OUTSIDE every
94
+ * fenced block to link. See the class remarks before changing either
95
+ * renderer.
96
+ *
97
+ * @public
98
+ */
99
+ renderBareLines() {
100
+ return this.ids.map((id) => `Closes #${id}`).join("\n");
101
+ }
102
+ };
103
+ /**
104
+ * The `owned="…"` attribute on the references region's opening marker.
105
+ *
106
+ * @remarks
107
+ * Records the issue ids a generating run emitted itself, so the next run can
108
+ * tell its own references from ones an agent or human added. "Not in this
109
+ * run's linked set" is NOT enough: a reference the previous run emitted also
110
+ * disappears from the linked set when the release stops tracking that issue,
111
+ * and treating it as agent-authored would preserve it forever — re-linking,
112
+ * and on merge auto-closing, an issue the release deliberately dropped.
113
+ *
114
+ * **Never hand-edit the attribute.** A wrong value makes the next run delete
115
+ * a real reference or resurrect a dropped one.
116
+ *
117
+ * @public
118
+ */
119
+ var OwnedAttribute = class {
120
+ constructor() {}
121
+ /**
122
+ * The attribute as emitted on the opening marker.
123
+ *
124
+ * @public
125
+ */
126
+ static render(ids) {
127
+ return `owned="${ids.join(",")}"`;
128
+ }
129
+ /**
130
+ * The ids the prior body's opening marker claims as the previous run's
131
+ * own.
132
+ *
133
+ * @remarks
134
+ * An absent or malformed attribute reads as "none", which degrades to
135
+ * treating every reference in the region as agent-authored. That
136
+ * preserves too much rather than deleting someone's work — the safe
137
+ * direction to fail. The match is anchored to an attribute boundary: an
138
+ * unanchored match also finds `data-owned="…"` and `unowned="…"`, which
139
+ * would let an unrelated attribute claim an agent's reference and get it
140
+ * dropped on the next run.
141
+ *
142
+ * @public
143
+ */
144
+ static parse(priorBody) {
145
+ const from = priorBody.indexOf(Markers.REFERENCES_START_PREFIX);
146
+ if (from === -1) return /* @__PURE__ */ new Set();
147
+ const openEnd = priorBody.indexOf("-->", from);
148
+ if (openEnd === -1) return /* @__PURE__ */ new Set();
149
+ const attributes = priorBody.slice(from + Markers.REFERENCES_START_PREFIX.length, openEnd);
150
+ const owned = /(?:^|\s)owned="([\d,\s]*)"/.exec(attributes)?.[1];
151
+ if (owned === void 0) return /* @__PURE__ */ new Set();
152
+ return new Set(owned.split(",").map((part) => part.trim()).filter((part) => part !== "").map(Number));
153
+ }
154
+ };
155
+
156
+ //#endregion
157
+ export { ClosingReferences, OwnedAttribute };