@savvy-web/changesets 0.3.0 → 0.4.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/README.md CHANGED
@@ -14,11 +14,12 @@ Custom changelog formatter and markdown processing pipeline for the Silk Suite.
14
14
  - **GitHub integration** -- Automatic PR links, commit references, and contributor attribution
15
15
  - **Version file syncing** -- Bump version fields in additional JSON files using glob patterns and JSONPath expressions
16
16
  - **Editor support** -- markdownlint rules for real-time validation in VS Code and CI
17
+ - **AI-agent-friendly errors** -- All lint and validation errors include inline fix instructions and documentation links, so AI agents can resolve issues without examining source code
17
18
 
18
19
  ## Installation
19
20
 
20
21
  ```bash
21
- pnpm add @savvy-web/changesets
22
+ npm install @savvy-web/changesets -D
22
23
  ```
23
24
 
24
25
  ## Quick Start
@@ -64,6 +65,7 @@ Added a new authentication system with OAuth2 support.
64
65
  - [CLI Reference](./docs/cli.md) -- All commands and options
65
66
  - [API Reference](./docs/api.md) -- Classes, types, Effect services, remark plugins
66
67
  - [Architecture](./docs/architecture.md) -- Three-layer pipeline design and export map
68
+ - [Markdownlint Rule Docs](./docs/rules/) -- Per-rule documentation with examples, fix instructions, and rationale
67
69
 
68
70
  ## License
69
71
 
package/cjs/changelog.cjs CHANGED
@@ -181,7 +181,7 @@ var __webpack_modules__ = {
181
181
  }
182
182
  }
183
183
  const UsernameSchema = effect__rspack_import_0.Schema.String.pipe(effect__rspack_import_0.Schema.pattern(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, {
184
- message: ()=>"Invalid GitHub username format"
184
+ message: ()=>'Invalid GitHub username format. Usernames must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen. Example: "octocat" or "my-user-123"'
185
185
  }));
186
186
  const IssueNumberSchema = _primitives_js__rspack_import_1.e.annotations({
187
187
  title: "IssueNumber",
@@ -192,7 +192,7 @@ var __webpack_modules__ = {
192
192
  const match = _utils_markdown_link_js__rspack_import_2.o.exec(value);
193
193
  return match?.[2] ? isValidUrl(match[2]) : false;
194
194
  }, {
195
- message: ()=>"Invalid URL or markdown link format"
195
+ message: ()=>'Value must be a valid URL or a markdown link. Expected a plain URL (e.g., "https://github.com/owner/repo/pull/42") or a markdown link (e.g., "[#42](https://github.com/owner/repo/pull/42)")'
196
196
  }));
197
197
  const GitHubInfoSchema = effect__rspack_import_0.Schema.Struct({
198
198
  user: effect__rspack_import_0.Schema.optional(UsernameSchema),
package/cjs/index.cjs CHANGED
@@ -490,7 +490,7 @@ var __webpack_modules__ = {
490
490
  }
491
491
  }
492
492
  const UsernameSchema = effect__rspack_import_0.Schema.String.pipe(effect__rspack_import_0.Schema.pattern(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, {
493
- message: ()=>"Invalid GitHub username format"
493
+ message: ()=>'Invalid GitHub username format. Usernames must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen. Example: "octocat" or "my-user-123"'
494
494
  }));
495
495
  const IssueNumberSchema = _primitives_js__rspack_import_1.e.annotations({
496
496
  title: "IssueNumber",
@@ -501,7 +501,7 @@ var __webpack_modules__ = {
501
501
  const match = _utils_markdown_link_js__rspack_import_2.o.exec(value);
502
502
  return match?.[2] ? isValidUrl(match[2]) : false;
503
503
  }, {
504
- message: ()=>"Invalid URL or markdown link format"
504
+ message: ()=>'Value must be a valid URL or a markdown link. Expected a plain URL (e.g., "https://github.com/owner/repo/pull/42") or a markdown link (e.g., "[#42](https://github.com/owner/repo/pull/42)")'
505
505
  }));
506
506
  const GitHubInfoSchema = effect__rspack_import_0.Schema.Struct({
507
507
  user: effect__rspack_import_0.Schema.optional(UsernameSchema),
@@ -830,25 +830,32 @@ var __webpack_exports__ = {};
830
830
  var external_mdast_util_to_string_ = __webpack_require__("mdast-util-to-string");
831
831
  const external_unified_lint_rule_namespaceObject = require("unified-lint-rule");
832
832
  const external_unist_util_visit_namespaceObject = require("unist-util-visit");
833
+ const DOCS_BASE = "https://github.com/savvy-web/changesets/blob/main/docs/rules";
834
+ const RULE_DOCS = {
835
+ CSH001: `${DOCS_BASE}/CSH001.md`,
836
+ CSH002: `${DOCS_BASE}/CSH002.md`,
837
+ CSH003: `${DOCS_BASE}/CSH003.md`,
838
+ CSH004: `${DOCS_BASE}/CSH004.md`
839
+ };
833
840
  const ContentStructureRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-content-structure", (tree, file)=>{
834
841
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node, index, parent)=>{
835
842
  if (2 !== node.depth || null == parent || null == index) return;
836
843
  const next = parent.children[index + 1];
837
- if (!next || "heading" === next.type && 2 === next.depth) file.message("Empty section: heading has no content before the next section or end of file", node);
844
+ if (!next || "heading" === next.type && 2 === next.depth) file.message(`Empty section: heading has no content before the next section or end of file. Add a list of changes (e.g., "- Added feature X") under this heading, or remove the empty heading. See: ${RULE_DOCS.CSH003}`, node);
838
845
  });
839
846
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "code", (node)=>{
840
- if (!node.lang) file.message("Code block is missing a language identifier", node);
847
+ if (!node.lang) file.message(`Code block is missing a language identifier. Add a language after the opening fence (e.g., \`\`\`ts, \`\`\`json, \`\`\`bash). See: ${RULE_DOCS.CSH003}`, node);
841
848
  });
