@savvy-web/silk-effects 0.6.1 → 1.0.1

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 (155) hide show
  1. package/README.md +48 -17
  2. package/_virtual/_rolldown/runtime.js +18 -0
  3. package/changesets/api/categories.js +247 -0
  4. package/changesets/api/changelog.js +134 -0
  5. package/changesets/api/dependency-table.js +163 -0
  6. package/changesets/api/linter.js +168 -0
  7. package/changesets/api/transformer.js +140 -0
  8. package/changesets/categories/index.js +299 -0
  9. package/changesets/categories/types.js +66 -0
  10. package/changesets/changelog/formatting.js +119 -0
  11. package/changesets/changelog/getDependencyReleaseLine.js +114 -0
  12. package/changesets/changelog/getReleaseLine.js +122 -0
  13. package/changesets/changelog/index.js +99 -0
  14. package/changesets/constants.js +43 -0
  15. package/changesets/errors.js +305 -0
  16. package/changesets/index.js +146 -0
  17. package/changesets/markdownlint/index.js +29 -0
  18. package/changesets/markdownlint/rules/content-structure.js +98 -0
  19. package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
  20. package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
  21. package/changesets/markdownlint/rules/required-sections.js +54 -0
  22. package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
  23. package/changesets/markdownlint/rules/utils.js +30 -0
  24. package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
  25. package/changesets/remark/plugins/contributor-footnotes.js +123 -0
  26. package/changesets/remark/plugins/deduplicate-items.js +30 -0
  27. package/changesets/remark/plugins/issue-link-refs.js +58 -0
  28. package/changesets/remark/plugins/merge-sections.js +43 -0
  29. package/changesets/remark/plugins/normalize-format.js +47 -0
  30. package/changesets/remark/plugins/reorder-sections.js +34 -0
  31. package/changesets/remark/presets.js +119 -0
  32. package/changesets/remark/rules/content-structure.js +22 -0
  33. package/changesets/remark/rules/dependency-table-format.js +40 -0
  34. package/changesets/remark/rules/heading-hierarchy.js +19 -0
  35. package/changesets/remark/rules/required-sections.js +17 -0
  36. package/changesets/remark/rules/uncategorized-content.js +31 -0
  37. package/changesets/schemas/changeset.js +146 -0
  38. package/changesets/schemas/dependency-table.js +189 -0
  39. package/changesets/schemas/git.js +69 -0
  40. package/changesets/schemas/github.js +175 -0
  41. package/changesets/schemas/options.js +182 -0
  42. package/changesets/schemas/package-scope.js +128 -0
  43. package/changesets/schemas/primitives.js +72 -0
  44. package/changesets/schemas/version-files.js +151 -0
  45. package/changesets/services/branch-analyzer.js +278 -0
  46. package/changesets/services/changelog.js +50 -0
  47. package/changesets/services/config-inspector.js +390 -0
  48. package/changesets/services/github.js +178 -0
  49. package/changesets/services/markdown.js +106 -0
  50. package/changesets/services/workspace-snapshot.js +182 -0
  51. package/changesets/utils/commit-parser.js +80 -0
  52. package/changesets/utils/dep-diff.js +77 -0
  53. package/changesets/utils/dependency-table.js +347 -0
  54. package/changesets/utils/issue-refs.js +101 -0
  55. package/changesets/utils/jsonpath.js +175 -0
  56. package/changesets/utils/logger.js +50 -0
  57. package/changesets/utils/markdown-link.js +57 -0
  58. package/changesets/utils/publishability.js +39 -0
  59. package/changesets/utils/remark-pipeline.js +79 -0
  60. package/changesets/utils/section-parser.js +94 -0
  61. package/changesets/utils/strip-frontmatter.js +46 -0
  62. package/changesets/utils/version-blocks.js +108 -0
  63. package/changesets/utils/version-files.js +336 -0
  64. package/changesets/utils/worktree-snapshot.js +142 -0
  65. package/changesets/vendor/github-info.js +55 -0
  66. package/commitlint/config/factory.js +69 -0
  67. package/commitlint/config/plugins.js +227 -0
  68. package/commitlint/config/rules.js +155 -0
  69. package/commitlint/config/schema.js +46 -0
  70. package/commitlint/detection/dco.js +53 -0
  71. package/commitlint/detection/scopes.js +45 -0
  72. package/commitlint/formatter/format.js +85 -0
  73. package/commitlint/formatter/messages.js +79 -0
  74. package/commitlint/hook/diagnostics/branch.js +36 -0
  75. package/commitlint/hook/diagnostics/cache.js +37 -0
  76. package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
  77. package/commitlint/hook/diagnostics/open-issues.js +56 -0
  78. package/commitlint/hook/diagnostics/package-manager.js +51 -0
  79. package/commitlint/hook/diagnostics/signing.js +107 -0
  80. package/commitlint/hook/envelope.js +46 -0
  81. package/commitlint/hook/output.js +45 -0
  82. package/commitlint/hook/parse-bash-command.js +105 -0
  83. package/commitlint/hook/rules/closes-trailer.js +31 -0
  84. package/commitlint/hook/rules/forbidden-content.js +32 -0
  85. package/commitlint/hook/rules/plan-leakage.js +36 -0
  86. package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
  87. package/commitlint/hook/rules/soft-wrap.js +37 -0
  88. package/commitlint/hook/rules/types.js +14 -0
  89. package/commitlint/hook/rules/verbosity.js +31 -0
  90. package/commitlint/hook/silence-logger.js +39 -0
  91. package/commitlint/index.js +146 -0
  92. package/commitlint/prompt/config.js +91 -0
  93. package/commitlint/prompt/emojis.js +74 -0
  94. package/commitlint/prompt/prompter.js +135 -0
  95. package/commitlint/static.js +73 -0
  96. package/errors/BiomeSyncError.js +21 -0
  97. package/errors/ChangesetConfigError.js +20 -0
  98. package/errors/ConfigNotFoundError.js +21 -0
  99. package/errors/SectionParseError.js +16 -0
  100. package/errors/SectionValidationError.js +16 -0
  101. package/errors/SectionWriteError.js +16 -0
  102. package/errors/TagFormatError.js +20 -0
  103. package/errors/ToolNotFoundError.js +11 -0
  104. package/errors/ToolResolutionError.js +11 -0
  105. package/errors/ToolVersionMismatchError.js +11 -0
  106. package/errors/VersioningDetectionError.js +20 -0
  107. package/errors/WorkspaceAnalysisError.js +21 -0
  108. package/index.d.ts +9743 -8380
  109. package/index.js +36 -6657
  110. package/lint/Handler.js +39 -0
  111. package/lint/cli/sections.js +65 -0
  112. package/lint/cli/templates/markdownlint.gen.js +183 -0
  113. package/lint/config/Preset.js +152 -0
  114. package/lint/config/createConfig.js +89 -0
  115. package/lint/handlers/Biome.js +179 -0
  116. package/lint/handlers/Markdown.js +139 -0
  117. package/lint/handlers/PackageJson.js +130 -0
  118. package/lint/handlers/PnpmWorkspace.js +141 -0
  119. package/lint/handlers/ShellScripts.js +58 -0
  120. package/lint/handlers/TypeScript.js +134 -0
  121. package/lint/handlers/Yaml.js +167 -0
  122. package/lint/index.js +52 -0
  123. package/lint/utils/Command.js +285 -0
  124. package/lint/utils/Filter.js +100 -0
  125. package/lint/utils/Workspace.js +86 -0
  126. package/package.json +52 -63
  127. package/schemas/CommentStyle.js +16 -0
  128. package/schemas/ResolvedTool.js +63 -0
  129. package/schemas/SavvySections.js +113 -0
  130. package/schemas/SectionBlock.js +70 -0
  131. package/schemas/SectionDefinition.js +121 -0
  132. package/schemas/SectionResults.js +12 -0
  133. package/schemas/TagStrategySchemas.js +18 -0
  134. package/schemas/ToolDefinition.js +39 -0
  135. package/schemas/ToolResults.js +14 -0
  136. package/schemas/VersioningSchemas.js +95 -0
  137. package/schemas/WorkspaceAnalysisSchemas.js +190 -0
  138. package/services/BiomeSchemaSync.js +133 -0
  139. package/services/ChangesetConfig.js +78 -0
  140. package/services/ChangesetConfigReader.js +106 -0
  141. package/services/ConfigDiscovery.js +71 -0
  142. package/services/ManagedSection.js +288 -0
  143. package/services/SilkPublishability.js +193 -0
  144. package/services/SilkWorkspaceAnalyzer.js +213 -0
  145. package/services/TagStrategy.js +54 -0
  146. package/services/ToolDiscovery.js +229 -0
  147. package/services/VersioningStrategy.js +67 -0
  148. package/tsdoc-metadata.json +11 -11
  149. package/turbo/digest.js +127 -0
  150. package/turbo/errors.js +48 -0
  151. package/turbo/index.js +32 -0
  152. package/turbo/schemas/DryRun.js +57 -0
  153. package/turbo/schemas/results.js +61 -0
  154. package/turbo/services/TurboInspector.js +100 -0
  155. package/utils/ToolCommand.js +40 -0
