@savvy-web/silk-effects 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,390 @@
1
+ import { ConfigurationError } from "../errors.js";
2
+ import { ChangesetOptionsSchema } from "../schemas/options.js";
3
+ import { ChangesetConfigReader } from "../../services/ChangesetConfigReader.js";
4
+ import { Context, Effect, Layer, Schema } from "effect";
5
+ import { isAbsolute, join, relative, resolve } from "node:path";
6
+ import { globSync } from "tinyglobby";
7
+ import { WorkspaceDiscovery } from "workspaces-effect";
8
+
9
+ //#region src/changesets/services/config-inspector.ts
10
+ /**
11
+ * `ConfigInspector` service — surface the project's `.changeset/config.json`
12
+ * in a structured, validated form that agents and tooling can consume.
13
+ *
14
+ * @remarks
15
+ * The inspector handles three responsibilities that the schemas alone cannot:
16
+ *
17
+ * 1. **Legacy normalization.** Configs from 0.8.x still use the flat
18
+ * top-level `versionFiles[]` array. When that field is populated, the
19
+ * inspector emits a one-line deprecation warning to stderr (naming the
20
+ * config path and the required edit) and folds each legacy entry into
21
+ * the equivalent `packages[entry.package].versionFiles` shape for
22
+ * downstream consumers. Removed in 1.0.0.
23
+ * 2. **Resolution against the workspace.** Each `packages` key must resolve
24
+ * to a known workspace package via `WorkspaceDiscovery`. Unknown keys
25
+ * surface as a {@link ConfigurationError}. The inspector returns the
26
+ * package's absolute workspace directory alongside the resolved scope.
27
+ * 3. **Overlap detection.** Cross-package validation rules that the schemas
28
+ * cannot enforce:
29
+ * - `additionalScopes` of two different packages must not overlap.
30
+ * - `additionalScopes` of one package must not shadow another package's
31
+ * workspace directory.
32
+ * - Two `versionFiles` entries (within or across packages) must not
33
+ * resolve to the same `(file, $.path)` tuple.
34
+ *
35
+ * Validation is performed eagerly on the first {@link ConfigInspectorShape.inspect}
36
+ * call. The resolved state is cached internally per `cwd` so subsequent
37
+ * {@link ConfigInspectorShape.classify} calls reuse it.
38
+ *
39
+ * @see {@link ConfigInspector} for the Effect service tag
40
+ * @see {@link ConfigInspectorLive} for the production layer
41
+ *
42
+ * @packageDocumentation
43
+ */
44
+ const _tag = Context.Tag("ConfigInspector");
45
+ /**
46
+ * Base class for {@link ConfigInspector}.
47
+ *
48
+ * @privateRemarks
49
+ * Effect's `Context.Tag` creates an anonymous base class that api-extractor
50
+ * cannot follow without an explicit export. Do not delete.
51
+ *
52
+ * @internal
53
+ */
54
+ const ConfigInspectorBase = _tag();
55
+ /**
56
+ * Effect service tag for {@link ConfigInspectorShape}.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { Effect } from "effect";
61
+ * import { ConfigInspector, ConfigInspectorLive } from "@savvy-web/changesets";
62
+ *
63
+ * const program = Effect.gen(function* () {
64
+ * const inspector = yield* ConfigInspector;
65
+ * const config = yield* inspector.inspect(process.cwd());
66
+ * return config.packages.map((p) => p.name);
67
+ * });
68
+ *
69
+ * Effect.runPromise(program.pipe(Effect.provide(ConfigInspectorLive)));
70
+ * ```
71
+ *
72
+ * @public
73
+ */
74
+ var ConfigInspector = class extends ConfigInspectorBase {};
75
+ /**
76
+ * Pull the changelog formatter ID and its options object out of the raw
77
+ * `.changeset/config.json` shape (where `changelog` may be a tuple, a string,
78
+ * or absent).
79
+ */
80
+ function extractChangelogOptions(config) {
81
+ const { changelog } = config;
82
+ if (Array.isArray(changelog)) return {
83
+ changelogId: typeof changelog[0] === "string" ? changelog[0] : null,
84
+ options: changelog[1] ?? {}
85
+ };
86
+ if (typeof changelog === "string") return {
87
+ changelogId: changelog,
88
+ options: {}
89
+ };
90
+ return {
91
+ changelogId: null,
92
+ options: {}
93
+ };
94
+ }
95
+ /**
96
+ * Normalize the deprecated top-level `versionFiles[]` array into the
97
+ * equivalent `packages[entry.package].versionFiles` shape. Mutates a fresh
98
+ * copy of the options; never the input.
99
+ *
100
+ * Returns the normalized options and a flag indicating whether normalization
101
+ * happened (so the caller can emit a deprecation warning once per inspect).
102
+ */
103
+ function normalizeLegacyOptions(options, configPath) {
104
+ const legacy = options.versionFiles;
105
+ if (!Array.isArray(legacy) || legacy.length === 0) return {
106
+ normalized: options,
107
+ legacyUsed: false
108
+ };
109
+ console.warn(`[changesets] DEPRECATION: ${configPath} uses the top-level \`versionFiles\` array. Migrate to \`changelog[1].packages[<name>].versionFiles\`. Removed in 1.0.0.`);
110
+ const existing = options.packages ?? {};
111
+ const next = {};
112
+ for (const [k, v] of Object.entries(existing)) next[k] = { ...v };
113
+ for (const entry of legacy) {
114
+ const ownerName = typeof entry.package === "string" ? entry.package : void 0;
115
+ if (!ownerName) throw new ConfigurationError({
116
+ field: "versionFiles",
117
+ reason: `Legacy versionFiles entry { glob: ${JSON.stringify(entry.glob)} } in ${configPath} has no \`package\` field. Path-based owner inference is removed during the 0.9.0 migration — add an explicit \`package\` field, or migrate the entry to \`packages[<name>].versionFiles\`.`
118
+ });
119
+ if (!next[ownerName]) next[ownerName] = {};
120
+ const slot = next[ownerName];
121
+ const arr = Array.isArray(slot.versionFiles) ? slot.versionFiles.slice() : [];
122
+ const cleaned = { glob: entry.glob };
123
+ if (Array.isArray(entry.paths)) cleaned.paths = entry.paths;
124
+ arr.push(cleaned);
125
+ slot.versionFiles = arr;
126
+ }
127
+ const normalized = {
128
+ ...options,
129
+ packages: next
130
+ };
131
+ delete normalized.versionFiles;
132
+ return {
133
+ normalized,
134
+ legacyUsed: true
135
+ };
136
+ }
137
+ /**
138
+ * Materialize a glob against `cwd` and return the matched file paths as
139
+ * repo-relative strings. Honors negation patterns and ignores `node_modules`.
140
+ */
141
+ function materializeGlob(glob, cwd) {
142
+ return globSync(glob, {
143
+ cwd,
144
+ ignore: ["**/node_modules/**"],
145
+ dot: true
146
+ });
147
+ }
148
+ /**
149
+ * Determine whether `child` is the same directory as `parent` or sits inside
150
+ * it. Both paths must be absolute.
151
+ */
152
+ function isInside(parent, child) {
153
+ const rel = relative(parent, child);
154
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
155
+ }
156
+ /**
157
+ * Build per-package resolved scopes by intersecting the (possibly normalized)
158
+ * options with workspace info.
159
+ */
160
+ function buildResolvedScopes(params) {
161
+ const { options, workspaces, projectDir, configPath } = params;
162
+ const packages = options.packages ?? {};
163
+ const workspacesByName = new Map(workspaces.map((w) => [w.name, w]));
164
+ const scopes = [];
165
+ for (const [pkgName, scope] of Object.entries(packages)) {
166
+ const ws = workspacesByName.get(pkgName);
167
+ if (!ws) throw new ConfigurationError({
168
+ field: `packages["${pkgName}"]`,
169
+ reason: `Unknown package "${pkgName}" in ${configPath}. Known workspace packages: ${workspaces.map((w) => w.name).join(", ") || "(none)"}.`
170
+ });
171
+ const additionalScopes = scope.additionalScopes ?? [];
172
+ const additionalScopeFiles = additionalScopes.flatMap((g) => materializeGlob(g, projectDir));
173
+ const resolvedVersionFiles = (scope.versionFiles ?? []).map((entry) => ({
174
+ glob: entry.glob,
175
+ paths: entry.paths ?? ["$.version"],
176
+ matchedFiles: materializeGlob(entry.glob, projectDir).map((rel) => join(projectDir, rel))
177
+ }));
178
+ scopes.push({
179
+ name: pkgName,
180
+ workspaceDir: ws.path,
181
+ version: ws.version,
182
+ additionalScopes,
183
+ additionalScopeFiles: additionalScopeFiles.map((rel) => join(projectDir, rel)),
184
+ versionFiles: resolvedVersionFiles
185
+ });
186
+ }
187
+ return scopes;
188
+ }
189
+ /**
190
+ * Cross-package validation: no overlap in `additionalScopes`, no shadowing
191
+ * of another workspace package's directory (regardless of whether that
192
+ * package is itself declared in `config.packages`), and no duplicate
193
+ * `(file, JSONPath)` tuples in `versionFiles`.
194
+ */
195
+ function checkConflicts(scopes, allWorkspaces, projectDir, configPath) {
196
+ const scopeOwner = /* @__PURE__ */ new Map();
197
+ for (const s of scopes) for (const f of s.additionalScopeFiles) {
198
+ const prev = scopeOwner.get(f);
199
+ if (prev && prev !== s.name) throw new ConfigurationError({
200
+ field: `packages["${s.name}"].additionalScopes`,
201
+ reason: `Overlap in ${configPath}: file ${JSON.stringify(f)} is matched by both "${prev}" and "${s.name}". additionalScopes must not overlap between packages.`
202
+ });
203
+ scopeOwner.set(f, s.name);
204
+ }
205
+ for (const s of scopes) for (const f of s.additionalScopeFiles) for (const ws of allWorkspaces) {
206
+ if (ws.name === s.name) continue;
207
+ if (ws.path === projectDir) continue;
208
+ if (isInside(ws.path, f)) throw new ConfigurationError({
209
+ field: `packages["${s.name}"].additionalScopes`,
210
+ reason: `Shadowing in ${configPath}: "${s.name}" claims ${JSON.stringify(f)} via additionalScopes, but that path is inside "${ws.name}"'s workspace directory (${ws.path}). A package's additionalScopes must not include another package's workspace files.`
211
+ });
212
+ }
213
+ const seen = /* @__PURE__ */ new Map();
214
+ for (const s of scopes) for (const vf of s.versionFiles) for (const file of vf.matchedFiles) for (const path of vf.paths) {
215
+ const key = `${file}::${path}`;
216
+ const prev = seen.get(key);
217
+ if (prev && (prev.pkg !== s.name || prev.glob !== vf.glob)) throw new ConfigurationError({
218
+ field: `packages["${s.name}"].versionFiles`,
219
+ reason: `Conflict in ${configPath}: target (${JSON.stringify(file)}, ${path}) is claimed by both "${prev.pkg}" (glob ${JSON.stringify(prev.glob)}) and "${s.name}" (glob ${JSON.stringify(vf.glob)}). Two versionFiles entries must not resolve to the same (file, JSONPath) tuple.`
220
+ });
221
+ seen.set(key, {
222
+ pkg: s.name,
223
+ glob: vf.glob
224
+ });
225
+ }
226
+ }
227
+ /**
228
+ * Build a `ConfigurationError` from a Schema `ParseError`. Captures the
229
+ * original message verbatim so the user sees exactly what the schema
230
+ * complained about.
231
+ */
232
+ function configErrorFromParseError(parseError, configPath) {
233
+ return new ConfigurationError({
234
+ field: "options",
235
+ reason: `Invalid options in ${configPath}: ${String(parseError)}`
236
+ });
237
+ }
238
+ /**
239
+ * Build a {@link ConfigInspectorShape} that closes over already-resolved
240
+ * service implementations. This keeps the public `inspect`/`classify`
241
+ * signatures requirement-free (`R = never`) while still allowing the
242
+ * implementation to use `ChangesetConfigReader` and `WorkspaceDiscovery`.
243
+ *
244
+ * Each shape carries a private cache keyed by absolute project dir so
245
+ * repeat `inspect`/`classify` calls reuse the materialized state.
246
+ */
247
+ function makeShape(reader, discovery) {
248
+ const cache = /* @__PURE__ */ new Map();
249
+ const inspect = (cwd) => Effect.gen(function* () {
250
+ const projectDir = resolve(cwd);
251
+ const cached = cache.get(projectDir);
252
+ if (cached) return cached;
253
+ const config = yield* reader.read(projectDir).pipe(Effect.mapError((err) => new ConfigurationError({
254
+ field: "configFile",
255
+ reason: err.message
256
+ })));
257
+ const configPath = join(projectDir, ".changeset", "config.json");
258
+ const { changelogId, options: rawOptions } = extractChangelogOptions(config);
259
+ const optionsRecord = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
260
+ if (Array.isArray(optionsRecord.versionFiles) && typeof optionsRecord.packages === "object" && optionsRecord.packages !== null) return yield* Effect.fail(new ConfigurationError({
261
+ field: "options",
262
+ reason: `Configuration in ${configPath} declares both \`packages\` and the deprecated top-level \`versionFiles\` array. Migrate the legacy entries into \`packages[<name>].versionFiles\` and remove the top-level field.`
263
+ }));
264
+ let normalized;
265
+ let legacyUsed;
266
+ try {
267
+ const result = normalizeLegacyOptions(optionsRecord, configPath);
268
+ normalized = result.normalized;
269
+ legacyUsed = result.legacyUsed;
270
+ } catch (e) {
271
+ if (e instanceof ConfigurationError) return yield* Effect.fail(e);
272
+ throw e;
273
+ }
274
+ const decodedOptions = yield* Schema.decodeUnknown(ChangesetOptionsSchema)(normalized).pipe(Effect.mapError((parseError) => configErrorFromParseError(parseError, configPath)));
275
+ const workspaces = (yield* discovery.listPackages(projectDir).pipe(Effect.mapError((err) => new ConfigurationError({
276
+ field: "workspace",
277
+ reason: `Workspace discovery failed for ${projectDir}: ${err.message}`
278
+ })))).map((w) => ({
279
+ name: w.name,
280
+ path: w.path,
281
+ version: w.version
282
+ }));
283
+ let scopes;
284
+ try {
285
+ scopes = buildResolvedScopes({
286
+ options: decodedOptions,
287
+ workspaces,
288
+ projectDir,
289
+ configPath
290
+ });
291
+ checkConflicts(scopes, workspaces, projectDir, configPath);
292
+ } catch (e) {
293
+ if (e instanceof ConfigurationError) return yield* Effect.fail(e);
294
+ throw e;
295
+ }
296
+ const inspected = {
297
+ configPath,
298
+ projectDir,
299
+ changelog: changelogId,
300
+ baseBranch: config.baseBranch ?? "main",
301
+ access: config.access ?? "restricted",
302
+ ignore: config.ignore ?? [],
303
+ packages: scopes,
304
+ legacyVersionFilesUsed: legacyUsed
305
+ };
306
+ cache.set(projectDir, inspected);
307
+ return inspected;
308
+ });
309
+ const classify = (cwd, paths) => Effect.gen(function* () {
310
+ const inspected = yield* inspect(cwd);
311
+ return paths.map((p) => classifyOne(inspected, p));
312
+ });
313
+ return {
314
+ inspect,
315
+ classify
316
+ };
317
+ }
318
+ /**
319
+ * Classify a single path against an inspected config.
320
+ */
321
+ function classifyOne(inspected, path) {
322
+ const abs = resolve(inspected.projectDir, path);
323
+ let bestWorkspace = null;
324
+ for (const s of inspected.packages) if (isInside(s.workspaceDir, abs)) {
325
+ const depth = s.workspaceDir.length;
326
+ if (!bestWorkspace || depth > bestWorkspace.depth) bestWorkspace = {
327
+ pkg: s.name,
328
+ depth
329
+ };
330
+ }
331
+ if (bestWorkspace) return {
332
+ path,
333
+ package: bestWorkspace.pkg,
334
+ reason: "workspace"
335
+ };
336
+ for (const s of inspected.packages) if (s.additionalScopeFiles.includes(abs)) {
337
+ const glob = s.additionalScopes.find((g) => materializeGlob(g, inspected.projectDir).map((rel) => join(inspected.projectDir, rel)).includes(abs));
338
+ return {
339
+ path,
340
+ package: s.name,
341
+ reason: {
342
+ kind: "additionalScope",
343
+ glob: glob ?? s.additionalScopes[0] ?? ""
344
+ }
345
+ };
346
+ }
347
+ for (const s of inspected.packages) for (const vf of s.versionFiles) if (vf.matchedFiles.includes(abs)) return {
348
+ path,
349
+ package: s.name,
350
+ reason: {
351
+ kind: "versionFile",
352
+ glob: vf.glob
353
+ }
354
+ };
355
+ return {
356
+ path,
357
+ package: null,
358
+ reason: null
359
+ };
360
+ }
361
+ /**
362
+ * Live layer for {@link ConfigInspector}.
363
+ *
364
+ * Requires {@link ChangesetConfigReader} and {@link WorkspaceDiscovery}
365
+ * in the environment.
366
+ *
367
+ * @public
368
+ */
369
+ const ConfigInspectorLive = Layer.effect(ConfigInspector, Effect.gen(function* () {
370
+ return makeShape(yield* ChangesetConfigReader, yield* WorkspaceDiscovery);
371
+ }));
372
+ /**
373
+ * Test factory — build a {@link ConfigInspector} that returns a fixed
374
+ * {@link InspectedConfig} without touching the filesystem.
375
+ *
376
+ * Tests that need to exercise the inspect/classify logic against real files
377
+ * should compose `ConfigInspectorLive` with test layers for
378
+ * `ChangesetConfigReader` and `WorkspaceDiscovery` instead.
379
+ *
380
+ * @public
381
+ */
382
+ function makeConfigInspectorTest(fixed) {
383
+ return Layer.succeed(ConfigInspector, {
384
+ inspect: () => Effect.succeed(fixed),
385
+ classify: (_cwd, paths) => Effect.succeed(paths.map((p) => classifyOne(fixed, p)))
386
+ });
387
+ }
388
+
389
+ //#endregion
390
+ export { ConfigInspector, ConfigInspectorBase, ConfigInspectorLive, makeConfigInspectorTest };
@@ -0,0 +1,178 @@
1
+ import { GitHubApiError } from "../errors.js";
2
+ import { getGitHubInfo } from "../vendor/github-info.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+
5
+ //#region src/changesets/services/github.ts
6
+ /**
7
+ * GitHub service for fetching commit metadata.
8
+ *
9
+ * Defines the {@link GitHubService} Effect service tag, the
10
+ * {@link GitHubLive | production layer} backed by `\@changesets/get-github-info`,
11
+ * and the {@link makeGitHubTest} helper for constructing deterministic test
12
+ * layers.
13
+ *
14
+ * @remarks
15
+ * The GitHub service is consumed by the changelog formatters to resolve
16
+ * commit hashes into pull-request numbers, author usernames, and link URLs.
17
+ * In production, {@link GitHubLive} calls the GitHub REST API via the
18
+ * vendored `getGitHubInfo` wrapper. In tests, {@link makeGitHubTest}
19
+ * returns canned responses from a `Map` keyed by commit hash.
20
+ *
21
+ * @see {@link GitHubService} for the Effect service tag
22
+ * @see {@link GitHubServiceShape} for the service interface
23
+ * @see {@link GitHubLive} for the production layer
24
+ * @see {@link makeGitHubTest} for constructing test layers
25
+ */
26
+ const _tag = Context.Tag("GitHubService");
27
+ /**
28
+ * Base class for GitHubService.
29
+ *
30
+ * @privateRemarks
31
+ * This export is required for api-extractor documentation generation.
32
+ * Effect's Context.Tag creates an anonymous base class that must be
33
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
34
+ *
35
+ * @internal
36
+ */
37
+ const GitHubServiceBase = _tag();
38
+ /**
39
+ * Effect service tag for GitHub API operations.
40
+ *
41
+ * Provides dependency-injected access to GitHub commit metadata lookups.
42
+ * Use `yield* GitHubService` inside an `Effect.gen` block to obtain the
43
+ * service instance.
44
+ *
45
+ * @remarks
46
+ * This tag follows the standard Effect `Context.Tag` pattern. Two layers
47
+ * are provided out of the box:
48
+ *
49
+ * - {@link GitHubLive} — production layer backed by the GitHub REST API
50
+ * - {@link makeGitHubTest} — factory for deterministic test layers
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { Effect, Layer } from "effect";
55
+ * import { GitHubService, GitHubLive } from "\@savvy-web/changesets";
56
+ *
57
+ * const program = Effect.gen(function* () {
58
+ * const github = yield* GitHubService;
59
+ * const info = yield* github.getInfo({
60
+ * commit: "abc1234567890",
61
+ * repo: "savvy-web/changesets",
62
+ * });
63
+ * console.log(info.user, info.pull, info.links);
64
+ * });
65
+ *
66
+ * // Provide the live layer and run
67
+ * Effect.runPromise(program.pipe(Effect.provide(GitHubLive)));
68
+ * ```
69
+ *
70
+ * @example Creating a test layer with canned responses
71
+ * ```typescript
72
+ * import { Effect } from "effect";
73
+ * import type { GitHubCommitInfo } from "\@savvy-web/changesets";
74
+ * import { GitHubService, makeGitHubTest } from "\@savvy-web/changesets";
75
+ *
76
+ * const testResponses = new Map<string, GitHubCommitInfo>([
77
+ * ["abc1234", { user: "octocat", pull: 42, links: { pull: "#42", user: "\@octocat" } }],
78
+ * ]);
79
+ *
80
+ * const TestLayer = makeGitHubTest(testResponses);
81
+ *
82
+ * const program = Effect.gen(function* () {
83
+ * const github = yield* GitHubService;
84
+ * return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
85
+ * });
86
+ *
87
+ * Effect.runPromise(program.pipe(Effect.provide(TestLayer)));
88
+ * ```
89
+ *
90
+ * @see {@link GitHubServiceShape} for the service interface
91
+ * @see {@link GitHubLive} for the production layer
92
+ * @see {@link makeGitHubTest} for creating test layers
93
+ * @see {@link GitHubServiceBase} for the api-extractor base class
94
+ *
95
+ * @public
96
+ */
97
+ var GitHubService = class extends GitHubServiceBase {};
98
+ /**
99
+ * Production layer for {@link GitHubService}.
100
+ *
101
+ * Delegates to `\@changesets/get-github-info` to fetch commit metadata
102
+ * from the GitHub REST API. Requires a `GITHUB_TOKEN` environment variable
103
+ * to be set for authenticated requests.
104
+ *
105
+ * @remarks
106
+ * This layer is used by the `\@savvy-web/changesets/changelog` entry point
107
+ * to resolve commit hashes into PR numbers and author attribution. It is
108
+ * composed with {@link MarkdownLive} in the changelog formatter's
109
+ * `MainLayer`.
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * import { Effect } from "effect";
114
+ * import { GitHubService, GitHubLive } from "\@savvy-web/changesets";
115
+ *
116
+ * const program = Effect.gen(function* () {
117
+ * const github = yield* GitHubService;
118
+ * return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
119
+ * });
120
+ *
121
+ * Effect.runPromise(program.pipe(Effect.provide(GitHubLive)));
122
+ * ```
123
+ *
124
+ * @public
125
+ */
126
+ const GitHubLive = Layer.succeed(GitHubService, { getInfo: getGitHubInfo });
127
+ /**
128
+ * Create a test layer for {@link GitHubService} with pre-configured responses.
129
+ *
130
+ * Returns a `Layer` that resolves commit hashes from the provided `Map`.
131
+ * Lookups for commits not present in the map fail with a
132
+ * {@link GitHubApiError}.
133
+ *
134
+ * @remarks
135
+ * This helper is the recommended way to test code that depends on
136
+ * `GitHubService` without making real API calls. Provide the layer
137
+ * via `Effect.provide` in your test setup.
138
+ *
139
+ * @param responses - A `Map` of full commit hash to {@link GitHubCommitInfo} objects
140
+ * @returns A `Layer` providing the {@link GitHubService} with deterministic responses
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * import { Effect } from "effect";
145
+ * import type { GitHubCommitInfo } from "\@savvy-web/changesets";
146
+ * import { GitHubService, makeGitHubTest } from "\@savvy-web/changesets";
147
+ *
148
+ * const responses = new Map<string, GitHubCommitInfo>([
149
+ * ["abc1234", { user: "octocat", pull: 42, links: { pull: "#42", user: "\@octocat" } }],
150
+ * ]);
151
+ *
152
+ * const TestGitHub = makeGitHubTest(responses);
153
+ *
154
+ * const program = Effect.gen(function* () {
155
+ * const github = yield* GitHubService;
156
+ * return yield* github.getInfo({ commit: "abc1234", repo: "owner/repo" });
157
+ * });
158
+ *
159
+ * // In a Vitest test:
160
+ * const result = await Effect.runPromise(program.pipe(Effect.provide(TestGitHub)));
161
+ * // result.user === "octocat"
162
+ * ```
163
+ *
164
+ * @public
165
+ */
166
+ function makeGitHubTest(responses) {
167
+ return Layer.succeed(GitHubService, { getInfo: (params) => {
168
+ const info = responses.get(params.commit);
169
+ if (info) return Effect.succeed(info);
170
+ return Effect.fail(new GitHubApiError({
171
+ operation: "getInfo",
172
+ reason: `No mock response for commit ${params.commit}`
173
+ }));
174
+ } });
175
+ }
176
+
177
+ //#endregion
178
+ export { GitHubLive, GitHubService, GitHubServiceBase, makeGitHubTest };
@@ -0,0 +1,106 @@
1
+ import { parseMarkdown, stringifyMarkdown } from "../utils/remark-pipeline.js";
2
+ import { Context, Effect, Layer } from "effect";
3
+
4
+ //#region src/changesets/services/markdown.ts
5
+ /**
6
+ * Markdown service for parsing and stringifying mdast trees.
7
+ *
8
+ * Defines the {@link MarkdownService} Effect service tag and the
9
+ * {@link MarkdownLive | production layer} backed by the remark/unified
10
+ * pipeline. This service provides a pure, side-effect-free interface to
11
+ * markdown parsing and stringification used throughout the changelog
12
+ * formatter and remark transform pipeline.
13
+ *
14
+ * @remarks
15
+ * The service wraps the `remark-pipeline` utility functions (`parseMarkdown`
16
+ * and `stringifyMarkdown`) behind an Effect interface. The `parse` operation
17
+ * returns an mdast `Root` node; the `stringify` operation serializes an
18
+ * mdast `Root` back to a markdown string. Both operations are synchronous
19
+ * under the hood but are lifted into `Effect.sync` for composability.
20
+ *
21
+ * @see {@link MarkdownService} for the Effect service tag
22
+ * @see {@link MarkdownServiceShape} for the service interface
23
+ * @see {@link MarkdownLive} for the production layer
24
+ */
25
+ const _tag = Context.Tag("MarkdownService");
26
+ /**
27
+ * Base class for MarkdownService.
28
+ *
29
+ * @privateRemarks
30
+ * This export is required for api-extractor documentation generation.
31
+ * Effect's Context.Tag creates an anonymous base class that must be
32
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
33
+ *
34
+ * @internal
35
+ */
36
+ const MarkdownServiceBase = _tag();
37
+ /**
38
+ * Effect service tag for markdown parsing and stringification.
39
+ *
40
+ * Provides dependency-injected access to markdown parse/stringify operations.
41
+ * Use `yield* MarkdownService` inside an `Effect.gen` block to obtain the
42
+ * service instance.
43
+ *
44
+ * @remarks
45
+ * This tag follows the standard Effect `Context.Tag` pattern. The
46
+ * {@link MarkdownLive} layer provides the default implementation backed by
47
+ * the remark/unified pipeline. For testing, you can supply a custom layer
48
+ * via `Layer.succeed(MarkdownService, { parse: ..., stringify: ... })`.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { Effect } from "effect";
53
+ * import { MarkdownService, MarkdownLive } from "\@savvy-web/changesets";
54
+ *
55
+ * const program = Effect.gen(function* () {
56
+ * const md = yield* MarkdownService;
57
+ * const tree = yield* md.parse("# Hello\n\nWorld");
58
+ * const output = yield* md.stringify(tree);
59
+ * console.log(output); // "# Hello\n\nWorld\n"
60
+ * });
61
+ *
62
+ * Effect.runPromise(program.pipe(Effect.provide(MarkdownLive)));
63
+ * ```
64
+ *
65
+ * @see {@link MarkdownServiceShape} for the service interface
66
+ * @see {@link MarkdownLive} for the production layer
67
+ * @see {@link MarkdownServiceBase} for the api-extractor base class
68
+ *
69
+ * @public
70
+ */
71
+ var MarkdownService = class extends MarkdownServiceBase {};
72
+ /**
73
+ * Production layer for {@link MarkdownService}.
74
+ *
75
+ * Wraps the remark-pipeline utility functions (`parseMarkdown` and
76
+ * `stringifyMarkdown`) in `Effect.sync` for use in Effect programs.
77
+ * Both operations are synchronous and infallible.
78
+ *
79
+ * @remarks
80
+ * This layer is composed with {@link GitHubLive} in the
81
+ * `\@savvy-web/changesets/changelog` entry point to form the `MainLayer`
82
+ * that powers the Changesets API integration.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * import { Effect, Layer } from "effect";
87
+ * import { MarkdownService, MarkdownLive } from "\@savvy-web/changesets";
88
+ *
89
+ * const program = Effect.gen(function* () {
90
+ * const md = yield* MarkdownService;
91
+ * const tree = yield* md.parse("**bold** text");
92
+ * return yield* md.stringify(tree);
93
+ * });
94
+ *
95
+ * Effect.runPromise(program.pipe(Effect.provide(MarkdownLive)));
96
+ * ```
97
+ *
98
+ * @public
99
+ */
100
+ const MarkdownLive = Layer.succeed(MarkdownService, {
101
+ parse: (content) => Effect.sync(() => parseMarkdown(content)),
102
+ stringify: (tree) => Effect.sync(() => stringifyMarkdown(tree))
103
+ });
104
+
105
+ //#endregion
106
+ export { MarkdownLive, MarkdownService, MarkdownServiceBase };