842
849
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "listItem", (node)=>{
843
850
  const text = (0, external_mdast_util_to_string_.toString)(node).trim();
844
- if (!text) file.message("Empty list item", node);
851
+ if (!text) file.message(`Empty list item. Each list item must contain descriptive text (e.g., "- Fixed login timeout issue"). See: ${RULE_DOCS.CSH003}`, node);
845
852
  });
846
853
  });
847
854
  const HeadingHierarchyRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-heading-hierarchy", (tree, file)=>{
848
855
  let prevDepth = 0;
849
856
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node)=>{
850
- if (1 === node.depth) return void file.message("h1 headings are not allowed in changeset files", node);
851
- if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}`, node);
857
+ if (1 === node.depth) return void file.message(`h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`, node);
858
+ if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`, node);
852
859
  prevDepth = node.depth;
853
860
  });
854
861
  });
@@ -856,7 +863,7 @@ var __webpack_exports__ = {};
856
863
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node)=>{
857
864
  if (2 !== node.depth) return;
858
865
  const text = (0, external_mdast_util_to_string_.toString)(node);
859
- if (!(0, categories.Lr)(text)) file.message(`Unknown section "${text}". Valid sections: ${(0, categories.Rr)().join(", ")}`, node);
866
+ if (!(0, categories.Lr)(text)) file.message(`Unknown section "${text}". Valid h2 headings are: ${(0, categories.Rr)().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`, node);
860
867
  });
861
868
  });
862
869
  const IGNORED_TYPES = new Set([
@@ -869,7 +876,7 @@ var __webpack_exports__ = {};
869
876
  const UncategorizedContentRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-uncategorized-content", (tree, file)=>{
870
877
  for (const node of tree.children){
871
878
  if ("heading" === node.type && 2 === node.depth) break;
872
- if (isContentNode(node)) file.message("Content must be placed under a category heading (## heading)", node);
879
+ if (isContentNode(node)) file.message(`Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`, node);
873
880
  }
874
881
  });
875
882
  function stripFrontmatter(content) {
@@ -1228,14 +1235,14 @@ var __webpack_exports__ = {};
1228
1235
  description: external_effect_.Schema.String
1229
1236
  });
1230
1237
  const CommitHashSchema = external_effect_.Schema.String.pipe(external_effect_.Schema.pattern(/^[a-f0-9]{7,}$/, {
1231
- message: ()=>"Commit hash must be at least 7 hexadecimal characters"
1238
+ message: ()=>'Commit hash must be 7 or more lowercase hexadecimal characters (0-9, a-f). Example: "a1b2c3d" or a full 40-character SHA like "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2". Uppercase letters are not allowed'
1232
1239
  }));
1233
1240
  const VersionTypeSchema = external_effect_.Schema.Literal("major", "minor", "patch", "none");
1234
1241
  var primitives = __webpack_require__("./src/schemas/primitives.ts");
1235
1242
  const ChangesetSummarySchema = external_effect_.Schema.String.pipe(external_effect_.Schema.minLength(1, {
1236
- message: ()=>"Changeset summary cannot be empty"
1243
+ message: ()=>'Changeset summary cannot be empty. Provide a 1-1000 character description of the change (e.g., "Fix authentication timeout in login flow")'
1237
1244
  }), external_effect_.Schema.maxLength(1000, {
1238
- message: ()=>"Changeset summary is too long"
1245
+ message: ()=>"Changeset summary exceeds the 1000 character limit. Shorten the summary to at most 1000 characters — use the changeset body for additional details"
1239
1246
  }));
