@savvy-web/silk-effects 7.0.1 → 7.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.
Files changed (33) hide show
  1. package/README.md +4 -1
  2. package/changesets/api/linter.js +1 -1
  3. package/changesets/api/transformer.js +10 -7
  4. package/changesets/changelog/getReleaseLine.js +114 -20
  5. package/changesets/changelog/vanilla.js +47 -0
  6. package/changesets/constants.js +7 -2
  7. package/changesets/index.js +7 -4
  8. package/changesets/markdownlint/rules/content-structure.js +2 -2
  9. package/changesets/markdownlint/rules/dependency-table-format.js +12 -11
  10. package/changesets/markdownlint/rules/heading-hierarchy.js +2 -2
  11. package/changesets/markdownlint/rules/required-sections.js +2 -2
  12. package/changesets/markdownlint/rules/uncategorized-content.js +3 -3
  13. package/changesets/markdownlint/rules/utils.js +17 -6
  14. package/changesets/remark/plugins/aggregate-dependency-tables.js +53 -1
  15. package/changesets/remark/plugins/contributor-footnotes.js +276 -64
  16. package/changesets/remark/plugins/reorder-sections.js +18 -3
  17. package/changesets/remark/presets.js +1 -1
  18. package/changesets/remark/rules/dependency-table-format.js +7 -9
  19. package/changesets/schemas/dependency-table.js +11 -3
  20. package/changesets/schemas/options.js +8 -0
  21. package/changesets/services/config-inspector.js +51 -4
  22. package/changesets/services/deps-regen.js +106 -10
  23. package/changesets/services/release-planner.js +29 -8
  24. package/changesets/utils/dep-diff.js +34 -5
  25. package/changesets/utils/dependency-section.js +34 -0
  26. package/changesets/utils/dependency-table.js +14 -9
  27. package/changesets/utils/markdown-emit.js +86 -0
  28. package/changesets/utils/remark-pipeline.js +14 -85
  29. package/changesets/utils/section-parser.js +4 -2
  30. package/index.d.ts +158 -24
  31. package/index.js +1 -1
  32. package/lint/index.js +1 -1
  33. package/package.json +4 -2
