@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/LICENSE +21 -0
- package/README.md +47 -0
- package/_virtual/_rolldown/runtime.js +18 -0
- package/errors/BiomeSyncError.js +22 -0
- package/errors/ChangesetConfigError.js +21 -0
- package/errors/ConfigNotFoundError.js +22 -0
- package/errors/PublishTargetBindingError.js +30 -0
- package/errors/WorkspaceAnalysisError.js +22 -0
- package/index.d.ts +1261 -0
- package/index.js +15 -0
- package/package.json +39 -0
- package/pr-body/body.js +145 -0
- package/pr-body/diagnostics.js +104 -0
- package/pr-body/index.js +22 -0
- package/pr-body/linked-issue.js +44 -0
- package/pr-body/markers.js +103 -0
- package/pr-body/references.js +157 -0
- package/pr-body/region.js +105 -0
- package/schemas/BiomeConfig.js +36 -0
- package/schemas/ConfigDiscoverySchemas.js +50 -0
- package/schemas/SavvyInstallSection.js +232 -0
- package/schemas/SavvySections.js +194 -0
- package/schemas/VersioningSchemas.js +87 -0
- package/schemas/WorkspaceAnalysisSchemas.js +209 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/TrailingSlash.js +21 -0
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
|
+
}
|
package/pr-body/body.js
ADDED
|
@@ -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 };
|
package/pr-body/index.js
ADDED
|
@@ -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 };
|