@savvy-web/silk-effects 5.9.2 → 6.0.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.
@@ -1,65 +1,44 @@
1
+ import { collectReferenceLists, keywordFamily } from "@effected/github-references";
2
+
1
3
  //#region src/changesets/utils/issue-refs.ts
2
4
  /**
3
- * Matches "closes #123" or "close: #456, #789" patterns (case-insensitive).
4
- *
5
- * Capture group `[1]` contains the comma-separated issue number list.
6
- *
7
- * @internal
8
- */
9
- const CLOSES_ISSUE_PATTERN = /closes?:?\s*#?(\d+(?:, *#?\d+)*)/i;
10
- /**
11
- * Matches "fixes #123" or "fix: #456, #789" patterns (case-insensitive).
12
- *
13
- * Capture group `[1]` contains the comma-separated issue number list.
14
- *
15
- * @internal
16
- */
17
- const FIXES_ISSUE_PATTERN = /fix(?:es)?:?\s*#?(\d+(?:, *#?\d+)*)/i;
18
- /**
19
- * Matches "refs #123" or "ref: #456, #789" patterns (case-insensitive).
20
- *
21
- * Capture group `[1]` contains the comma-separated issue number list.
22
- *
23
- * @internal
24
- */
25
- const REFS_ISSUE_PATTERN = /refs?:?\s*#?(\d+(?:, *#?\d+)*)/i;
26
- /**
27
- * Pattern for splitting comma-separated issue numbers.
28
- *
29
- * @internal
30
- */
31
- const ISSUE_NUMBER_SPLIT_PATTERN = /, */;
32
- /**
33
- * Extract issue numbers from a regex match against a commit message.
5
+ * The output category each keyword family lands in.
34
6
  *
35
7
  * @remarks
36
- * Truncates the input to 10,000 characters before matching to prevent
37
- * ReDoS on adversarial input. Strips `#` prefixes from matched numbers.
38
- *
39
- * @param pattern - The regex pattern to match (must have a capture group for issue numbers)
40
- * @param message - The commit message body to search
41
- * @returns Array of issue number strings (without `#` prefix)
8
+ * The `resolve` family reads as `closes` since closing is what those keywords
9
+ * do; the record is total over `KeywordFamily`, so a family the kit adds
10
+ * without a category here is a type error, never a silent drop.
42
11
  *
43
12
  * @internal
44
13
  */