package/README.md CHANGED
@@ -109,7 +109,7 @@ const tags = strategy.tagsFor([{ name: "@savvy-web/silk-effects", version: "1.0.
109
109
 
110
110
  #### ChangesetLinter
111
111
 
112
- Validate a changeset file against the Silk section rules. `ChangesetLinter.validateContent(content, filePath?)` and `ChangesetLinter.validateFile(filePath)` are static and synchronous, returning `LintMessage[]` — no Effect, no layers. Rules cover the valid section headings, structural constraints, and the dependency-table format, so a `## Dependencies` section written as prose or a bullet list is reported rather than accepted.
112
+ Validate a changeset file against the Silk section rules. `ChangesetLinter.validateContent(content, filePath?)` and `ChangesetLinter.validateFile(filePath)` are static and synchronous, returning `LintMessage[]` — no Effect, no layers. Rules cover the valid section headings, structural constraints, and the dependency-table format: a `## Dependencies` section must contain a dependency table somewhere between its heading and the next — prose may sit before or after the table — and a section with no table at all is reported at the heading. Each rule is documented under [`docs/rules/`](./docs/rules/CSH001.md) (CSH001 through CSH005).
113
113
 
114
114
  ```typescript
115
115
  import { Changesets } from "@savvy-web/silk-effects";
@@ -294,6 +294,8 @@ const preview = yield* planner.preview(root, {
294
294
 
295
295
  `Changesets.ReleasePlanner.layer` requires `ConfigInspector` and `FileSystem`.
296
296
 
297
+ The `Changesets` namespace also re-exports the stock [`@changesets/changelog-git`](https://www.npmjs.com/package/@changesets/changelog-git) renderer as `Changesets.vanillaChangelogFunctions`, for consumers who want the vanilla changelog output without depending on that package directly.
298
+
297
299
  ---
298
300
 
299
301
  ### FileSystem + process layer required
@@ -408,6 +410,7 @@ const report = await Effect.runPromise(
408
410
  - [Changeset config](./docs/04-changeset-config.md) — reading and decoding `.changeset/config.json`
409
411
  - [Config discovery](./docs/05-config-discovery.md) — priority-based config file search
410
412
  - [Biome sync](./docs/06-biome-sync.md) — keeping Biome `$schema` URLs current
413
+ - Changeset lint rules — [CSH001](./docs/rules/CSH001.md) through [CSH005](./docs/rules/CSH005.md), the section rules `ChangesetLinter` enforces
411
414
 
412
415
  ## License
413
416
 
@@ -6,10 +6,10 @@ import { UncategorizedContentRule } from "../remark/rules/uncategorized-content.
6
6
  import { stripFrontmatter } from "../utils/strip-frontmatter.js";
7
7
  import remarkGfm from "remark-gfm";
8
8
  import remarkParse from "remark-parse";
9
- import remarkStringify from "remark-stringify";
10
9
  import { unified } from "unified";
11
10
  import { readFileSync, readdirSync } from "node:fs";
12
11
  import { join } from "node:path";
12
+ import remarkStringify from "remark-stringify";
13
13
 
14
14
  //#region src/changesets/api/linter.ts
15
15
  /**
@@ -1,8 +1,9 @@
1
+ import { emitMarkdown } from "../utils/markdown-emit.js";
2
+ import { ContributorFootnotesPlugin } from "../remark/plugins/contributor-footnotes.js";
1
3
  import { MaintenanceNotePlugin } from "../remark/plugins/maintenance-note.js";
2
4
  import { SilkChangesetTransformPreset } from "../remark/presets.js";
3
5
  import remarkGfm from "remark-gfm";
4
6
  import remarkParse from "remark-parse";
5
- import remarkStringify from "remark-stringify";
6
7
  import { unified } from "unified";
7
8
  import { readFileSync, writeFileSync } from "node:fs";
8
9
 
@@ -92,8 +93,9 @@ var ChangelogTransformer = class ChangelogTransformer {
92
93
  * @remarks
93
94
  * The input is parsed with `remark-parse` and `remark-gfm` (for table
94
95
  * support), processed through every plugin in {@link SilkChangesetTransformPreset}
95
- * in order, and stringified back to markdown. The operation is synchronous
96
- * and idempotent.
96
+ * in order, and emitted back to markdown through the canonical
97
+ * `@effected/markdown` stringifier. The operation is synchronous and
98
+ * idempotent.
97
99
  *
98
100
  * @param content - Raw CHANGELOG markdown string (may contain multiple
99
101
  * version blocks, GFM tables, footnotes, and reference links)
@@ -103,11 +105,12 @@ var ChangelogTransformer = class ChangelogTransformer {
103
105
  */
104
106
  static transformContent(content, options) {
105
107
  const processor = unified().use(remarkParse).use(remarkGfm);
106
- for (const plugin of SilkChangesetTransformPreset) processor.use(plugin);
108
+ for (const plugin of SilkChangesetTransformPreset) if (plugin === ContributorFootnotesPlugin && options?.thanks !== void 0) processor.use(ContributorFootnotesPlugin, { thanks: options.thanks });
109
+ else processor.use(plugin);
107
110
  if (options?.maintenance) processor.use(MaintenanceNotePlugin, options.maintenance);
108
- processor.use(remarkStringify);
109
- const file = processor.processSync(content);
110
- return String(file);
111
+ const parsed = processor.parse(content);
112
+ const transformed = processor.runSync(parsed);
113
+ return emitMarkdown(transformed);
111
114
  }
112
115
  /**
113
116
  * Transform a CHANGELOG file in-place.
@@ -1,5 +1,6 @@
1
1
  import { resolveCommitType } from "../categories/index.js";
2
2
  import { GitHubService } from "../services/github.js";
3
+ import { parseMarkdown, stringifyMarkdown } from "../utils/remark-pipeline.js";
3
4
  import { GitHubInfoSchema } from "../schemas/github.js";
4
5
  import { parseCommitMessage } from "../utils/commit-parser.js";
5
6
  import { parseIssueReferences } from "../utils/issue-refs.js";
@@ -41,6 +42,105 @@ import { Effect, Schema } from "effect";
41
42
  * @internal
42
43
  */
43
44
  /**
45
+ * Stringify a single MDAST block node back to markdown, trimmed.
46
+ *
47
+ * @internal
48
+ */
49
+ function stringifyNode(node) {
50
+ return stringifyMarkdown({
51
+ type: "root",
52
+ children: [node]
53
+ }).trimEnd();
54
+ }
55
+ /**
56
+ * Render one top-level section content node to markdown.
57
+ *
58
+ * @remarks
59
+ * The emit decision is made on the node type, never on string prefixes:
60
+ *
61
+ * - **paragraph** — rendered as a `- ` bullet, with continuation lines
62
+ * indented two spaces so multi-line prose stays inside the list item
63
+ * (the historical behavior for simple prose content).
64
+ * - **heading** — promoted one level so a changeset `###` sub-heading emits
65
+ * as `####` and can never collide with the changelog's depth-3 category
66
+ * headings (which the version-block utilities and merge/reorder/dedup
67
+ * plugins treat as section boundaries). Depths are clamped to the 4–6
68
+ * range: promoting below 4 could fabricate a version or category heading.
69
+ * - **everything else** (list, table, code fence, blockquote, ...) — passes
70
+ * through verbatim as a block: never bullet-prefixed, never
71
+ * continuation-indented.
72
+ *
73
+ * @internal
74
+ */
75
+ function renderSectionNode(node) {
76
+ if (node.type === "paragraph") {
77
+ const [first, ...rest] = stringifyNode(node).split("\n");
78
+ return [`- ${first}`, ...rest.map((line) => line.length > 0 ? ` ${line}` : line)].join("\n");
79
+ }
80
+ if (node.type === "heading") {
81
+ const heading = node;
82
+ return stringifyNode({
83
+ ...heading,
84
+ depth: Math.min(Math.max(heading.depth + 1, 4), 6)
85
+ });
86
+ }
87
+ return stringifyNode(node);
88
+ }
89
+ /**
90
+ * Parse an attribution suffix (e.g. `" [#42](url) Thanks [\@user](url)!"`)
91
+ * into phrasing nodes ready to append to a paragraph, prefixed with a
92
+ * separating space.
93
+ *
94
+ * @internal
95
+ */
96
+ function attributionPhrasing(attribution) {
97
+ const trimmed = attribution.trim();
98
+ if (trimmed === "") return void 0;
99
+ const first = parseMarkdown(trimmed).children[0];
100
+ if (first?.type !== "paragraph") return void 0;
101
+ return [{
102
+ type: "text",
103
+ value: " "
104
+ }, ...first.children];
105
+ }
106
+ /**
107
+ * Find the paragraph of a list's FINAL RENDERED bullet: the last paragraph
108
+ * of the last item, recursing into a trailing nested list first — for
109
+ * `- Parent` / ` - Child`, the child is the bullet a reader sees last, so
110
+ * the attribution must land there, not on the parent line above it.
111
+ *
112
+ * @internal
113
+ */
114
+ function lastRenderedBulletParagraph(list) {
115
+ const lastItem = list.children[list.children.length - 1];
116
+ if (!lastItem) return void 0;
117
+ for (let i = lastItem.children.length - 1; i >= 0; i--) {
118
+ const child = lastItem.children[i];
119
+ if (child.type === "list") {
120
+ const nested = lastRenderedBulletParagraph(child);
121
+ if (nested) return nested;
122
+ } else if (child.type === "paragraph") return child;
123
+ }
124
+ }
125
+ /**
126
+ * Find the paragraph the attribution should be appended to: the LAST
127
+ * top-level paragraph, or the deepest trailing bullet's paragraph of the
128
+ * LAST top-level list (see {@link lastRenderedBulletParagraph}), across all
129
+ * section content nodes. Tables, headings, code fences and blockquotes are
130
+ * never targets — attribution must not land inside them.
131
+ *
132
+ * @internal
133
+ */
134
+ function findAttributionTarget(sections) {
135
+ let target;
136
+ for (const section of sections) for (const node of section.contentNodes) if (node.type === "paragraph") target = node;
137
+ else if (node.type === "list") {
138
+ const lastParagraph = lastRenderedBulletParagraph(node);
139
+ if (lastParagraph) target = lastParagraph;
140
+ }
141
+ return target;
142
+ }
143
+ /**
44
144
  * Format a single changeset into a markdown release line.
45
145
  *
46
146
  * This is the core Effect program that implements the `getReleaseLine` contract
@@ -86,30 +186,24 @@ function getReleaseLine(changeset, versionType, options) {
86
186
  const commitMsg = parseCommitMessage(firstLine);
87
187
  const bodyText = changeset.summary.split("\n").slice(1).join("\n");
88
188
  const issueRefs = parseIssueReferences(bodyText);
89
- const attribution = commitInfo ? formatPRAndUserAttribution(commitInfo.pull ?? void 0, commitInfo.user ?? void 0, commitInfo.links) : "";
189
+ const includeThanks = options.thanks !== false;
190
+ const attribution = commitInfo ? formatPRAndUserAttribution(commitInfo.pull ?? void 0, includeThanks ? commitInfo.user ?? void 0 : void 0, commitInfo.links) : "";
90
191
  if (parsed.sections.length > 0) {
91
- const lines = [];
92
- if (parsed.preamble) {
93
- lines.push(parsed.preamble);
94
- lines.push("");
192
+ const phrasing = attributionPhrasing(attribution);
193
+ let standaloneAttribution;
194
+ if (phrasing) {
195
+ const target = findAttributionTarget(parsed.sections);
196
+ if (target) target.children.push(...phrasing);
197
+ else standaloneAttribution = attribution.trim();
95
198
  }
199
+ const blocks = [];
200
+ if (parsed.preamble) blocks.push(parsed.preamble);
96
201
  for (const section of parsed.sections) {
97
- lines.push(`### ${section.category.heading}`);
98
- lines.push("");
99
- if (section.content) {
100
- const contentLines = section.content.split("\n");
101
- const firstContentLine = contentLines[0];
102
- if (firstContentLine.startsWith("- ") || firstContentLine.startsWith("* ")) {
103
- lines.push(firstContentLine);
104
- lines.push(...contentLines.slice(1));
105
- } else {
106
- lines.push(`- ${firstContentLine}`);
107
- lines.push(...contentLines.slice(1).map((line) => line.length > 0 ? ` ${line}` : line));
108
- }
109
- }
110
- lines.push("");
202
+ blocks.push(`### ${section.category.heading}`);
203
+ for (const node of section.contentNodes) blocks.push(renderSectionNode(node));
111
204
  }
112
- return `${lines.join("\n").trimEnd()}${attribution}`;
205
+ if (standaloneAttribution) blocks.push(standaloneAttribution);
206
+ return blocks.join("\n\n");
113
207
  }
114
208
  const commitType = commitMsg.type ?? versionType;
115
209
  const entryInput = {
@@ -0,0 +1,47 @@
1
+ import changelogGit from "@changesets/changelog-git";
2
+
3
+ //#region src/changesets/changelog/vanilla.ts
4
+ /**
5
+ * The vanilla changesets changelog renderer — `\@changesets/changelog-git`,
6
+ * re-exported.
7
+ *
8
+ * @remarks
9
+ * Some consumers (notably `silk-release-action`) need the STOCK changesets
10
+ * rendering — plain summary lines with commit hashes, no sections, no
11
+ * attribution, no dependency tables — as a fallback or comparison path
12
+ * alongside the silk formatter. Re-exporting the upstream implementation
13
+ * here means those consumers depend only on this package: they get exact
14
+ * parity with `\@changesets/changelog-git` (this is a re-export, never a
15
+ * reimplementation) without declaring the dependency themselves.
16
+ *
17
+ * @see {@link changelogFunctions} in `./index.ts` for the silk formatter
18
+ * this is the vanilla alternative to
19
+ */
20
+ /**
21
+ * The stock `\@changesets/changelog-git` changelog implementation.
22
+ *
23
+ * @remarks
24
+ * This is the exact upstream default export (identity-equal, pinned by
25
+ * test), typed against this package's re-exported {@link ChangelogFunctions}
26
+ * interface. Use it wherever the plain changesets rendering is wanted
27
+ * instead of the silk section-aware formatter — e.g. in
28
+ * `.changeset/config.json` via a changelog module that forwards it, or
29
+ * programmatically from a release pipeline.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { Changesets } from "@savvy-web/silk-effects";
34
+ *
35
+ * const line = await Changesets.vanillaChangelogFunctions.getReleaseLine(
36
+ * { id: "x", summary: "Fix a thing", releases: [{ name: "pkg", type: "patch" }] },
37
+ * "patch",
38
+ * null,
39
+ * );
40
+ * ```
41
+ *
42
+ * @public
43
+ */
44
+ const vanillaChangelogFunctions = changelogGit;
45
+
46
+ //#endregion
47
+ export { vanillaChangelogFunctions };
@@ -1,6 +1,11 @@
1
1
  //#region src/changesets/constants.ts
2
- /** Base URL for rule documentation on GitHub. */
3
- const DOCS_BASE = "https://github.com/savvy-web/changesets/blob/main/docs/rules";
2
+ /**
3
+ * Base URL for rule documentation on GitHub.
4
+ *
5
+ * The former home (savvy-web/changesets) is archived; the docs live in the
6
+ * systems monorepo alongside the rule sources (savvy-web/systems#456).
7
+ */
8
+ const DOCS_BASE = "https://github.com/savvy-web/systems/blob/main/packages/silk-effects/docs/rules";
4
9
  /**
5
10
  * Documentation URLs for each changeset lint rule.
6
11
  *
@@ -18,9 +18,9 @@ import { HeadingHierarchyRule } from "./remark/rules/heading-hierarchy.js";
18
18
  import { RequiredSectionsRule } from "./remark/rules/required-sections.js";
19
19
  import { UncategorizedContentRule } from "./remark/rules/uncategorized-content.js";
20
20
  import { ChangesetLinter } from "./api/linter.js";
21
+ import { ContributorFootnotesPlugin } from "./remark/plugins/contributor-footnotes.js";
21
22
  import { MaintenanceNotePlugin } from "./remark/plugins/maintenance-note.js";
22
23
  import { AggregateDependencyTablesPlugin } from "./remark/plugins/aggregate-dependency-tables.js";
23
- import { ContributorFootnotesPlugin } from "./remark/plugins/contributor-footnotes.js";
24
24
  import { DeduplicateItemsPlugin } from "./remark/plugins/deduplicate-items.js";
25
25
  import { IssueLinkRefsPlugin } from "./remark/plugins/issue-link-refs.js";
26
26
  import { MergeSectionsPlugin } from "./remark/plugins/merge-sections.js";
@@ -28,13 +28,14 @@ import { NormalizeFormatPlugin } from "./remark/plugins/normalize-format.js";
28
28
  import { ReorderSectionsPlugin } from "./remark/plugins/reorder-sections.js";
29
29
  import { SilkChangesetPreset, SilkChangesetTransformPreset } from "./remark/presets.js";
30
30
  import { ChangelogTransformer } from "./api/transformer.js";
31
+ import { vanillaChangelogFunctions } from "./changelog/vanilla.js";
31
32
  import { ClassificationReasonSchema, ClassificationSchema, ConfigInspector, InspectedConfigSchema, ResolvedPackageScopeSchema, ResolvedVersionFileSchema, makeConfigInspectorTest } from "./services/config-inspector.js";
32
33
  import { BranchAnalysisSchema, BranchAnalyzer, BranchFileEntrySchema, FileStatusSchema, makeBranchAnalyzerTest } from "./services/branch-analyzer.js";
33
34
  import { ChangelogService } from "./services/changelog.js";
34
35
  import { computeWorkspaceDependencyDiffs } from "./utils/dep-diff.js";
35
36
  import { gitMergeBase } from "./utils/git.js";
36
37
  import { listPublishablePackageNames } from "./utils/publishability.js";
37
- import { DepsRegen, DepsRegenDefault, isPureDependencyChangeset, makeDepsRegenDefault } from "./services/deps-regen.js";
38
+ import { DepsRegen, DepsRegenDefault, isPureDependencyChangeset, makeDepsRegenDefault, parseChangesetPackages } from "./services/deps-regen.js";
38
39
  import { MaintenanceReasonSchema, MaintenanceTriggerSchema, deriveMaintenanceReason } from "./services/maintenance-reason.js";
39
40
  import { VersionFiles } from "./utils/version-files.js";
40
41
  import { ReleasePlanner, makeReleasePlannerTest } from "./services/release-planner.js";
@@ -150,8 +151,10 @@ var changesets_exports = /* @__PURE__ */ __exportAll({
150
151
  makeDepsRegenDefault: () => makeDepsRegenDefault,
151
152
  makeGitHubTest: () => makeGitHubTest,
152
153
  makeReleasePlannerTest: () => makeReleasePlannerTest,
153
- serializeDependencyTableToMarkdown: () => serializeDependencyTableToMarkdown
154
+ parseChangesetPackages: () => parseChangesetPackages,
155
+ serializeDependencyTableToMarkdown: () => serializeDependencyTableToMarkdown,
156
+ vanillaChangelogFunctions: () => vanillaChangelogFunctions
154
157
  });
155
158
 
156
159
  //#endregion
157
- export { AggregateDependencyTablesPlugin, AppliedReleaseEntrySchema, AppliedReleaseSchema, BranchAnalysisSchema, BranchAnalyzer, BranchFileEntrySchema, BumpTypeSchema, Categories, Changelog, ChangelogService, ChangelogTransformer, ChangesetIOError, ChangesetLinter, ChangesetOptionsSchema, ChangesetPreviewSchema, ChangesetSchema, ChangesetSummarySchema, ChangesetValidationError, ClassificationReasonSchema, ClassificationSchema, CommitHashSchema, ConfigInspector, ConfigurationError, ContentStructureRule, ContributorFootnotesPlugin, DeduplicateItemsPlugin, DependencyActionSchema, DependencyTable, DependencyTableFormatRule, DependencyTableRowSchema, DependencyTableSchema, DependencyTableTypeSchema, DependencyTypeSchema, DependencyUpdateSchema, DepsRegen, DepsRegenDefault, FileStatusSchema, GitError, GitHubApiError, GitHubInfoSchema, GitHubService, GlobSchema, HeadingHierarchyRule, InspectedConfigSchema, IssueLinkRefsPlugin, IssueNumberSchema, JsonPathSchema, LegacyVersionFileConfigSchema, LegacyVersionFilesSchema, MaintenanceNotePlugin, MaintenanceReasonSchema, MaintenanceTriggerSchema, MarkdownParseError, ContentStructureRule$1 as MarkdownlintContentStructureRule, DependencyTableFormatRule$1 as MarkdownlintDependencyTableFormatRule, HeadingHierarchyRule$1 as MarkdownlintHeadingHierarchyRule, RequiredSectionsRule$1 as MarkdownlintRequiredSectionsRule, UncategorizedContentRule$1 as MarkdownlintUncategorizedContentRule, MergeSectionsPlugin, NonEmptyString, NormalizeFormatPlugin, PackageScopeSchema, PackagesRecordSchema, PendingChangesetSchema, PositiveInteger, PreviewReleaseSchema, ReleasePlanError, ReleasePlanner, ReorderSectionsPlugin, RepoSchema, RequiredSectionsRule, ResolvedPackageScopeSchema, ResolvedVersionFileSchema, SectionCategorySchema, SilkChangesetPreset, SilkChangesetTransformPreset, SilkChangesetsRules, UncategorizedContentRule, UrlOrMarkdownLinkSchema, UsernameSchema, VERSION_RE, VersionFileConfigSchema, VersionFileError, VersionFileUpdateRecordSchema, VersionFiles, VersionFilesSchema, VersionOrEmptySchema, VersionTypeSchema, changelogFunctions, changesets_exports, computeWorkspaceDependencyDiffs, deriveMaintenanceReason, gitMergeBase, isPureDependencyChangeset, listPublishablePackageNames, makeBranchAnalyzerTest, makeConfigInspectorTest, makeDepsRegenDefault, makeGitHubTest, makeReleasePlannerTest, serializeDependencyTableToMarkdown };
160
+ export { AggregateDependencyTablesPlugin, AppliedReleaseEntrySchema, AppliedReleaseSchema, BranchAnalysisSchema, BranchAnalyzer, BranchFileEntrySchema, BumpTypeSchema, Categories, Changelog, ChangelogService, ChangelogTransformer, ChangesetIOError, ChangesetLinter, ChangesetOptionsSchema, ChangesetPreviewSchema, ChangesetSchema, ChangesetSummarySchema, ChangesetValidationError, ClassificationReasonSchema, ClassificationSchema, CommitHashSchema, ConfigInspector, ConfigurationError, ContentStructureRule, ContributorFootnotesPlugin, DeduplicateItemsPlugin, DependencyActionSchema, DependencyTable, DependencyTableFormatRule, DependencyTableRowSchema, DependencyTableSchema, DependencyTableTypeSchema, DependencyTypeSchema, DependencyUpdateSchema, DepsRegen, DepsRegenDefault, FileStatusSchema, GitError, GitHubApiError, GitHubInfoSchema, GitHubService, GlobSchema, HeadingHierarchyRule, InspectedConfigSchema, IssueLinkRefsPlugin, IssueNumberSchema, JsonPathSchema, LegacyVersionFileConfigSchema, LegacyVersionFilesSchema, MaintenanceNotePlugin, MaintenanceReasonSchema, MaintenanceTriggerSchema, MarkdownParseError, ContentStructureRule$1 as MarkdownlintContentStructureRule, DependencyTableFormatRule$1 as MarkdownlintDependencyTableFormatRule, HeadingHierarchyRule$1 as MarkdownlintHeadingHierarchyRule, RequiredSectionsRule$1 as MarkdownlintRequiredSectionsRule, UncategorizedContentRule$1 as MarkdownlintUncategorizedContentRule, MergeSectionsPlugin, NonEmptyString, NormalizeFormatPlugin, PackageScopeSchema, PackagesRecordSchema, PendingChangesetSchema, PositiveInteger, PreviewReleaseSchema, ReleasePlanError, ReleasePlanner, ReorderSectionsPlugin, RepoSchema, RequiredSectionsRule, ResolvedPackageScopeSchema, ResolvedVersionFileSchema, SectionCategorySchema, SilkChangesetPreset, SilkChangesetTransformPreset, SilkChangesetsRules, UncategorizedContentRule, UrlOrMarkdownLinkSchema, UsernameSchema, VERSION_RE, VersionFileConfigSchema, VersionFileError, VersionFileUpdateRecordSchema, VersionFiles, VersionFilesSchema, VersionOrEmptySchema, VersionTypeSchema, changelogFunctions, changesets_exports, computeWorkspaceDependencyDiffs, deriveMaintenanceReason, gitMergeBase, isPureDependencyChangeset, listPublishablePackageNames, makeBranchAnalyzerTest, makeConfigInspectorTest, makeDepsRegenDefault, makeGitHubTest, makeReleasePlannerTest, parseChangesetPackages, serializeDependencyTableToMarkdown, vanillaChangelogFunctions };
@@ -44,7 +44,7 @@ function hasContentBetween(tokens, currentIdx, nextIdx) {
44
44
  * }
45
45
  * ```
46
46
  *
47
- * @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH003.md | CSH003 rule documentation}
47
+ * @see {@link https://github.com/savvy-web/systems/blob/main/packages/silk-effects/docs/rules/CSH003.md | CSH003 rule documentation}
48
48
  * @see `src/remark/rules/content-structure.ts` for the corresponding remark-lint rule
49
49
  *
50
50
  * @public
@@ -57,7 +57,7 @@ const ContentStructureRule = {
57
57
  function: function CSH003(params, onError) {
58
58
  const tokens = params.parsers.micromark.tokens;
59
59
  const h2Indices = [];
60
- for (let i = 0; i < tokens.length; i++) if (tokens[i].type === "atxHeading" && getHeadingLevel(tokens[i]) === 2) h2Indices.push(i);
60
+ for (let i = 0; i < tokens.length; i++) if ((tokens[i].type === "atxHeading" || tokens[i].type === "setextHeading") && getHeadingLevel(tokens[i]) === 2) h2Indices.push(i);
61
61
  for (let i = 0; i < h2Indices.length; i++) {
62
62
  const currentIdx = h2Indices[i];
63
63
  if (!hasContentBetween(tokens, currentIdx, i + 1 < h2Indices.length ? h2Indices[i + 1] : tokens.length)) onError({
@@ -1,5 +1,6 @@
1
1
  import { VERSION_RE } from "../../schemas/dependency-table.js";
2
2
  import { RULE_DOCS } from "../../constants.js";
3
+ import { scanDependencySection } from "../../utils/dependency-section.js";
3
4
  import { getHeadingLevel, getHeadingText, unescapeMarkdown } from "./utils.js";
4
5
 
5
6
  //#region src/changesets/markdownlint/rules/dependency-table-format.ts
@@ -10,7 +11,9 @@ const VALID_TYPES = /* @__PURE__ */ new Set([
10
11
  "peerDependency",
11
12
  "optionalDependency",
12
13
  "workspace",
13
- "config"
14
+ "config",
15
+ "runtime",
16
+ "packageManager"
14
17
  ]);
15
18
  const VALID_ACTIONS = /* @__PURE__ */ new Set([
16
19
  "added",
@@ -64,19 +67,17 @@ const DependencyTableFormatRule = {
64
67
  const tokens = params.parsers.micromark.tokens;
65
68
  for (let i = 0; i < tokens.length; i++) {
66
69
  const token = tokens[i];
67
- if (token.type !== "atxHeading") continue;
70
+ if (token.type !== "atxHeading" && token.type !== "setextHeading") continue;
68
71
  if (getHeadingLevel(token) !== 2) continue;
69
72
  if (getHeadingText(token).toLowerCase() !== "dependencies") continue;
70
73
  const headingLine = token.startLine;
71
- let tableToken = null;
72
- for (let j = i + 1; j < tokens.length; j++) {
73
- const next = tokens[j];
74
- if (next.type === "lineEnding" || next.type === "lineEndingBlank") continue;
75
- if (next.type === "atxHeading") break;
76
- if (next.type === "table") tableToken = next;
77
- break;
78
- }
79
- if (tableToken === null) {
74
+ const { table } = scanDependencySection(tokens, i + 1, {
75
+ isSkippable: (t) => t.type === "lineEnding" || t.type === "lineEndingBlank",
76
+ isHeading: (t) => t.type === "atxHeading" || t.type === "setextHeading",
77
+ isTable: (t) => t.type === "table"
78
+ });
79
+ const tableToken = table;
80
+ if (tableToken === void 0) {
80
81
  onError({
81
82
  lineNumber: headingLine,
82
83
  detail: `Dependencies section must contain a table, not a list or paragraph. See: ${RULE_DOCS.CSH005}`
@@ -26,7 +26,7 @@ import { getHeadingLevel } from "./utils.js";
26
26
  * }
27
27
  * ```
28
28
  *
29
- * @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH001.md | CSH001 rule documentation}
29
+ * @see {@link https://github.com/savvy-web/systems/blob/main/packages/silk-effects/docs/rules/CSH001.md | CSH001 rule documentation}
30
30
  * @see `src/remark/rules/heading-hierarchy.ts` for the corresponding remark-lint rule
31
31
  *
32
32
  * @public
@@ -39,7 +39,7 @@ const HeadingHierarchyRule = {
39
39
  function: function CSH001(params, onError) {
40
40
  let prevDepth = 0;
41
41
  for (const token of params.parsers.micromark.tokens) {
42
- if (token.type !== "atxHeading") continue;
42
+ if (token.type !== "atxHeading" && token.type !== "setextHeading") continue;
43
43
  const depth = getHeadingLevel(token);
44
44
  if (depth === 1) {
45
45
  onError({
@@ -27,7 +27,7 @@ import { getHeadingLevel, getHeadingText } from "./utils.js";
27
27
  * }
28
28
  * ```
29
29
  *
30
- * @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH002.md | CSH002 rule documentation}
30
+ * @see {@link https://github.com/savvy-web/systems/blob/main/packages/silk-effects/docs/rules/CSH002.md | CSH002 rule documentation}
31
31
  * @see `src/remark/rules/required-sections.ts` for the corresponding remark-lint rule
32
32
  *
33
33
  * @public
@@ -39,7 +39,7 @@ const RequiredSectionsRule = {
39
39
  parser: "micromark",
40
40
  function: function CSH002(params, onError) {
41
41
  for (const token of params.parsers.micromark.tokens) {
42
- if (token.type !== "atxHeading") continue;
42
+ if (token.type !== "atxHeading" && token.type !== "setextHeading") continue;
43
43
  if (getHeadingLevel(token) !== 2) continue;
44
44
  const text = getHeadingText(token);
45
45
  if (!isValidHeading(text)) onError({
@@ -27,7 +27,7 @@ import { getHeadingLevel } from "./utils.js";
27
27
  * }
28
28
  * ```
29
29
  *
30
- * @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH004.md | CSH004 rule documentation}
30
+ * @see {@link https://github.com/savvy-web/systems/blob/main/packages/silk-effects/docs/rules/CSH004.md | CSH004 rule documentation}
31
31
  * @see `src/remark/rules/uncategorized-content.ts` for the corresponding remark-lint rule
32
32
  *
33
33
  * @public
@@ -40,9 +40,9 @@ const UncategorizedContentRule = {
40
40
  function: function CSH004(params, onError) {
41
41
  const tokens = params.parsers.micromark.tokens;
42
42
  for (const token of tokens) {
43
- if (token.type === "atxHeading" && getHeadingLevel(token) === 2) break;
43
+ if ((token.type === "atxHeading" || token.type === "setextHeading") && getHeadingLevel(token) === 2) break;
44
44
  if (token.type === "lineEnding" || token.type === "lineEndingBlank" || token.type === "htmlFlow") continue;
45
- if (token.type !== "atxHeading") onError({
45
+ if (token.type !== "atxHeading" && token.type !== "setextHeading") onError({
46
46
  lineNumber: token.startLine,
47
47
  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}`
48
48
  });
@@ -2,14 +2,25 @@ import { RULE_DOCS } from "../../constants.js";
2
2
 
3
3
  //#region src/changesets/markdownlint/rules/utils.ts
4
4
  /**
5
- * Get the heading level (1-6) from an `atxHeading` token.
5
+ * Get the heading level (1-6) from an `atxHeading` or `setextHeading` token.
6
6
  *
7
- * @param heading - The `atxHeading` micromark token
8
- * @returns The heading depth (number of `#` characters), or 0 if no sequence found
7
+ * @remarks
8
+ * remark-parse normalizes setext headings to plain depth-1/2 heading nodes,
9
+ * so the remark siblings of these rules see them as ordinary headings. The
10
+ * micromark extractors must agree, or the two engines disagree about the
11
+ * same file (the issue #367 class of drift).
12
+ *
13
+ * @param heading - The `atxHeading` or `setextHeading` micromark token
14
+ * @returns The heading depth (`#` count, or 1/2 for `=`/`-` setext
15
+ * underlines), or 0 if no sequence found
9
16
  *
10
17
  * @internal
11
18
  */
12
19
  function getHeadingLevel(heading) {
20
+ if (heading.type === "setextHeading") {
21
+ const line = heading.children.find((c) => c.type === "setextHeadingLine");
22
+ return line ? line.text.startsWith("=") ? 1 : 2 : 0;
23
+ }
13
24
  const sequence = heading.children.find((c) => c.type === "atxHeadingSequence");
14
25
  return sequence ? sequence.text.length : 0;
15
26
  }
@@ -39,20 +50,20 @@ function unescapeMarkdown(raw) {
39
50
  return raw.replace(BACKSLASH_ESCAPE_RE, "$1");
40
51
  }
41
52
  /**
42
- * Get the plain text content of an `atxHeading` token.
53
+ * Get the plain text content of an `atxHeading` or `setextHeading` token.
43
54
  *
44
55
  * @remarks
45
56
  * Escape-resolved, so a heading compares equal to the same heading as the
46
57
  * remark rules see it. `getHeadingLevel` needs no equivalent — it counts
47
58
  * sequence characters rather than reading text.
48
59
  *
49
- * @param heading - The `atxHeading` micromark token
60
+ * @param heading - The `atxHeading` or `setextHeading` micromark token
50
61
  * @returns The heading text, or empty string if no text token found
51
62
  *
52
63
  * @internal
53
64
  */
54
65
  function getHeadingText(heading) {
55
- const textToken = heading.children.find((c) => c.type === "atxHeadingText");
66
+ const textToken = heading.children.find((c) => c.type === "atxHeadingText" || c.type === "setextHeadingText");
56
67
  return textToken ? unescapeMarkdown(textToken.text) : "";
57
68
  }
58
69
 
@@ -2,6 +2,52 @@ import { collapseDependencyRows, parseDependencyTable, serializeDependencyTable,
2
2
  import { getBlockSections, getHeadingText, getVersionBlocks } from "../../utils/version-blocks.js";
3
3
 
4
4
  //#region src/changesets/remark/plugins/aggregate-dependency-tables.ts
5
+ /**
6
+ * Unwrap dependency tables that an earlier (broken) formatter nested inside
7
+ * list items.
8
+ *
9
+ * @remarks
10
+ * Historical CHANGELOG blocks (e.g. silk\@3.10.0) carry an authored
11
+ * dependency table bullet-wrapped into a list item, which the aggregation
12
+ * pass otherwise cannot recognize. Each list item's tables that parse as
13
+ * dependency tables contribute their rows; ONLY those tables leave the
14
+ * list. The non-table items (e.g. an explanatory bullet alongside the
15
+ * wrapped table) keep their bullet structure: they survive as a List node
16
+ * in the legacy content rather than being flattened to bare paragraphs.
17
+ * When no table in the list parses, the caller keeps the whole list as
18
+ * legacy content untouched.
19
+ *
20
+ * @param list - A list node found inside a `### Dependencies` section
21
+ * @returns Parsed rows plus the remaining non-table content (a pruned copy
22
+ * of the list, when any items survive)
23
+ *
24
+ * @internal
25
+ */
26
+ function extractTablesFromList(list) {
27
+ const rows = [];
28
+ const keptItems = [];
29
+ for (const item of list.children) {
30
+ const keptChildren = [];
31
+ for (const child of item.children) {
32
+ if (child.type === "table") try {
33
+ rows.push(...parseDependencyTable(child));
34
+ continue;
35
+ } catch {}
36
+ keptChildren.push(child);
37
+ }
38
+ if (keptChildren.length > 0) keptItems.push({
39
+ ...item,
40
+ children: keptChildren
41
+ });
42
+ }
43
+ return {
44
+ rows,
45
+ rest: keptItems.length > 0 ? [{
46
+ ...list,
47
+ children: keptItems
48
+ }] : []
49
+ };
50
+ }
5
51
  const AggregateDependencyTablesPlugin = () => {
6
52
  return (tree) => {
7
53
  const blocks = getVersionBlocks(tree);
@@ -16,7 +62,13 @@ const AggregateDependencyTablesPlugin = () => {
16
62
  } catch {
17
63
  legacyContent.push(node);
18
64
  }
19
- else legacyContent.push(node);
65
+ else if (node.type === "list") {
66
+ const extracted = extractTablesFromList(node);
67
+ if (extracted.rows.length > 0) {
68
+ allRows.push(...extracted.rows);
69
+ legacyContent.push(...extracted.rest);
70
+ } else legacyContent.push(node);
71
+ } else legacyContent.push(node);
20
72
  const collapsed = sortDependencyRows(collapseDependencyRows(allRows));
21
73
  const indicesToRemove = [];
22
74
  for (const section of depSections) {