@@ -0,0 +1,182 @@
1
+ import { GitError } from "../errors.js";
2
+ import { Context, Effect, Layer } from "effect";
3
+ import { execFileSync } from "node:child_process";
4
+
5
+ //#region src/changesets/services/workspace-snapshot.ts
6
+ /**
7
+ * Read workspace package snapshots at arbitrary git refs.
8
+ *
9
+ * @remarks
10
+ * `WorkspaceDiscovery` reads the **current** workspace state. To compute
11
+ * dependency diffs between two points in time, we also need to read
12
+ * `pnpm-workspace.yaml` and each `package.json` as they existed at a
13
+ * specific commit. This service shells out to `git show <ref>:<path>`
14
+ * for each file, returns a plain-object snapshot per workspace package,
15
+ * and caches per `(cwd, ref)` pair.
16
+ *
17
+ * The snapshot intentionally returns plain objects rather than
18
+ * `WorkspacePackage` instances — `WorkspacePackage` is a `Schema.Class`
19
+ * tightly coupled to the live filesystem, and snapshot consumers only
20
+ * need the declared dependency records to compute a diff.
21
+ *
22
+ * @see {@link WorkspaceSnapshotReader} for the service tag
23
+ * @see {@link WorkspaceSnapshotReaderLive} for the production layer
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ const _tag = Context.Tag("WorkspaceSnapshotReader");
28
+ /**
29
+ * @internal
30
+ */
31
+ const WorkspaceSnapshotReaderBase = _tag();
32
+ /**
33
+ * Effect service tag for {@link WorkspaceSnapshotReaderShape}.
34
+ *
35
+ * @public
36
+ */
37
+ var WorkspaceSnapshotReader = class extends WorkspaceSnapshotReaderBase {};
38
+ function runGitShow(cwd, ref, path) {
39
+ return Effect.try({
40
+ try: () => execFileSync("git", ["show", `${ref}:${path}`], {
41
+ cwd,
42
+ encoding: "utf8",
43
+ stdio: [
44
+ "ignore",
45
+ "pipe",
46
+ "pipe"
47
+ ]
48
+ }),
49
+ catch: (error) => {
50
+ const stderr = error.stderr;
51
+ const text = typeof stderr === "string" ? stderr : stderr?.toString() ?? "";
52
+ if (/exists on disk, but not in|does not exist|unknown revision|bad object/.test(text)) return new GitError({
53
+ command: `git show ${ref}:${path}`,
54
+ cwd,
55
+ reason: "PATH_NOT_AT_REF"
56
+ });
57
+ return new GitError({
58
+ command: `git show ${ref}:${path}`,
59
+ cwd,
60
+ reason: text.trim() || (error.message ?? String(error))
61
+ });
62
+ }
63
+ }).pipe(Effect.catchTag("GitError", (err) => err.reason === "PATH_NOT_AT_REF" ? Effect.succeed(null) : Effect.fail(err)));
64
+ }
65
+ /**
66
+ * Parse a minimal `pnpm-workspace.yaml` (`packages:` list only). Tolerant
67
+ * of comments and varied indentation; rejects on missing `packages:` key.
68
+ */
69
+ function parseWorkspaceGlobs(yamlText) {
70
+ const lines = yamlText.split(/\r?\n/);
71
+ const globs = [];
72
+ let inPackagesBlock = false;
73
+ for (const line of lines) {
74
+ if (/^\s*#/.test(line)) continue;
75
+ if (/^\s*packages\s*:\s*$/.test(line)) {
76
+ inPackagesBlock = true;
77
+ continue;
78
+ }
79
+ if (inPackagesBlock) {
80
+ const match = line.match(/^\s+-\s+["']?(.+?)["']?\s*$/);
81
+ if (match) {
82
+ globs.push(match[1]);
83
+ continue;
84
+ }
85
+ if (line.length > 0 && !line.startsWith(" ") && !line.startsWith(" ")) inPackagesBlock = false;
86
+ }
87
+ }
88
+ return globs;
89
+ }
90
+ function toSnapshot(pkg, relativePath) {
91
+ if (!pkg.name) return null;
92
+ return {
93
+ name: pkg.name,
94
+ relativePath,
95
+ version: pkg.version ?? "0.0.0",
96
+ dependencies: pkg.dependencies ?? {},
97
+ devDependencies: pkg.devDependencies ?? {},
98
+ peerDependencies: pkg.peerDependencies ?? {},
99
+ optionalDependencies: pkg.optionalDependencies ?? {}
100
+ };
101
+ }
102
+ /**
103
+ * Expand a workspace glob like `packages/*` or `apps/web` against the
104
+ * directories present at the given git ref. We can't `globSync` here
105
+ * (the directories may not be on disk at this ref); instead we use
106
+ * `git ls-tree` to enumerate paths.
107
+ */
108
+ function expandGlobAtRef(cwd, ref, glob) {
109
+ return Effect.gen(function* () {
110
+ const cleanGlob = glob.replace(/\/\*\*$/, "/*");
111
+ if (!cleanGlob.includes("*") && !cleanGlob.includes("?")) return [cleanGlob];
112
+ const prefix = cleanGlob.includes("/") ? cleanGlob.slice(0, cleanGlob.lastIndexOf("/") + 1) : "";
113
+ const entries = (yield* Effect.try({
114
+ try: () => execFileSync("git", [
115
+ "ls-tree",
116
+ "--name-only",
117
+ ref,
118
+ prefix
119
+ ], {
120
+ cwd,
121
+ encoding: "utf8",
122
+ stdio: [
123
+ "ignore",
124
+ "pipe",
125
+ "pipe"
126
+ ]
127
+ }),
128
+ catch: (error) => {
129
+ const stderr = error.stderr;
130
+ const text = typeof stderr === "string" ? stderr : stderr?.toString() ?? "";
131
+ return new GitError({
132
+ command: `git ls-tree ${ref} ${prefix}`,
133
+ cwd,
134
+ reason: text.trim() || (error.message ?? String(error))
135
+ });
136
+ }
137
+ })).split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
138
+ const regex = new RegExp(`^${cleanGlob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`);
139
+ return entries.filter((e) => regex.test(e));
140
+ });
141
+ }
142
+ function makeShape() {
143
+ const cache = /* @__PURE__ */ new Map();
144
+ const snapshotAt = (cwd, ref) => Effect.gen(function* () {
145
+ const cacheKey = `${cwd}::${ref}`;
146
+ const cached = cache.get(cacheKey);
147
+ if (cached) return cached;
148
+ const wsYaml = yield* runGitShow(cwd, ref, "pnpm-workspace.yaml");
149
+ const globs = wsYaml ? parseWorkspaceGlobs(wsYaml) : [];
150
+ const dirs = [];
151
+ for (const glob of globs) {
152
+ const expanded = yield* expandGlobAtRef(cwd, ref, glob);
153
+ for (const d of expanded) if (!dirs.includes(d)) dirs.push(d);
154
+ }
155
+ if (!dirs.includes(".")) dirs.unshift(".");
156
+ const snapshots = [];
157
+ for (const dir of dirs) {
158
+ const pkgText = yield* runGitShow(cwd, ref, dir === "." ? "package.json" : `${dir}/package.json`);
159
+ if (!pkgText) continue;
160
+ let parsed;
161
+ try {
162
+ parsed = JSON.parse(pkgText);
163
+ } catch {
164
+ continue;
165
+ }
166
+ const snap = toSnapshot(parsed, dir);
167
+ if (snap) snapshots.push(snap);
168
+ }
169
+ cache.set(cacheKey, snapshots);
170
+ return snapshots;
171
+ });
172
+ return { snapshotAt };
173
+ }
174
+ /**
175
+ * Production layer for {@link WorkspaceSnapshotReader}.
176
+ *
177
+ * @public
178
+ */
179
+ const WorkspaceSnapshotReaderLive = Layer.succeed(WorkspaceSnapshotReader, makeShape());
180
+
181
+ //#endregion
182
+ export { WorkspaceSnapshotReader, WorkspaceSnapshotReaderBase, WorkspaceSnapshotReaderLive };
@@ -0,0 +1,80 @@
1
+ //#region src/changesets/utils/commit-parser.ts
2
+ /**
3
+ * Conventional commit message parsing.
4
+ *
5
+ * Implements a parser for the
6
+ * {@link https://www.conventionalcommits.org/ | Conventional Commits}
7
+ * specification, extracting type, scope, breaking-change indicator,
8
+ * description, and body from commit messages.
9
+ *
10
+ * @remarks
11
+ * The parser uses a single regex pass for the first line, then splits
12
+ * remaining lines to extract the body. Non-conventional messages are
13
+ * returned with only the `description` field populated (set to the
14
+ * full message text).
15
+ *
16
+ * @see {@link Changelog} for the public API that consumes parsed commits
17
+ *
18
+ * @internal
19
+ */
20
+ /**
21
+ * Matches conventional commit format: `type(scope)!: description`.
22
+ *
23
+ * Capture groups:
24
+ * - `[1]` — commit type (e.g., `feat`, `fix`)
25
+ * - `[2]` — optional scope (e.g., `api`, `ui`)
26
+ * - `[3]` — optional `!` breaking-change indicator
27
+ * - `[4]` — description text after the colon
28
+ *
29
+ * @internal
30
+ */
31
+ const CONVENTIONAL_COMMIT_PATTERN = /^(\w+)(?:\(([^)]+)\))?(!)?\s*:\s*(.+)/;
32
+ /**
33
+ * Parse a commit message following the Conventional Commits specification.
34
+ *
35
+ * Supports the full format: `type(scope)!: description\n\nbody`
36
+ *
37
+ * @remarks
38
+ * The algorithm first attempts to match the first line against the
39
+ * conventional commit pattern. If matched, the type, optional scope,
40
+ * optional breaking indicator, and description are extracted. The body
41
+ * is everything after the first line, trimmed. If the message does not
42
+ * match, a fallback result with only the `description` set to the raw
43
+ * message is returned.
44
+ *
45
+ * @param message - The commit message to parse
46
+ * @returns Parsed components; only `description` is guaranteed present
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { parseCommitMessage } from "../utils/commit-parser.js";
51
+ *
52
+ * const result = parseCommitMessage("feat(api)!: add v2 routes\n\nBREAKING CHANGE: v1 removed");
53
+ * // result.type === "feat"
54
+ * // result.scope === "api"
55
+ * // result.breaking === true
56
+ * // result.description === "add v2 routes"
57
+ * // result.body === "BREAKING CHANGE: v1 removed"
58
+ * ```
59
+ *
60
+ * @internal
61
+ */
62
+ function parseCommitMessage(message) {
63
+ const match = CONVENTIONAL_COMMIT_PATTERN.exec(message);
64
+ if (match) {
65
+ const [, type, scope, bang, description] = match;
66
+ const body = message.split("\n").slice(1).join("\n").trim();
67
+ const result = {
68
+ type,
69
+ description: description.trim()
70
+ };
71
+ if (scope) result.scope = scope;
72
+ if (bang === "!") result.breaking = true;
73
+ if (body) result.body = body;
74
+ return result;
75
+ }
76
+ return { description: message };
77
+ }
78
+
79
+ //#endregion
80
+ export { parseCommitMessage };
@@ -0,0 +1,77 @@
1
+ import { sortDependencyRows } from "./dependency-table.js";
2
+
3
+ //#region src/changesets/utils/dep-diff.ts
4
+ const EM_DASH = "—";
5
+ const DEP_TYPE_MAP = [
6
+ ["dependencies", "dependency"],
7
+ ["devDependencies", "devDependency"],
8
+ ["peerDependencies", "peerDependency"],
9
+ ["optionalDependencies", "optionalDependency"]
10
+ ];
11
+ function diffOneRecord(before, after, type) {
12
+ const rows = [];
13
+ const seen = /* @__PURE__ */ new Set();
14
+ for (const [name, beforeVersion] of Object.entries(before)) {
15
+ seen.add(name);
16
+ const afterVersion = after[name];
17
+ if (afterVersion === void 0) rows.push({
18
+ dependency: name,
19
+ type,
20
+ action: "removed",
21
+ from: beforeVersion,
22
+ to: EM_DASH
23
+ });
24
+ else if (afterVersion !== beforeVersion) rows.push({
25
+ dependency: name,
26
+ type,
27
+ action: "updated",
28
+ from: beforeVersion,
29
+ to: afterVersion
30
+ });
31
+ }
32
+ for (const [name, afterVersion] of Object.entries(after)) {
33
+ if (seen.has(name)) continue;
34
+ rows.push({
35
+ dependency: name,
36
+ type,
37
+ action: "added",
38
+ from: EM_DASH,
39
+ to: afterVersion
40
+ });
41
+ }
42
+ return rows;
43
+ }
44
+ /**
45
+ * Diff two workspace snapshots and return per-package dependency-table rows.
46
+ *
47
+ * @param before - Snapshot at the older ref (typically the merge base). Pass
48
+ * `null` for workspace packages that did not exist at the older ref — every
49
+ * declared dep is then reported as `"added"`.
50
+ * @param after - Snapshot at the newer ref (typically the working tree).
51
+ * @returns One {@link WorkspaceDependencyDiff} entry per workspace package
52
+ * that has at least one row. Packages with no changes are omitted.
53
+ *
54
+ * @public
55
+ */
56
+ function computeWorkspaceDependencyDiffs(beforeSnapshots, afterSnapshots) {
57
+ const beforeByName = new Map(beforeSnapshots.map((s) => [s.name, s]));
58
+ const result = [];
59
+ for (const after of afterSnapshots) {
60
+ const before = beforeByName.get(after.name);
61
+ const rows = [];
62
+ for (const [field, type] of DEP_TYPE_MAP) {
63
+ const beforeRecord = before?.[field] ?? {};
64
+ const afterRecord = after[field];
65
+ rows.push(...diffOneRecord(beforeRecord, afterRecord, type));
66
+ }
67
+ if (rows.length > 0) result.push({
68
+ package: after.name,
69
+ relativePath: after.relativePath,
70
+ rows: sortDependencyRows(rows)
71
+ });
72
+ }
73
+ return result;
74
+ }
75
+
76
+ //#endregion
77
+ export { computeWorkspaceDependencyDiffs };
@@ -0,0 +1,347 @@
1
+ import { DependencyTableRowSchema } from "../schemas/dependency-table.js";
2
+ import { Schema } from "effect";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkStringify from "remark-stringify";
5
+ import { unified } from "unified";
6
+ import { toString } from "mdast-util-to-string";
7
+
8
+ //#region src/changesets/utils/dependency-table.ts
9
+ /**
10
+ * Dependency table utilities for parsing, serializing, collapsing, and sorting
11
+ * dependency table rows between MDAST table nodes and typed representations.
12
+ *
13
+ * @remarks
14
+ * This module provides the low-level functional primitives that the
15
+ * {@link DependencyTable} class wraps with a stateful, fluent API.
16
+ * It operates on arrays of `DependencyTableRow` objects and MDAST `Table`
17
+ * nodes, converting between the two representations.
18
+ *
19
+ * The collapse algorithm merges rows sharing the same `dependency + type`
20
+ * key, applying semantic rules (e.g., added then removed = net zero).
21
+ * Sorting follows a stable order: removed, updated, added, then
22
+ * alphabetically by type and dependency name.
23
+ *
24
+ * @see {@link DependencyTable} for the public class-based API
25
+ *
26
+ * @internal
27
+ */
28
+ /**
29
+ * Ordered column headers for dependency tables.
30
+ *
31
+ * Defines the canonical header row: `Dependency | Type | Action | From | To`.
32
+ *
33
+ * @internal
34
+ */
35
+ const COLUMN_HEADERS = [
36
+ "Dependency",
37
+ "Type",
38
+ "Action",
39
+ "From",
40
+ "To"
41
+ ];
42
+ /**
43
+ * Property keys on {@link DependencyTableRow} corresponding to each column
44
+ * in {@link COLUMN_HEADERS}.
45
+ *
46
+ * Used to map between positional table cells and typed object fields.
47
+ *
48
+ * @internal
49
+ */
50
+ const COLUMN_KEYS = [
51
+ "dependency",
52
+ "type",
53
+ "action",
54
+ "from",
55
+ "to"
56
+ ];
57
+ /** Schema decoder for validating raw cell values into a typed row. */
58
+ const decode = Schema.decodeUnknownSync(DependencyTableRowSchema);
59
+ /**
60
+ * Parse an MDAST table node into validated dependency table rows.
61
+ *
62
+ * @remarks
63
+ * Validates the header row against {@link COLUMN_HEADERS} (case-insensitive),
64
+ * then decodes each data row through the `DependencyTableRowSchema` for
65
+ * type-safe validation of action values, version strings, and dependency types.
66
+ *
67
+ * @param table - An MDAST `Table` node (from remark-gfm)
68
+ * @returns Array of validated `DependencyTableRow` objects
69
+ * @throws If the table has fewer than 2 rows (header + at least one data row)
70
+ * @throws If the header columns do not match the expected column names
71
+ * @throws If any data row fails schema validation
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * import { parseDependencyTable } from "../utils/dependency-table.js";
76
+ * import { parseMarkdown } from "../utils/remark-pipeline.js";
77
+ * import type { Table } from "mdast";
78
+ *
79
+ * const tree = parseMarkdown("| Dependency | Type | Action | From | To |\n| --- | --- | --- | --- | --- |\n| foo | dependency | added | — | 1.0.0 |");
80
+ * const table = tree.children.find((n) => n.type === "table") as Table;
81
+ * const rows = parseDependencyTable(table);
82
+ * // rows[0].dependency === "foo"
83
+ * ```
84
+ *
85
+ * @internal
86
+ */
87
+ function parseDependencyTable(table) {
88
+ const rows = table.children;
89
+ if (rows.length < 2) throw new Error("Dependency table must have at least one data row");
90
+ const headers = rows[0].children.map((cell) => toString(cell).trim().toLowerCase());
91
+ const expected = COLUMN_HEADERS.map((h) => h.toLowerCase());
92
+ if (headers.length !== expected.length || !headers.every((h, i) => h === expected[i])) throw new Error(`Table must have columns: ${COLUMN_HEADERS.join(", ")}. Got: ${headers.join(", ")}`);
93
+ const result = [];
94
+ for (let i = 1; i < rows.length; i++) {
95
+ const cells = rows[i].children;
96
+ const raw = {};
97
+ for (let c = 0; c < COLUMN_KEYS.length; c++) raw[COLUMN_KEYS[c]] = toString(cells[c]).trim();
98
+ result.push(decode(raw));
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Create a table cell with a text node.
104
+ *
105
+ * @param text - The cell text content
106
+ * @returns An MDAST `TableCell` node
107
+ *
108
+ * @internal
109
+ */
110
+ function makeCell(text) {
111
+ return {
112
+ type: "tableCell",
113
+ children: [{
114
+ type: "text",
115
+ value: text
116
+ }]
117
+ };
118
+ }
119
+ /**
120
+ * Create a table row from an array of cell texts.
121
+ *
122
+ * @param texts - Array of cell text values
123
+ * @returns An MDAST `TableRow` node
124
+ *
125
+ * @internal
126
+ */
127
+ function makeRow(texts) {
128
+ return {
129
+ type: "tableRow",
130
+ children: texts.map(makeCell)
131
+ };
132
+ }
133
+ /**
134
+ * Serialize dependency table rows into an MDAST `Table` node.
135
+ *
136
+ * @remarks
137
+ * Creates a well-formed GFM table with the canonical header row
138
+ * ({@link COLUMN_HEADERS}) followed by one data row per entry.
139
+ * The inverse of {@link parseDependencyTable}.
140
+ *
141
+ * @param rows - Array of `DependencyTableRow` objects
142
+ * @returns An MDAST `Table` node ready for insertion into an AST
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * import { serializeDependencyTable } from "../utils/dependency-table.js";
147
+ * import type { DependencyTableRow } from "../schemas/dependency-table.js";
148
+ *
149
+ * const rows: DependencyTableRow[] = [
150
+ * { dependency: "effect", type: "dependency", action: "updated", from: "3.18.0", to: "3.19.0" },
151
+ * ];
152
+ * const table = serializeDependencyTable(rows);
153
+ * // table.type === "table"
154
+ * // table.children.length === 2 (header + 1 data row)
155
+ * ```
156
+ *
157
+ * @internal
158
+ */
159
+ function serializeDependencyTable(rows) {
160
+ return {
161
+ type: "table",
162
+ children: [makeRow([...COLUMN_HEADERS]), ...rows.map((row) => makeRow(COLUMN_KEYS.map((key) => row[key])))]
163
+ };
164
+ }
165
+ /**
166
+ * Serialize dependency table rows to a markdown table string.
167
+ *
168
+ * @remarks
169
+ * Combines {@link serializeDependencyTable} with unified/remark-gfm/remark-stringify
170
+ * to produce a ready-to-use GFM markdown table string. The result is trimmed
171
+ * of leading/trailing whitespace.
172
+ *
173
+ * @param rows - Array of `DependencyTableRow` objects
174
+ * @returns Markdown table string (GFM format)
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * import { serializeDependencyTableToMarkdown } from "../utils/dependency-table.js";
179
+ * import type { DependencyTableRow } from "../schemas/dependency-table.js";
180
+ *
181
+ * const rows: DependencyTableRow[] = [
182
+ * { dependency: "effect", type: "dependency", action: "updated", from: "3.18.0", to: "3.19.0" },
183
+ * ];
184
+ * const md = serializeDependencyTableToMarkdown(rows);
185
+ * // "| Dependency | Type | Action | From | To |\n| --- | --- | ..."
186
+ * ```
187
+ *
188
+ * @internal
189
+ */
190
+ function serializeDependencyTableToMarkdown(rows) {
191
+ const tree = {
192
+ type: "root",
193
+ children: [serializeDependencyTable(rows)]
194
+ };
195
+ return unified().use(remarkGfm).use(remarkStringify).stringify(tree).trim();
196
+ }
197
+ /**
198
+ * Sort priority for dependency actions: removed first, then updated, then added.
199
+ *
200
+ * @remarks
201
+ * Used by {@link sortDependencyRows} for primary sort ordering. The numeric
202
+ * values define the sort position (lower = earlier in the sorted output).
203
+ *
204
+ * @internal
205
+ */
206
+ const ACTION_ORDER = {
207
+ removed: 0,
208
+ updated: 1,
209
+ added: 2
210
+ };
211
+ /**
212
+ * Collapse dependency table rows with the same `dependency + type` key.
213
+ *
214
+ * @remarks
215
+ * When multiple changelog entries affect the same dependency, this function
216
+ * merges them into a single row using semantic collapse rules. Rows are
217
+ * grouped by a composite key of `dependency\0type` (null-byte separated).
218
+ *
219
+ * The collapse rules applied by {@link collapseTwo} are:
220
+ *
221
+ * | First action | Second action | Result |
222
+ * |---|---|---|
223
+ * | `updated` | `updated` | `updated` (earliest `from`, latest `to`) |
224
+ * | `added` | `updated` | `added` (final `to`) |
225
+ * | `added` | `removed` | dropped (net zero change) |
226
+ * | `updated` | `removed` | `removed` (original `from`) |
227
+ * | `removed` | `added` | `updated` (original `from`, new `to`) |
228
+ * | other | other | keep later entry |
229
+ *
230
+ * @param rows - Array of `DependencyTableRow` objects (in chronological order)
231
+ * @returns Collapsed array with at most one row per `dependency + type` pair
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * import { collapseDependencyRows } from "../utils/dependency-table.js";
236
+ * import type { DependencyTableRow } from "../schemas/dependency-table.js";
237
+ *
238
+ * const rows: DependencyTableRow[] = [
239
+ * { dependency: "effect", type: "dependency", action: "updated", from: "3.17.0", to: "3.18.0" },
240
+ * { dependency: "effect", type: "dependency", action: "updated", from: "3.18.0", to: "3.19.0" },
241
+ * ];
242
+ * const collapsed = collapseDependencyRows(rows);
243
+ * // collapsed[0].from === "3.17.0", collapsed[0].to === "3.19.0"
244
+ * ```
245
+ *
246
+ * @see {@link DependencyTable} for the public API that wraps this function
247
+ *
248
+ * @internal
249
+ */
250
+ function collapseDependencyRows(rows) {
251
+ const groups = /* @__PURE__ */ new Map();
252
+ for (const row of rows) {
253
+ const key = `${row.dependency}\0${row.type}`;
254
+ const existing = groups.get(key);
255
+ if (!existing) {
256
+ groups.set(key, { ...row });
257
+ continue;
258
+ }
259
+ const merged = collapseTwo(existing, row);
260
+ if (merged === null) groups.delete(key);
261
+ else groups.set(key, merged);
262
+ }
263
+ return [...groups.values()];
264
+ }
265
+ /**
266
+ * Collapse two rows sharing the same `dependency + type` key into one,
267
+ * or return `null` if they cancel out (net zero).
268
+ *
269
+ * @remarks
270
+ * This is the core merge function called by {@link collapseDependencyRows}.
271
+ * It applies the semantic collapse rules based on the action pair.
272
+ * See {@link collapseDependencyRows} for the full rule table.
273
+ *
274
+ * @param first - The earlier (existing) row
275
+ * @param second - The later (incoming) row
276
+ * @returns The merged row, or `null` to drop the entry entirely
277
+ *
278
+ * @internal
279
+ */
280
+ function collapseTwo(first, second) {
281
+ const a = first.action;
282
+ const b = second.action;
283
+ if (a === "updated" && b === "updated") return {
284
+ ...first,
285
+ to: second.to
286
+ };
287
+ if (a === "added" && b === "updated") return {
288
+ ...first,
289
+ to: second.to
290
+ };
291
+ if (a === "added" && b === "removed") return null;
292
+ if (a === "updated" && b === "removed") return {
293
+ ...first,
294
+ action: "removed",
295
+ to: "—"
296
+ };
297
+ if (a === "removed" && b === "added") return {
298
+ ...first,
299
+ action: "updated",
300
+ to: second.to
301
+ };
302
+ return { ...second };
303
+ }
304
+ /**
305
+ * Sort dependency table rows into canonical display order.
306
+ *
307
+ * @remarks
308
+ * Applies a three-level stable sort:
309
+ * 1. **Action** — `removed` first, then `updated`, then `added`
310
+ * (per {@link ACTION_ORDER})
311
+ * 2. **Type** — alphabetically (e.g., `config` before `dependency`)
312
+ * 3. **Dependency name** — alphabetically within each action+type group
313
+ *
314
+ * Returns a new array; the input is not mutated.
315
+ *
316
+ * @param rows - Array of `DependencyTableRow` objects
317
+ * @returns New sorted array
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * import { sortDependencyRows } from "../utils/dependency-table.js";
322
+ * import type { DependencyTableRow } from "../schemas/dependency-table.js";
323
+ *
324
+ * const rows: DependencyTableRow[] = [
325
+ * { dependency: "zod", type: "dependency", action: "added", from: "\u2014", to: "3.0.0" },
326
+ * { dependency: "effect", type: "dependency", action: "removed", from: "3.19.0", to: "\u2014" },
327
+ * ];
328
+ * const sorted = sortDependencyRows(rows);
329
+ * // sorted[0].dependency === "effect" (removed comes first)
330
+ * ```
331
+ *
332
+ * @see {@link DependencyTable} for the public API that wraps this function
333
+ *
334
+ * @internal
335
+ */
336
+ function sortDependencyRows(rows) {
337
+ return [...rows].sort((a, b) => {
338
+ const actionDiff = (ACTION_ORDER[a.action] ?? 99) - (ACTION_ORDER[b.action] ?? 99);
339
+ if (actionDiff !== 0) return actionDiff;
340
+ const typeDiff = a.type.localeCompare(b.type);
341
+ if (typeDiff !== 0) return typeDiff;
342
+ return a.dependency.localeCompare(b.dependency);
343
+ });
344
+ }
345
+
346
+ //#endregion
347
+ export { collapseDependencyRows, parseDependencyTable, serializeDependencyTable, serializeDependencyTableToMarkdown, sortDependencyRows };