1240
1247
  const ChangesetSchema = external_effect_.Schema.Struct({
1241
1248
  summary: ChangesetSummarySchema,
@@ -30,6 +30,13 @@ __webpack_require__.d(__webpack_exports__, {
30
30
  default: ()=>markdownlint,
31
31
  RequiredSectionsRule: ()=>RequiredSectionsRule
32
32
  });
33
+ const DOCS_BASE = "https://github.com/savvy-web/changesets/blob/main/docs/rules";
34
+ const RULE_DOCS = {
35
+ CSH001: `${DOCS_BASE}/CSH001.md`,
36
+ CSH002: `${DOCS_BASE}/CSH002.md`,
37
+ CSH003: `${DOCS_BASE}/CSH003.md`,
38
+ CSH004: `${DOCS_BASE}/CSH004.md`
39
+ };
33
40
  function getHeadingLevel(heading) {
34
41
  const sequence = heading.children.find((c)=>"atxHeadingSequence" === c.type);
35
42
  return sequence ? sequence.text.length : 0;
@@ -64,7 +71,7 @@ const ContentStructureRule = {
64
71
  const nextIdx = i + 1 < h2Indices.length ? h2Indices[i + 1] : tokens.length;
65
72
  if (!hasContentBetween(tokens, currentIdx, nextIdx)) onError({
66
73
  lineNumber: tokens[currentIdx].startLine,
67
- detail: "Empty section: heading has no content before the next section or end of file"
74
+ detail: `Empty section: heading has no content before the next section or end of file. Add a list of changes (e.g., "- Added feature X") under this heading, or remove the empty heading. See: ${RULE_DOCS.CSH003}`
68
75
  });
69
76
  }
70
77
  for (const token of tokens){
@@ -73,7 +80,7 @@ const ContentStructureRule = {
73
80
  const hasInfo = openingFence?.children.some((c)=>"codeFencedFenceInfo" === c.type) ?? false;
74
81
  if (!hasInfo) onError({
75
82
  lineNumber: token.startLine,
76
- detail: "Code block is missing a language identifier"
83
+ detail: `Code block is missing a language identifier. Add a language after the opening fence (e.g., \`\`\`ts, \`\`\`json, \`\`\`bash). See: ${RULE_DOCS.CSH003}`
77
84
  });
78
85
  }
79
86
  for (const token of tokens){
@@ -91,7 +98,7 @@ const ContentStructureRule = {
91
98
  }
92
99
  if (!hasContent) onError({
93
100
  lineNumber: children[i].startLine,
94
- detail: "Empty list item"
101
+ detail: `Empty list item. Each list item must contain descriptive text (e.g., "- Fixed login timeout issue"). See: ${RULE_DOCS.CSH003}`
95
102
  });
96
103
  }
97
104
  }
@@ -115,13 +122,13 @@ const HeadingHierarchyRule = {
115
122
  if (1 === depth) {
116
123
  onError({
117
124
  lineNumber: token.startLine,
118
- detail: "h1 headings are not allowed in changeset files"
125
+ detail: `h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`
119
126
  });
120
127
  continue;
121
128
  }
122
129
  if (prevDepth > 0 && depth > prevDepth + 1) onError({
123
130
  lineNumber: token.startLine,
124
- detail: `Heading level skipped: expected h${prevDepth + 1} or lower, found h${depth}`
131
+ detail: `Heading level skipped: expected h${prevDepth + 1} or lower, found h${depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`
125
132
  });
126
133
  prevDepth = depth;
127
134
  }
@@ -272,7 +279,7 @@ const RequiredSectionsRule = {
272
279
  const text = getHeadingText(token);
273
280
  if (!isValidHeading(text)) onError({
274
281
  lineNumber: token.startLine,
275
- detail: `Unknown section "${text}". Valid sections: ${allHeadings().join(", ")}`
282
+ detail: `Unknown section "${text}". Valid h2 headings are: ${allHeadings().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`
276
283
  });
277
284
  }
278
285
  }
@@ -294,7 +301,7 @@ const UncategorizedContentRule = {
294
301
  if ("lineEnding" !== token.type && "lineEndingBlank" !== token.type && "htmlFlow" !== token.type) {
295
302
  if ("atxHeading" !== token.type) onError({
296
303
  lineNumber: token.startLine,
297
- detail: "Content must be placed under a category heading (## heading)"
304
+ detail: `Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`
298
305
  });
299
306
  }
300
307
  }
package/cjs/remark.cjs CHANGED
@@ -474,25 +474,32 @@ const ReorderSectionsPlugin = ()=>(tree)=>{
474
474
  }
475
475
  };
476
476
  const external_unified_lint_rule_namespaceObject = require("unified-lint-rule");
477
+ const DOCS_BASE = "https://github.com/savvy-web/changesets/blob/main/docs/rules";
478
+ const RULE_DOCS = {
479
+ CSH001: `${DOCS_BASE}/CSH001.md`,
480
+ CSH002: `${DOCS_BASE}/CSH002.md`,
481
+ CSH003: `${DOCS_BASE}/CSH003.md`,
482
+ CSH004: `${DOCS_BASE}/CSH004.md`
483
+ };
477
484
  const ContentStructureRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-content-structure", (tree, file)=>{
478
485
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node, index, parent)=>{
479
486
  if (2 !== node.depth || null == parent || null == index) return;
480
487
  const next = parent.children[index + 1];
481
- if (!next || "heading" === next.type && 2 === next.depth) file.message("Empty section: heading has no content before the next section or end of file", node);
488
+ if (!next || "heading" === next.type && 2 === next.depth) file.message(`Empty section: heading has no content before the next section or end of file. Add a list of changes (e.g., "- Added feature X") under this heading, or remove the empty heading. See: ${RULE_DOCS.CSH003}`, node);
482
489
  });