45
- function extractIssueNumbers(pattern, message) {
46
- const safeMessage = message.slice(0, 1e4);
47
- const match = pattern.exec(safeMessage);
48
- if (!match?.[1]) return [];
49
- return match[1].split(ISSUE_NUMBER_SPLIT_PATTERN).map((num) => num.replace("#", "").trim());
14
+ const CATEGORY_BY_FAMILY = {
15
+ close: "closes",
16
+ fix: "fixes",
17
+ ref: "refs",
18
+ resolve: "closes"
19
+ };
20
+ /** @internal */
21
+ function categoryOf(list) {
22
+ return CATEGORY_BY_FAMILY[keywordFamily(list.keyword)];
50
23
  }
51
24
  /**
52
25
  * Parse a commit message for GitHub issue references.
53
26
  *
54
- * Recognizes `Closes`, `Fixes`, and `Refs` keywords with various formats:
55
- * - `Closes #123`
56
- * - `Fixes: #456, #789`
57
- * - `Refs #101`
27
+ * Recognizes GitHub's closing keywords plus `Ref`/`Refs`/`References` in
28
+ * both list dialects:
29
+ * - whole-line trailers — `Closes #123`, `Fixes: #456, #789`, `Refs #101`;
30
+ * - inline in prose — `This closes #12 in passing`, or several lists on one
31
+ * line: `Closes #123, Fixes #456`.
58
32
  *
59
33
  * @remarks
60
- * Each keyword category is matched independently, so a single commit
61
- * message can contain references in all three categories. Keywords are
62
- * case-insensitive and accept both singular and plural forms.
34
+ * References accumulate in message order: two `Closes` lists contribute to
35
+ * the same category. Keywords are case-insensitive and the `#` is mandatory.
36
+ * The colon spelling is line-dialect-only; `collectReferenceLists` owns the
37
+ * per-line composition (whole-line trailer parse first, inline harvest
38
+ * otherwise), guaranteeing a trailer line is never double-counted. Malformed
39
+ * candidates are simply skipped. The kit preserves duplicates by contract;
40
+ * each category dedupes them here (first occurrence wins), mirroring the
41
+ * PR-body path, so a repeated reference renders one changelog link.
63
42
  *
64
43
  * @param commitMessage - The commit message body to parse
65
44
  * @returns Categorized issue references with numbers as strings (no `#` prefix)
@@ -68,20 +47,23 @@ function extractIssueNumbers(pattern, message) {
68
47
  * ```typescript
69
48
  * import { parseIssueReferences } from "../utils/issue-refs.js";
70
49
  *
71
- * const refs = parseIssueReferences("feat: add API\n\nCloses #42\nFixes: #17, #23");
50
+ * const refs = parseIssueReferences("feat: add API\n\nCloses #42, Fixes #17");
72
51
  * // refs.closes === ["42"]
73
- * // refs.fixes === ["17", "23"]
52
+ * // refs.fixes === ["17"]
74
53
  * // refs.refs === []
75
54
  * ```
76
55
  *
77
56
  * @internal
78
57
  */
79
58
  function parseIssueReferences(commitMessage) {
80
- return {
81
- closes: extractIssueNumbers(CLOSES_ISSUE_PATTERN, commitMessage),
82
- fixes: extractIssueNumbers(FIXES_ISSUE_PATTERN, commitMessage),
83
- refs: extractIssueNumbers(REFS_ISSUE_PATTERN, commitMessage)
59
+ const references = {
60
+ closes: [],
61
+ fixes: [],
62
+ refs: []
84
63
  };
64
+ for (const list of collectReferenceLists(commitMessage)) references[categoryOf(list)].push(...list.issueNumbers.map(String));
65
+ for (const category of Object.keys(references)) references[category] = [...new Set(references[category])];
66
+ return references;
85
67
  }
86
68
 
87
69
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { Effect } from "effect";
2
+ import { parseClosingLists } from "@effected/github-references";
2
3
 
3
4
  //#region src/commitlint/hook/rules/closes-trailer.ts
4
5
  /**
@@ -8,33 +9,22 @@ import { Effect } from "effect";
8
9
  * @internal
9
10
  */
10
11
  /**
11
- * A closing keyword followed by its full list of issue references.
12
+ * Whether some line of the message is a closing trailer naming `ticketId`.
12
13
  *
13
14
  * @remarks
14
- * The house commit format puts every issue on ONE comma-separated trailer
15
- * (`Closes #247, #248, #251`), so a pattern anchored on `keyword\s+#N` sees
16
- * only the first id and reports a missing trailer that is plainly present.
17
- * Capturing the whole list and scanning it is what makes the later ids count.
18
- *
19
- * `and` is accepted alongside the comma because humans write it; the optional
20
- * colon matches the `Closes: #N` form the validator's trailer pattern allows.
21
- *
22
- * @internal
23
- */
24
- const CLOSING_LIST_PATTERN = /\b(?:closes|fixes|resolves):?\s+(#\d+(?:\s*(?:,|and)\s*#\d+)*)/gi;
25
- /**
26
- * Reference ids within a captured closing list.
15
+ * The message is read with `@effected/github-references`' `parseClosingLists`:
16
+ * per line, the whole line, after trimming, must be a closing keyword (any of
17
+ * GitHub's nine tenses, optional colon) followed by its full `#N` list
18
+ * comma, `and`, or Oxford `, and` separated so every id in the house
19
+ * one-trailer format (`Closes #247, #248, #251`) counts, not just the first.
20
+ * A `#N` mentioned mid-prose never qualifies: a trailer is a line of its own,
21
+ * which is why this rule deliberately stays on the whole-line dialect rather
22
+ * than the inline harvest.
27
23
  *
28
24
  * @internal
29
25
  */
30
- const REFERENCE_PATTERN = /#(\d+)/g;
31
26
  function hasClosingTrailer(message, ticketId) {
32
- for (const keywordMatch of message.matchAll(CLOSING_LIST_PATTERN)) {
33
- const list = keywordMatch[1];
34
- if (list === void 0) continue;
35
- for (const reference of list.matchAll(REFERENCE_PATTERN)) if (Number(reference[1]) === ticketId) return true;
36
- }
37
- return false;
27
+ return parseClosingLists(message).some((list) => list.issueNumbers.includes(ticketId));
38
28
  }
39
29
  const closesTrailerRule = {
40
30
  id: "closes-trailer",
package/index.d.ts CHANGED
@@ -6497,6 +6497,21 @@ interface ClosesTrailerCtx {
6497
6497
  branchInfo: BranchInfo;
6498
6498
  openIssues: ReadonlyArray<OpenIssue>;
6499
6499
  }
6500
+ /**
6501
+ * Whether some line of the message is a closing trailer naming `ticketId`.
6502
+ *
6503
+ * @remarks
6504
+ * The message is read with `@effected/github-references`' `parseClosingLists`:
6505
+ * per line, the whole line, after trimming, must be a closing keyword (any of
6506
+ * GitHub's nine tenses, optional colon) followed by its full `#N` list —
6507
+ * comma, `and`, or Oxford `, and` separated — so every id in the house
6508
+ * one-trailer format (`Closes #247, #248, #251`) counts, not just the first.
6509
+ * A `#N` mentioned mid-prose never qualifies: a trailer is a line of its own,
6510
+ * which is why this rule deliberately stays on the whole-line dialect rather
6511
+ * than the inline harvest.
6512
+ *
6513
+ * @internal
6514
+ */
6500
6515
  declare function hasClosingTrailer(message: string, ticketId: number): boolean;
6501
6516
  declare const closesTrailerRule: Rule<ClosesTrailerInput, ClosesTrailerCtx>;
6502
6517
  //#endregion
@@ -8608,18 +8623,6 @@ declare const ClosingReferences_base: Schema.Class<ClosingReferences, Schema.Str
8608
8623
  * @public
8609
8624
  */
8610
8625
  declare class ClosingReferences extends ClosingReferences_base {
8611
- /**
8612
- * A closing keyword and its issue reference, anchored per line.
8613
- *
8614
- * @remarks
8615
- * Anchored so a number mentioned in passing is not mistaken for a closing
8616
- * reference — matching what GitHub itself links on. Every keyword GitHub
8617
- * accepts is matched, not just the present-tense plural this contract
8618
- * emits: `close`/`closed`, `fix`/`fixed`, `resolve`/`resolved` and an
8619
- * optional colon are all valid, and a reference the parser fails to
8620
- * recognise is one the next regeneration silently deletes.
8621
- */
8622
- static readonly BARE_LINE_PATTERN: RegExp;
8623
8626
  /**
8624
8627
  * The open issues' ids, in input order, duplicates preserved.
8625
8628
  *
@@ -8634,6 +8637,15 @@ declare class ClosingReferences extends ClosingReferences_base {
8634
8637
  /**
8635
8638
  * Issue ids carried by a region's bare closing lines.
8636
8639
  *
8640
+ * @remarks
8641
+ * The region is read with `@effected/github-references`' `parseBareLines`
8642
+ * — per line, the whole line, after trimming, must be
8643
+ * `<keyword>[:] #<number>`, so a number mentioned in passing is never
8644
+ * mistaken for a closing reference. Every keyword GitHub accepts counts,
8645
+ * not just the present-tense plural this contract emits, because a
8646
+ * reference the parser fails to recognise is one the next regeneration
8647
+ * silently deletes.
8648
+ *
8637
8649
  * @public
8638
8650
  */
8639
8651
  static parseBare(region: string): ReadonlyArray<number>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "5.9.2",
3
+ "version": "6.0.0",
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",
@@ -35,12 +35,13 @@
35
35
  "@changesets/get-release-plan": "^5.0.0",
36
36
  "@effected/commands": "^0.5.0",
37
37
  "@effected/git": "^0.9.0",
38
+ "@effected/github-references": "^0.1.0",
38
39
  "@effected/glob": "^0.4.0",
39
40
  "@effected/jsonc": "^0.7.0",
40
- "@effected/package-json": "^0.10.0",
41
- "@effected/templates": "^0.3.0",
41
+ "@effected/package-json": "^0.10.1",
42
+ "@effected/templates": "^0.4.0",
42
43
  "@effected/walker": "^0.5.0",
43
- "@effected/workspaces": "^0.14.0",
44
+ "@effected/workspaces": "^0.14.1",
44
45
  "@effected/yaml": "^0.10.0",
45
46
  "@manypkg/get-packages": "^3.1.0",
46
47
  "mdast-util-heading-range": "^4.0.0",
@@ -1,6 +1,7 @@
1
1
  import { Markers } from "./markers.js";
2
2
  import { LinkedIssueRef } from "./linked-issue.js";
3
3
  import { Schema } from "effect";
4
+ import { parseBareLines } from "@effected/github-references";
4
5
 
5
6
  //#region src/pr-body/references.ts
6
7
  /**
@@ -32,18 +33,6 @@ import { Schema } from "effect";
32
33
  * @public
33
34
  */
34
35
  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
36
  /**
48
37
  * The open issues' ids, in input order, duplicates preserved.
49
38
  *
@@ -60,10 +49,19 @@ var ClosingReferences = class ClosingReferences extends Schema.Class("ClosingRef
60
49
  /**
61
50
  * Issue ids carried by a region's bare closing lines.
62
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
+ *
63
61
  * @public
64
62
  */
65
63
  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);
64
+ return parseBareLines(region).map((ref) => ref.issueNumber);
67
65
  }
68
66
  /**
69
67
  * A copy with duplicate ids removed, first occurrence winning.