483
490
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "code", (node)=>{
484
- if (!node.lang) file.message("Code block is missing a language identifier", node);
491
+ if (!node.lang) file.message(`Code block is missing a language identifier. Add a language after the opening fence (e.g., \`\`\`ts, \`\`\`json, \`\`\`bash). See: ${RULE_DOCS.CSH003}`, node);
485
492
  });
486
493
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "listItem", (node)=>{
487
494
  const text = (0, external_mdast_util_to_string_namespaceObject.toString)(node).trim();
488
- if (!text) file.message("Empty list item", node);
495
+ if (!text) file.message(`Empty list item. Each list item must contain descriptive text (e.g., "- Fixed login timeout issue"). See: ${RULE_DOCS.CSH003}`, node);
489
496
  });
490
497
  });
491
498
  const HeadingHierarchyRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-heading-hierarchy", (tree, file)=>{
492
499
  let prevDepth = 0;
493
500
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node)=>{
494
- if (1 === node.depth) return void file.message("h1 headings are not allowed in changeset files", node);
495
- if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}`, node);
501
+ if (1 === node.depth) return void file.message(`h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`, node);
502
+ if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`, node);
496
503
  prevDepth = node.depth;
497
504
  });
498
505
  });
@@ -500,7 +507,7 @@ const RequiredSectionsRule = (0, external_unified_lint_rule_namespaceObject.lint
500
507
  (0, external_unist_util_visit_namespaceObject.visit)(tree, "heading", (node)=>{
501
508
  if (2 !== node.depth) return;
502
509
  const text = (0, external_mdast_util_to_string_namespaceObject.toString)(node);
503
- if (!isValidHeading(text)) file.message(`Unknown section "${text}". Valid sections: ${allHeadings().join(", ")}`, node);
510
+ if (!isValidHeading(text)) file.message(`Unknown section "${text}". Valid h2 headings are: ${allHeadings().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`, node);
504
511
  });
505
512
  });
506
513
  const IGNORED_TYPES = new Set([
@@ -513,7 +520,7 @@ function isContentNode(node) {
513
520
  const UncategorizedContentRule = (0, external_unified_lint_rule_namespaceObject.lintRule)("remark-lint:changeset-uncategorized-content", (tree, file)=>{
514
521
  for (const node of tree.children){
515
522
  if ("heading" === node.type && 2 === node.depth) break;
516
- if (isContentNode(node)) file.message("Content must be placed under a category heading (## heading)", node);
523
+ if (isContentNode(node)) file.message(`Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`, node);
517
524
  }
518
525
  });
519
526
  const SilkChangesetPreset = [
package/esm/160.js CHANGED
@@ -143,7 +143,7 @@ function isValidUrl(value) {
143
143
  }
144
144
  }
145
145
  const UsernameSchema = Schema.String.pipe(Schema.pattern(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, {
146
- message: ()=>"Invalid GitHub username format"
146
+ message: ()=>'Invalid GitHub username format. Usernames must contain only alphanumeric characters and hyphens, and cannot start or end with a hyphen. Example: "octocat" or "my-user-123"'
147
147
  }));
148
148
  const IssueNumberSchema = PositiveInteger.annotations({
149
149
  title: "IssueNumber",
@@ -154,7 +154,7 @@ const UrlOrMarkdownLinkSchema = Schema.String.pipe(Schema.filter((value)=>{
154
154
  const match = MARKDOWN_LINK_PATTERN.exec(value);
155
155
  return match?.[2] ? isValidUrl(match[2]) : false;
156
156
  }, {
157
- message: ()=>"Invalid URL or markdown link format"
157
+ message: ()=>'Value must be a valid URL or a markdown link. Expected a plain URL (e.g., "https://github.com/owner/repo/pull/42") or a markdown link (e.g., "[#42](https://github.com/owner/repo/pull/42)")'
158
158
  }));
159
159
  const GitHubInfoSchema = Schema.Struct({
160
160
  user: Schema.optional(UsernameSchema),
package/esm/260.js ADDED
@@ -0,0 +1,8 @@
1
+ const DOCS_BASE = "https://github.com/savvy-web/changesets/blob/main/docs/rules";
2
+ const RULE_DOCS = {
3
+ CSH001: `${DOCS_BASE}/CSH001.md`,
4
+ CSH002: `${DOCS_BASE}/CSH002.md`,
5
+ CSH003: `${DOCS_BASE}/CSH003.md`,
6
+ CSH004: `${DOCS_BASE}/CSH004.md`
7
+ };
8
+ export { RULE_DOCS };
package/esm/622.js CHANGED
@@ -1,26 +1,27 @@
1
1
  import { lintRule } from "unified-lint-rule";
2
2
  import { SKIP, visit } from "unist-util-visit";
3
3
  import { external_mdast_util_to_string_toString } from "./689.js";
4
+ import { RULE_DOCS } from "./260.js";
4
5
  import { isValidHeading, fromHeading, allHeadings } from "./60.js";
5
6
  const ContentStructureRule = lintRule("remark-lint:changeset-content-structure", (tree, file)=>{
6
7
  visit(tree, "heading", (node, index, parent)=>{
7
8
  if (2 !== node.depth || null == parent || null == index) return;
8
9
  const next = parent.children[index + 1];
9
- if (!next || "heading" === next.type && 2 === next.depth) file.message("Empty section: heading has no content before the next section or end of file", node);
10
+ if (!next || "heading" === next.type && 2 === next.depth) file.message(`Empty section: heading has no content before the next section or end of file. Add a list of changes (e.g., "- Added feature X") under this heading, or remove the empty heading. See: ${RULE_DOCS.CSH003}`, node);
10
11
  });
11
12
  visit(tree, "code", (node)=>{
12
- if (!node.lang) file.message("Code block is missing a language identifier", node);
13
+ if (!node.lang) file.message(`Code block is missing a language identifier. Add a language after the opening fence (e.g., \`\`\`ts, \`\`\`json, \`\`\`bash). See: ${RULE_DOCS.CSH003}`, node);
13
14
  });
14
15
  visit(tree, "listItem", (node)=>{
15
16
  const text = external_mdast_util_to_string_toString(node).trim();
16
- if (!text) file.message("Empty list item", node);
17
+ if (!text) file.message(`Empty list item. Each list item must contain descriptive text (e.g., "- Fixed login timeout issue"). See: ${RULE_DOCS.CSH003}`, node);
17
18
  });
18
19
  });
19
20
  const HeadingHierarchyRule = lintRule("remark-lint:changeset-heading-hierarchy", (tree, file)=>{
20
21
  let prevDepth = 0;
21
22
  visit(tree, "heading", (node)=>{
22
- if (1 === node.depth) return void file.message("h1 headings are not allowed in changeset files", node);
23
- if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}`, node);
23
+ if (1 === node.depth) return void file.message(`h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`, node);
24
+ if (prevDepth > 0 && node.depth > prevDepth + 1) file.message(`Heading level skipped: expected h${prevDepth + 1} or lower, found h${node.depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`, node);
24
25
  prevDepth = node.depth;
25
26
  });
26
27
  });
@@ -28,7 +29,7 @@ const RequiredSectionsRule = lintRule("remark-lint:changeset-required-sections",
28
29
  visit(tree, "heading", (node)=>{
29
30
  if (2 !== node.depth) return;
30
31
  const text = external_mdast_util_to_string_toString(node);
31
- if (!isValidHeading(text)) file.message(`Unknown section "${text}". Valid sections: ${allHeadings().join(", ")}`, node);
32
+ if (!isValidHeading(text)) file.message(`Unknown section "${text}". Valid h2 headings are: ${allHeadings().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`, node);
32
33
  });
33
34
  });
34
35
  const IGNORED_TYPES = new Set([
@@ -41,7 +42,7 @@ function isContentNode(node) {
41
42
  const UncategorizedContentRule = lintRule("remark-lint:changeset-uncategorized-content", (tree, file)=>{
42
43
  for (const node of tree.children){
43
44
  if ("heading" === node.type && 2 === node.depth) break;
44
- if (isContentNode(node)) file.message("Content must be placed under a category heading (## heading)", node);
45
+ if (isContentNode(node)) file.message(`Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`, node);
45
46
  }
46
47
  });
47
48
  function getVersionBlocks(tree) {
@@ -2,8 +2,8 @@
2
2
  import { Args, Command, Options } from "@effect/cli";
3
3
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
4
  import { execSync } from "node:child_process";
5
+ import { applyEdits, modify, parse } from "jsonc-parser";
5
6
  import { findProjectRoot, getWorkspaceInfos } from "workspace-tools";
6
- import { parse } from "jsonc-parser";
7
7
  import { globSync } from "tinyglobby";
8
8
  import { readFileSync, ChangelogTransformer, ChangesetLinter, resolve, existsSync, relative, mkdirSync, writeFileSync, join } from "../273.js";
9
9
  import { VersionFilesSchema, Schema, VersionFileError, Data, Effect } from "../795.js";
@@ -92,9 +92,10 @@ function detectGitHubRepo(cwd) {
92
92
  } catch {}
93
93
  return null;
94
94
  }
95
- function stripJsoncComments(text) {
96
- return text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
97
- }
95
+ const JSONC_FORMAT = {
96
+ tabSize: 1,
97
+ insertSpaces: false
98
+ };
98
99
  function resolveWorkspaceRoot(cwd) {
99
100
  return findProjectRoot(cwd) ?? cwd;
100
101
  }
@@ -156,13 +157,38 @@ function handleBaseMarkdownlint(root) {
156
157
  const foundPath = findMarkdownlintConfig(root);
157
158
  if (!foundPath) return `Warning: no markdownlint config found (checked ${MARKDOWNLINT_CONFIG_PATHS.join(", ")})`;
158
159
  const fullPath = join(root, foundPath);
159
- const raw = readFileSync(fullPath, "utf-8");
160
- const parsed = JSON.parse(stripJsoncComments(raw));
161
- if (!Array.isArray(parsed.customRules)) parsed.customRules = [];
162
- if (!parsed.customRules.includes(CUSTOM_RULES_ENTRY)) parsed.customRules.push(CUSTOM_RULES_ENTRY);
163
- if ("object" != typeof parsed.config || null === parsed.config) parsed.config = {};
164
- for (const rule of RULE_NAMES)if (!(rule in parsed.config)) parsed.config[rule] = false;
165
- writeFileSync(fullPath, `${JSON.stringify(parsed, null, "\t")}\n`);
160
+ let text = readFileSync(fullPath, "utf-8");
161
+ const parsed = parse(text);
162
+ if (!Array.isArray(parsed.customRules) || !parsed.customRules.includes(CUSTOM_RULES_ENTRY)) {
163
+ const edits = modify(text, [
164
+ "customRules",
165
+ -1
166
+ ], CUSTOM_RULES_ENTRY, {
167
+ formattingOptions: JSONC_FORMAT,
168
+ isArrayInsertion: true
169
+ });
170
+ text = applyEdits(text, edits);
171
+ }
172
+ const currentConfig = parse(text).config;
173
+ if ("object" != typeof currentConfig || null === currentConfig) {
174
+ const edits = modify(text, [
175
+ "config"
176
+ ], {}, {
177
+ formattingOptions: JSONC_FORMAT
178
+ });
179
+ text = applyEdits(text, edits);
180
+ }
181
+ const config = parse(text).config;
182
+ for (const rule of RULE_NAMES)if (!(rule in config)) {
183
+ const edits = modify(text, [
184
+ "config",
185
+ rule
186
+ ], false, {
187
+ formattingOptions: JSONC_FORMAT
188
+ });
189
+ text = applyEdits(text, edits);
190
+ }
191
+ writeFileSync(fullPath, text);
166
192
  return `Updated ${foundPath}`;
167
193
  },
168
194
  catch: (error)=>new InitError({
@@ -248,7 +274,7 @@ function checkBaseMarkdownlint(root) {
248
274
  ];
249
275
  try {
250
276
  const raw = readFileSync(join(root, foundPath), "utf-8");
251
- const parsed = JSON.parse(stripJsoncComments(raw));
277
+ const parsed = parse(raw);
252
278
  const issues = [];
253
279
  if (!Array.isArray(parsed.customRules) || !parsed.customRules.includes(CUSTOM_RULES_ENTRY)) issues.push({
254
280
  file: foundPath,
@@ -759,7 +785,7 @@ const rootCommand = Command.make("savvy-changesets").pipe(Command.withSubcommand
759
785
  ]));
760
786
  const cli = Command.run(rootCommand, {
761
787
  name: "savvy-changesets",
762
- version: "0.3.0"
788
+ version: "0.4.0"
763
789
  });
764
790
  function runCli() {
765
791
  const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer));
package/esm/index.js CHANGED
@@ -48,13 +48,13 @@ const SectionCategorySchema = Schema.Struct({
48
48
  description: Schema.String
49
49
  });
50
50
  const CommitHashSchema = Schema.String.pipe(Schema.pattern(/^[a-f0-9]{7,}$/, {
51
- message: ()=>"Commit hash must be at least 7 hexadecimal characters"
51
+ message: ()=>'Commit hash must be 7 or more lowercase hexadecimal characters (0-9, a-f). Example: "a1b2c3d" or a full 40-character SHA like "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2". Uppercase letters are not allowed'
52
52
  }));
53
53
  const VersionTypeSchema = Schema.Literal("major", "minor", "patch", "none");
54
54
  const ChangesetSummarySchema = Schema.String.pipe(Schema.minLength(1, {
55
- message: ()=>"Changeset summary cannot be empty"
55
+ message: ()=>'Changeset summary cannot be empty. Provide a 1-1000 character description of the change (e.g., "Fix authentication timeout in login flow")'
56
56
  }), Schema.maxLength(1000, {
57
- message: ()=>"Changeset summary is too long"
57
+ message: ()=>"Changeset summary exceeds the 1000 character limit. Shorten the summary to at most 1000 characters — use the changeset body for additional details"
58
58
  }));
59
59
  const ChangesetSchema = Schema.Struct({
60
60
  summary: ChangesetSummarySchema,
@@ -1,3 +1,4 @@
1
+ import { RULE_DOCS } from "./260.js";
1
2
  import { isValidHeading, allHeadings } from "./60.js";
2
3
  function getHeadingLevel(heading) {
3
4
  const sequence = heading.children.find((c)=>"atxHeadingSequence" === c.type);
@@ -33,7 +34,7 @@ const ContentStructureRule = {
33
34
  const nextIdx = i + 1 < h2Indices.length ? h2Indices[i + 1] : tokens.length;
34
35
  if (!hasContentBetween(tokens, currentIdx, nextIdx)) onError({
35
36
  lineNumber: tokens[currentIdx].startLine,
36
- detail: "Empty section: heading has no content before the next section or end of file"
37
+ detail: `Empty section: heading has no content before the next section or end of file. Add a list of changes (e.g., "- Added feature X") under this heading, or remove the empty heading. See: ${RULE_DOCS.CSH003}`
37
38
  });
38
39
  }
39
40
  for (const token of tokens){
@@ -42,7 +43,7 @@ const ContentStructureRule = {
42
43
  const hasInfo = openingFence?.children.some((c)=>"codeFencedFenceInfo" === c.type) ?? false;
43
44
  if (!hasInfo) onError({
44
45
  lineNumber: token.startLine,
45
- detail: "Code block is missing a language identifier"
46
+ detail: `Code block is missing a language identifier. Add a language after the opening fence (e.g., \`\`\`ts, \`\`\`json, \`\`\`bash). See: ${RULE_DOCS.CSH003}`
46
47
  });
47
48
  }
48
49
  for (const token of tokens){
@@ -60,7 +61,7 @@ const ContentStructureRule = {
60
61
  }
61
62
  if (!hasContent) onError({
62
63
  lineNumber: children[i].startLine,
63
- detail: "Empty list item"
64
+ detail: `Empty list item. Each list item must contain descriptive text (e.g., "- Fixed login timeout issue"). See: ${RULE_DOCS.CSH003}`
64
65
  });
65
66
  }
66
67
  }
@@ -84,13 +85,13 @@ const HeadingHierarchyRule = {
84
85
  if (1 === depth) {
85
86
  onError({
86
87
  lineNumber: token.startLine,
87
- detail: "h1 headings are not allowed in changeset files"
88
+ detail: `h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`
88
89
  });
89
90
  continue;
90
91
  }
91
92
  if (prevDepth > 0 && depth > prevDepth + 1) onError({
92
93
  lineNumber: token.startLine,
93
- detail: `Heading level skipped: expected h${prevDepth + 1} or lower, found h${depth}`
94
+ detail: `Heading level skipped: expected h${prevDepth + 1} or lower, found h${depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`
94
95
  });
95
96
  prevDepth = depth;
96
97
  }
@@ -113,7 +114,7 @@ const RequiredSectionsRule = {
113
114
  const text = getHeadingText(token);
114
115
  if (!isValidHeading(text)) onError({
115
116
  lineNumber: token.startLine,
116
- detail: `Unknown section "${text}". Valid sections: ${allHeadings().join(", ")}`
117
+ detail: `Unknown section "${text}". Valid h2 headings are: ${allHeadings().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`
117
118
  });
118
119
  }
119
120
  }
@@ -135,7 +136,7 @@ const UncategorizedContentRule = {
135
136
  if ("lineEnding" !== token.type && "lineEndingBlank" !== token.type && "htmlFlow" !== token.type) {
136
137
  if ("atxHeading" !== token.type) onError({
137
138
  lineNumber: token.startLine,
138
- detail: "Content must be placed under a category heading (## heading)"
139
+ detail: `Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`
139
140
  });
140
141
  }
141
142
  }
package/package.json CHANGED
@@ -1,98 +1,98 @@
1
1
  {
2
- "name": "@savvy-web/changesets",
3
- "version": "0.3.0",
4
- "private": false,
5
- "description": "Custom changelog formatter and markdown processing pipeline for the Silk Suite. Provides structured changeset sections, remark-based validation and transformation, and an Effect CLI.",
6
- "keywords": [
7
- "changesets",
8
- "changelog",
9
- "release-notes",
10
- "remark",
11
- "markdown",
12
- "effect",
13
- "silk-suite"
14
- ],
15
- "homepage": "https://github.com/savvy-web/changesets#readme",
16
- "bugs": {
17
- "url": "https://github.com/savvy-web/changesets/issues"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/savvy-web/changesets.git"
22
- },
23
- "license": "MIT",
24
- "author": {
25
- "name": "C. Spencer Beggs",
26
- "email": "spencer@savvyweb.systems",
27
- "url": "https://savvyweb.systems"
28
- },
29
- "type": "module",
30
- "exports": {
31
- ".": {
32
- "types": "./esm/index.d.ts",
33
- "import": "./esm/index.js",
34
- "require": "./cjs/index.cjs"
35
- },
36
- "./changelog": {
37
- "types": "./esm/changelog.d.ts",
38
- "import": "./esm/changelog.js",
39
- "require": "./cjs/changelog.cjs"
40
- },
41
- "./markdownlint": {
42
- "types": "./esm/markdownlint.d.ts",
43
- "import": "./esm/markdownlint.js",
44
- "require": "./cjs/markdownlint.cjs"
45
- },
46
- "./remark": {
47
- "types": "./esm/remark.d.ts",
48
- "import": "./esm/remark.js",
49
- "require": "./cjs/remark.cjs"
50
- }
51
- },
52
- "bin": {
53
- "savvy-changesets": "./esm/bin/savvy-changesets.js"
54
- },
55
- "dependencies": {
56
- "@changesets/get-github-info": "^0.7.0",
57
- "@effect/cli": "^0.73.2",
58
- "@effect/platform": "^0.94.5",
59
- "@effect/platform-node": "^0.104.1",
60
- "effect": "^3.19.19",
61
- "jsonc-parser": "^3.3.1",
62
- "mdast-util-heading-range": "^4.0.0",
63
- "mdast-util-to-string": "^4.0.0",
64
- "remark-gfm": "^4.0.1",
65
- "remark-parse": "^11.0.0",
66
- "remark-stringify": "^11.0.0",
67
- "tinyglobby": "^0.2.15",
68
- "unified": "^11.0.5",
69
- "unified-lint-rule": "^3.0.1",
70
- "unist-util-visit": "^5.1.0",
71
- "workspace-tools": "^0.41.0"
72
- },
73
- "peerDependencies": {
74
- "@changesets/cli": "^2.29.8"
75
- },
76
- "peerDependenciesMeta": {
77
- "@changesets/cli": {
78
- "optional": false
79
- }
80
- },
81
- "engines": {
82
- "node": ">=24.0.0"
83
- },
84
- "scripts": {
85
- "postinstall": "savvy-changesets init --check"
86
- },
87
- "files": [
88
- "!changesets.api.json",
89
- "!tsconfig.json",
90
- "!tsdoc.json",
91
- "LICENSE",
92
- "README.md",
93
- "cjs",
94
- "esm",
95
- "package.json",
96
- "tsdoc-metadata.json"
97
- ]
98
- }
2
+ "name": "@savvy-web/changesets",
3
+ "version": "0.4.0",
4
+ "private": false,
5
+ "description": "Custom changelog formatter and markdown processing pipeline for the Silk Suite. Provides structured changeset sections, remark-based validation and transformation, and an Effect CLI.",
6
+ "keywords": [
7
+ "changesets",
8
+ "changelog",
9
+ "release-notes",
10
+ "remark",
11
+ "markdown",
12
+ "effect",
13
+ "silk-suite"
14
+ ],
15
+ "homepage": "https://github.com/savvy-web/changesets#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/savvy-web/changesets/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/savvy-web/changesets.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": {
25
+ "name": "C. Spencer Beggs",
26
+ "email": "spencer@savvyweb.systems",
27
+ "url": "https://savvyweb.systems"
28
+ },
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./esm/index.d.ts",
33
+ "import": "./esm/index.js",
34
+ "require": "./cjs/index.cjs"
35
+ },
36
+ "./changelog": {
37
+ "types": "./esm/changelog.d.ts",
38
+ "import": "./esm/changelog.js",
39
+ "require": "./cjs/changelog.cjs"
40
+ },
41
+ "./markdownlint": {
42
+ "types": "./esm/markdownlint.d.ts",
43
+ "import": "./esm/markdownlint.js",
44
+ "require": "./cjs/markdownlint.cjs"
45
+ },
46
+ "./remark": {
47
+ "types": "./esm/remark.d.ts",
48
+ "import": "./esm/remark.js",
49
+ "require": "./cjs/remark.cjs"
50
+ }
51
+ },
52
+ "bin": {
53
+ "savvy-changesets": "./esm/bin/savvy-changesets.js"
54
+ },
55
+ "dependencies": {
56
+ "@changesets/get-github-info": "^0.8.0",
57
+ "@effect/cli": "^0.73.2",
58
+ "@effect/platform": "^0.94.5",
59
+ "@effect/platform-node": "^0.104.1",
60
+ "effect": "^3.19.19",
61
+ "jsonc-parser": "^3.3.1",
62
+ "mdast-util-heading-range": "^4.0.0",
63
+ "mdast-util-to-string": "^4.0.0",
64
+ "remark-gfm": "^4.0.1",
65
+ "remark-parse": "^11.0.0",
66
+ "remark-stringify": "^11.0.0",
67
+ "tinyglobby": "^0.2.15",
68
+ "unified": "^11.0.5",
69
+ "unified-lint-rule": "^3.0.1",
70
+ "unist-util-visit": "^5.1.0",
71
+ "workspace-tools": "^0.41.0"
72
+ },
73
+ "peerDependencies": {
74
+ "@changesets/cli": "^2.30.0"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "@changesets/cli": {
78
+ "optional": false
79
+ }
80
+ },
81
+ "engines": {
82
+ "node": ">=24.0.0"
83
+ },
84
+ "scripts": {
85
+ "postinstall": "savvy-changesets init --check"
86
+ },
87
+ "files": [
88
+ "!changesets.api.json",
89
+ "!tsconfig.json",
90
+ "!tsdoc.json",
91
+ "LICENSE",
92
+ "README.md",
93
+ "cjs",
94
+ "esm",
95
+ "package.json",
96
+ "tsdoc-metadata.json"
97
+ ]
98
+ }