create-sdd-project 0.16.10 → 0.17.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.
package/lib/scanner.js CHANGED
@@ -7,17 +7,56 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.n
7
7
 
8
8
  /**
9
9
  * Scan an existing project directory and return detected configuration.
10
+ *
11
+ * v0.17.1: monorepo-aware. If the root `package.json` does not yield a
12
+ * backend/frontend detection AND the project is a monorepo with
13
+ * `package.json#workspaces`, enumerate workspace `package.json` files in
14
+ * declaration order (pattern outer, lexical inner, deduped by normalized
15
+ * path) and run `detectBackend` / `detectFrontend` per workspace. The
16
+ * FIRST workspace returning `detected: true` wins and its result is merged
17
+ * into `result.backend` / `result.frontend` with a `workspaceSource` field
18
+ * recording the detected workspace's relative path (for diagnostics).
19
+ *
20
+ * Scanner additive invariant (v0.17.1): for single-package projects, or
21
+ * monorepos where root detection already succeeded, the workspace
22
+ * enumeration never fires and the result is byte-identical to v0.17.0.
10
23
  */
11
24
  function scan(projectDir) {
12
25
  const pkg = readPackageJson(projectDir);
13
26
 
14
- const result = {
27
+ const backend = detectBackend(projectDir, pkg);
28
+ const frontend = detectFrontend(projectDir, pkg);
29
+ const isMonorepo = detectMonorepo(projectDir, pkg);
30
+
31
+ // v0.17.1 monorepo fallback
32
+ if (isMonorepo && (!backend.detected || !frontend.detected)) {
33
+ const workspaces = enumerateWorkspaces(projectDir, pkg);
34
+ for (const wsRel of workspaces) {
35
+ const wsAbs = path.join(projectDir, ...wsRel.split('/'));
36
+ const wsPkg = readPackageJson(wsAbs);
37
+ if (!backend.detected) {
38
+ const wsBackend = detectBackend(wsAbs, wsPkg);
39
+ if (wsBackend.detected) {
40
+ Object.assign(backend, wsBackend, { workspaceSource: wsRel });
41
+ }
42
+ }
43
+ if (!frontend.detected) {
44
+ const wsFrontend = detectFrontend(wsAbs, wsPkg);
45
+ if (wsFrontend.detected) {
46
+ Object.assign(frontend, wsFrontend, { workspaceSource: wsRel });
47
+ }
48
+ }
49
+ if (backend.detected && frontend.detected) break;
50
+ }
51
+ }
52
+
53
+ return {
15
54
  projectName: pkg.name || path.basename(projectDir),
16
55
  description: pkg.description || '',
17
56
  language: detectLanguage(projectDir),
18
- backend: detectBackend(projectDir, pkg),
19
- frontend: detectFrontend(projectDir, pkg),
20
- isMonorepo: detectMonorepo(projectDir, pkg),
57
+ backend,
58
+ frontend,
59
+ isMonorepo,
21
60
  rootDirs: listRootDirs(projectDir),
22
61
  srcStructure: detectArchitecture(projectDir, pkg),
23
62
  tests: detectTests(projectDir, pkg),
@@ -25,8 +64,90 @@ function scan(projectDir) {
25
64
  gitBranch: detectGitBranch(projectDir),
26
65
  hasGit: fs.existsSync(path.join(projectDir, '.git')),
27
66
  };
67
+ }
28
68
 
29
- return result;
69
+ /**
70
+ * v0.17.1: enumerate workspace paths declared in `pkg.workspaces`.
71
+ *
72
+ * Supports:
73
+ * - Array form: `"workspaces": ["packages/*", "apps/*"]`
74
+ * - Object form: `"workspaces": { "packages": ["packages/*"] }`
75
+ * - Literal paths: `"packages/api"` (no glob)
76
+ * - Single-wildcard patterns: `"packages/*"` (expand immediate subdirs)
77
+ *
78
+ * Does NOT support: `**` recursive patterns, `!exclude` negation, or
79
+ * `pnpm-workspace.yaml` — all deferred to v0.17.2.
80
+ *
81
+ * Returns a deterministic, deduplicated array of POSIX-style relative
82
+ * workspace paths. Ordering: outer = declaration order of patterns; inner
83
+ * = lexical Unicode codepoint sort of expanded subdirs; dedupe = first
84
+ * occurrence wins after flattening (Codex + Gemini round-2 Q7).
85
+ */
86
+ function enumerateWorkspaces(dir, pkg) {
87
+ let patterns = [];
88
+ if (Array.isArray(pkg.workspaces)) {
89
+ patterns = pkg.workspaces;
90
+ } else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
91
+ patterns = pkg.workspaces.packages;
92
+ }
93
+ if (patterns.length === 0) return [];
94
+
95
+ const flat = [];
96
+ for (const pattern of patterns) {
97
+ flat.push(...expandWorkspacePattern(dir, pattern));
98
+ }
99
+
100
+ const seen = new Set();
101
+ const deduped = [];
102
+ for (const wsPath of flat) {
103
+ const normalized = wsPath.replace(/\\/g, '/').replace(/\/$/, '');
104
+ if (seen.has(normalized)) continue;
105
+ seen.add(normalized);
106
+ deduped.push(normalized);
107
+ }
108
+ return deduped;
109
+ }
110
+
111
+ function expandWorkspacePattern(dir, pattern) {
112
+ // npm/yarn workspace semantics: a workspace is a DIRECTORY CONTAINING
113
+ // package.json. Directories without package.json are not workspaces,
114
+ // even if they live under a matched glob (Codex round-3 finding 1).
115
+ // This prevents the scanner from wasting work on stray folders
116
+ // (docs/, shared assets, build outputs) and keeps first-match-wins
117
+ // deterministic against only declared workspace packages.
118
+ const hasPkgJson = (absDir) => {
119
+ try {
120
+ return fs.statSync(path.join(absDir, 'package.json')).isFile();
121
+ } catch {
122
+ return false;
123
+ }
124
+ };
125
+
126
+ if (!pattern.includes('*')) {
127
+ const absPath = path.join(dir, pattern);
128
+ try {
129
+ if (fs.statSync(absPath).isDirectory() && hasPkgJson(absPath)) {
130
+ return [pattern.replace(/\\/g, '/')];
131
+ }
132
+ } catch { /* not found or not a dir */ }
133
+ return [];
134
+ }
135
+ // Only support trailing single-wildcard: `foo/*` or `foo/bar/*`
136
+ const match = pattern.match(/^(.+)\/\*$/);
137
+ if (!match) return [];
138
+ const baseDir = match[1];
139
+ const baseDirAbs = path.join(dir, baseDir);
140
+ let entries;
141
+ try {
142
+ entries = fs.readdirSync(baseDirAbs, { withFileTypes: true });
143
+ } catch {
144
+ return [];
145
+ }
146
+ return entries
147
+ .filter((e) => e.isDirectory() && hasPkgJson(path.join(baseDirAbs, e.name)))
148
+ .map((e) => e.name)
149
+ .sort()
150
+ .map((name) => `${baseDir}/${name}`);
30
151
  }
31
152
 
32
153
  // --- Helpers ---
@@ -541,4 +662,4 @@ function detectGitBranch(dir) {
541
662
  return 'main';
542
663
  }
543
664
 
544
- module.exports = { scan };
665
+ module.exports = { scan, enumerateWorkspaces, expandWorkspacePattern };
@@ -0,0 +1,335 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SDD DevFlow stack-specific adaptations — shared module (v0.17.0+).
5
+ *
6
+ * Extracted from lib/init-generator.js `adaptCopiedFiles` in v0.17.0 so
7
+ * the upgrade path can re-apply the same transformations after a
8
+ * hash-based smart-diff replacement. Previously init-generator.js ran
9
+ * these adaptations on install but upgrade-generator.js did not, so an
10
+ * init'd project upgrading would lose its stack customizations — the
11
+ * cross-path drift discovered during v0.16.10 implementation.
12
+ *
13
+ * Public API:
14
+ *
15
+ * applyStackAdaptations(dest, scan, config, allowlist = null)
16
+ * → walks the filesystem, applies adaptation rules to each file in
17
+ * the candidate set, respects the allowlist (upgrade path uses
18
+ * this to avoid touching preserved user-edited files). Returns the
19
+ * list of POSIX relative paths that were touched.
20
+ *
21
+ * applyStackAdaptationsToContent(content, posixRelativePath, scan, config)
22
+ * → pure, in-memory variant. Returns the adapted content for a
23
+ * single file. Used by upgrade-generator.js's FALLBACK path
24
+ * (when .sdd-meta.json is missing) to construct the "what init
25
+ * would have written" comparison target. This is critical for
26
+ * pre-v0.17.0 --init projects on their first v0.17.0 upgrade
27
+ * (Gemini M1 fix from plan v1.0 review).
28
+ *
29
+ * Idempotency invariant: every rule's source pattern MUST NOT appear in
30
+ * its own replacement value. The current rules satisfy this because they
31
+ * replace literal template strings like "Prisma ORM, and PostgreSQL"
32
+ * with "Mongoose, and MongoDB" — the source no longer appears after one
33
+ * pass. Verified by smoke scenario 56 (run every rule twice, assert
34
+ * second application is a no-op).
35
+ *
36
+ * Ordering: some rules run in phases. Phase 1 ("Zod data schemas" →
37
+ * "validation schemas") MUST run before phase 2 ("validation schemas in
38
+ * `shared/src/schemas/`" → "validation schemas") because phase 2's
39
+ * source depends on phase 1's replacement having happened. The rule
40
+ * arrays preserve this ordering; callers must apply them in sequence
41
+ * per file.
42
+ */
43
+
44
+ const fs = require('node:fs');
45
+ const path = require('node:path');
46
+
47
+ const { toPosix } = require('./meta');
48
+
49
+ /**
50
+ * Compute the ordered list of [from, to] replacement rules for a given
51
+ * (file, scan, config). Rules are pure data — no filesystem access.
52
+ *
53
+ * Returns null if this file has no adaptations for the given project
54
+ * state (e.g., a Zod project's backend-developer.md needs no Zod
55
+ * substitutions).
56
+ *
57
+ * The rules here mirror the imperative body of the original
58
+ * lib/init-generator.js adaptCopiedFiles function. Extracting them into
59
+ * a data-driven table allows both file-based and in-memory application.
60
+ */
61
+ function computeRulesFor(posixRelativePath, scan, config) {
62
+ const backend = scan.backend || {};
63
+ const orm = backend.orm || 'your ORM';
64
+ const db = backend.db || 'your database';
65
+ const validation = backend.validation;
66
+ const structure = scan.srcStructure || {};
67
+ const arch = structure.pattern || 'ddd';
68
+
69
+ // Phase 1: Zod → generic validation (applies only when validation !== 'Zod').
70
+ const zodReplacements = [
71
+ ['Zod data schemas', 'validation schemas'],
72
+ ['Zod schemas', 'validation schemas'],
73
+ ];
74
+
75
+ // Phase 2: shared/src/schemas/ path cleanup. Applied AFTER phase 1, so
76
+ // these match the post-replacement text.
77
+ const schemaPathReplacements = [
78
+ ['validation schemas in `shared/src/schemas/` if applicable', 'validation schemas if applicable'],
79
+ ['validation schemas in `shared/src/schemas/` (if shared workspace exists)', 'validation schemas (if shared workspace exists)'],
80
+ ['validation schemas in `shared/src/schemas/`', 'validation schemas'],
81
+ ['validation schemas (`shared/src/schemas/`)', 'validation schemas'],
82
+ ['`shared/src/schemas/` (if exists) for current validation schemas', 'project validation schemas'],
83
+ // Gemini spec-creator: no "Zod" prefix, standalone path reference
84
+ ['and `shared/src/schemas/` (if exists)', ''],
85
+ ['schemas vs `shared/src/schemas/`', 'validation schemas up to date'],
86
+ ];
87
+
88
+ // ORM/DB replacements for backend agents. Only apply when the detected
89
+ // ORM differs from Prisma (the template default) OR no ORM was
90
+ // detected at all (replace with generic text).
91
+ let ormReplacements = [];
92
+ if (backend.orm && backend.orm !== 'Prisma') {
93
+ ormReplacements = [
94
+ ['Prisma ORM, and PostgreSQL', `${orm}${db !== 'your database' ? `, and ${db}` : ''}`],
95
+ ['Repository implementations (Prisma)', `Repository implementations (${orm})`],
96
+ ];
97
+ } else if (!backend.orm) {
98
+ const dbLabel = db !== 'your database' ? `, and ${db}` : '';
99
+ ormReplacements = [
100
+ ['Prisma ORM, and PostgreSQL', dbLabel ? dbLabel.slice(6) : 'your database'],
101
+ ['Repository implementations (Prisma)', 'Repository implementations'],
102
+ ];
103
+ }
104
+
105
+ // Architecture (DDD → layered) replacements, applied to backend agents
106
+ // when the detected structure is NOT DDD.
107
+ const archReplacementsBackendPlanner = (arch !== 'ddd') ? [
108
+ ['specializing in Domain-Driven Design (DDD) layered architecture with deep knowledge of',
109
+ 'specializing in layered architecture with deep knowledge of'],
110
+ ['(DDD architecture)', '(layered architecture)'],
111
+ [/\d+\. Read `shared\/src\/schemas\/` \(if exists\) for current .* (?:data )?schemas\n/, ''],
112
+ [/\d+\. Explore existing domain entities, services, validators, repositories\n/,
113
+ '5. Explore the codebase for existing patterns, layer structure, and reusable code\n'],
114
+ [/\d+\. Explore `backend\/src\/infrastructure\/` for existing repositories\n/, ''],
115
+ ['following DDD layer order: Domain > Application > Infrastructure > Presentation > Tests',
116
+ 'following the layer order defined in backend-standards.mdc'],
117
+ ['Implementation Order (Domain > Application > Infrastructure > Presentation > Tests)',
118
+ 'Implementation Order (see backend-standards.mdc for layer order)'],
119
+ ['Follow DDD layer separation: Domain > Application > Infrastructure > Presentation',
120
+ 'Follow the layer separation defined in backend-standards.mdc'],
121
+ ] : [];
122
+
123
+ const archReplacementsBackendDeveloper = (arch !== 'ddd') ? [
124
+ ['follows DDD layered architecture', 'follows layered architecture'],
125
+ ['specializing in Domain-Driven Design (DDD) with', 'specializing in layered architecture with'],
126
+ ['(DDD architecture)', '(layered architecture)'],
127
+ [/\d+\. Read `shared\/src\/schemas\/` \(if exists\) for current .* (?:data )?schemas\n/, ''],
128
+ ['Follow the DDD layer order from the plan:',
129
+ 'Follow the layer order from the plan (see backend-standards.mdc for project layers):'],
130
+ [/\d+\. \*\*Domain Layer\*\*: Entities, value objects, repository interfaces, domain errors\n/,
131
+ '1. **Data Layer**: Models, database operations, data access\n'],
132
+ [/\d+\. \*\*Application Layer\*\*: Services, validators, DTOs\n/,
133
+ '2. **Business Logic Layer**: Controllers, services, external integrations\n'],
134
+ [/\d+\. \*\*Infrastructure Layer\*\*: Repository implementations \([^)]*\), external integrations\n/,
135
+ '3. **Presentation Layer**: Routes, handlers, middleware\n'],
136
+ [/\d+\. \*\*Presentation Layer\*\*: Controllers, routes, middleware\n/,
137
+ '4. **Integration Layer**: Wiring, configuration, server registration\n'],
138
+ ['Follow DDD layer order: Domain > Application > Infrastructure > Presentation.',
139
+ 'Follow the layer order defined in backend-standards.mdc.'],
140
+ ['**ALWAYS** follow DDD layer separation',
141
+ '**ALWAYS** follow the layer separation defined in backend-standards.mdc'],
142
+ ['**ALWAYS** handle errors with custom domain error classes',
143
+ '**ALWAYS** handle errors following the patterns in backend-standards.mdc'],
144
+ ['ALWAYS handle errors with domain error classes',
145
+ 'ALWAYS handle errors following the patterns in backend-standards.mdc'],
146
+ [/- (?:\*\*MANDATORY\*\*: )?If modifying a DB schema → update .* schemas in `shared\/src\/schemas\/` BEFORE continuing\n/, ''],
147
+ ] : [];
148
+
149
+ // Dispatch table keyed by the file's POSIX path suffix.
150
+ const isBackendAgent =
151
+ posixRelativePath.endsWith('/agents/backend-developer.md') ||
152
+ posixRelativePath.endsWith('/agents/backend-planner.md');
153
+ const isMultiPurposeAgent =
154
+ posixRelativePath.endsWith('/agents/spec-creator.md') ||
155
+ posixRelativePath.endsWith('/agents/production-code-validator.md') ||
156
+ posixRelativePath.endsWith('/agents/database-architect.md');
157
+ const isWorkflowSkill =
158
+ posixRelativePath.endsWith('/skills/development-workflow/SKILL.md') ||
159
+ posixRelativePath.endsWith('/skills/development-workflow/references/ticket-template.md');
160
+
161
+ // Accumulate rules for this file in the correct order.
162
+ const rules = [];
163
+
164
+ if (isBackendAgent) {
165
+ if (validation !== 'Zod') {
166
+ rules.push(...zodReplacements);
167
+ rules.push(...ormReplacements);
168
+ rules.push(...schemaPathReplacements);
169
+ } else if (ormReplacements.length > 0) {
170
+ rules.push(...ormReplacements);
171
+ }
172
+ // Architecture adaptations run after ORM/Zod.
173
+ if (posixRelativePath.endsWith('/agents/backend-planner.md')) {
174
+ rules.push(...archReplacementsBackendPlanner);
175
+ } else if (posixRelativePath.endsWith('/agents/backend-developer.md')) {
176
+ rules.push(...archReplacementsBackendDeveloper);
177
+ }
178
+ } else if (isMultiPurposeAgent) {
179
+ if (validation !== 'Zod') {
180
+ rules.push(...zodReplacements);
181
+ rules.push(...schemaPathReplacements);
182
+ }
183
+ } else if (isWorkflowSkill) {
184
+ if (validation !== 'Zod') {
185
+ rules.push(...zodReplacements);
186
+ rules.push(...schemaPathReplacements);
187
+ }
188
+ }
189
+
190
+ return rules.length > 0 ? rules : null;
191
+ }
192
+
193
+ /**
194
+ * Apply an ordered list of [from, to] rules to a content string.
195
+ * Strings are replaced with `.replaceAll` (all occurrences). Regexes are
196
+ * replaced with `.replace` (respects the regex's own flags — `g` for
197
+ * global, absent for first-occurrence; the current rule set uses regexes
198
+ * without `g` because they target unique structural lines).
199
+ */
200
+ function applyRulesToContent(content, rules) {
201
+ let result = content;
202
+ for (const [from, to] of rules) {
203
+ if (from instanceof RegExp) {
204
+ result = result.replace(from, to);
205
+ } else {
206
+ result = result.replaceAll(from, to);
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+
212
+ /**
213
+ * Pure, in-memory stack adaptation. Returns the adapted content.
214
+ * Zero filesystem I/O. Safe to call repeatedly on the same input
215
+ * (idempotent by rule design).
216
+ *
217
+ * @param {string} content - Raw file content
218
+ * @param {string} posixRelativePath - e.g. ".claude/agents/backend-developer.md"
219
+ * @param {object} scan
220
+ * @param {object} config
221
+ * @returns {string}
222
+ */
223
+ function applyStackAdaptationsToContent(content, posixRelativePath, scan, config) {
224
+ const rules = computeRulesFor(posixRelativePath, scan, config);
225
+ if (!rules) return content;
226
+ return applyRulesToContent(content, rules);
227
+ }
228
+
229
+ /**
230
+ * Candidate file list for stack adaptations. Mirrors the files touched
231
+ * by the original adaptCopiedFiles. Only files that exist on disk are
232
+ * returned.
233
+ */
234
+ function candidateFilesFor(dest, aiTools, projectType) {
235
+ const toolDirs = [];
236
+ if (aiTools !== 'gemini') toolDirs.push('.claude');
237
+ if (aiTools !== 'claude') toolDirs.push('.gemini');
238
+
239
+ const results = [];
240
+
241
+ for (const dir of toolDirs) {
242
+ // Backend agents
243
+ results.push(`${dir}/agents/backend-developer.md`);
244
+ results.push(`${dir}/agents/backend-planner.md`);
245
+ // Multi-purpose agents
246
+ results.push(`${dir}/agents/spec-creator.md`);
247
+ results.push(`${dir}/agents/production-code-validator.md`);
248
+ results.push(`${dir}/agents/database-architect.md`);
249
+ // Workflow skill files
250
+ results.push(`${dir}/skills/development-workflow/SKILL.md`);
251
+ results.push(`${dir}/skills/development-workflow/references/ticket-template.md`);
252
+ }
253
+
254
+ // Filter by on-disk presence AND by project-type (single-stack
255
+ // projects may have pruned backend-* files).
256
+ return results.filter((posixPath) => {
257
+ const absPath = path.join(dest, ...posixPath.split('/'));
258
+ return fs.existsSync(absPath);
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Apply stack adaptations to files on disk.
264
+ *
265
+ * @param {string} dest - Project root
266
+ * @param {object} scan - scan() result
267
+ * @param {object} config - { projectType, aiTools, ... }
268
+ * @param {Set<string>|null} allowlist - POSIX paths permitted to be
269
+ * touched. If null, all candidate files are touched (install path).
270
+ * If a Set, only files whose POSIX path is IN the Set are touched
271
+ * (upgrade path — prevents running adaptations on preserved user
272
+ * files).
273
+ * @returns {string[]} POSIX relative paths that were touched (whether
274
+ * their content actually changed or not — callers should re-hash them)
275
+ */
276
+ function applyStackAdaptations(dest, scan, config, allowlist = null) {
277
+ const touched = [];
278
+ const candidates = candidateFilesFor(dest, config.aiTools, config.projectType);
279
+
280
+ for (const posixPath of candidates) {
281
+ if (allowlist !== null && !allowlist.has(posixPath)) continue;
282
+ const absPath = path.join(dest, ...posixPath.split('/'));
283
+ let content;
284
+ try {
285
+ content = fs.readFileSync(absPath, 'utf8');
286
+ } catch {
287
+ continue;
288
+ }
289
+ const adapted = applyStackAdaptationsToContent(content, posixPath, scan, config);
290
+ if (adapted !== content) {
291
+ try {
292
+ fs.writeFileSync(absPath, adapted, 'utf8');
293
+ } catch (e) {
294
+ console.warn(` ⚠ Failed to write stack-adapted ${posixPath}: ${e.code || e.message}`);
295
+ continue;
296
+ }
297
+ }
298
+ touched.push(posixPath);
299
+ }
300
+
301
+ // Non-agent adaptations: documentation-standards.mdc is project-type-
302
+ // driven, not stack-driven. Keeps its own imperative branch here.
303
+ const docStdRelative = 'ai-specs/specs/documentation-standards.mdc';
304
+ const docStdPath = path.join(dest, docStdRelative);
305
+ if (
306
+ fs.existsSync(docStdPath) &&
307
+ (allowlist === null || allowlist.has(docStdRelative))
308
+ ) {
309
+ try {
310
+ let content = fs.readFileSync(docStdPath, 'utf8');
311
+ if (config.projectType === 'backend') {
312
+ content = content.replace(/\| `ai-specs\/specs\/frontend-standards\.mdc` \|[^\n]*\n/, '');
313
+ content = content.replace(/\| `docs\/specs\/ui-components\.md` \|[^\n]*\n/, '');
314
+ content = content.replace(/ - UI component changes → `docs\/specs\/ui-components\.md`\n/, '');
315
+ } else if (config.projectType === 'frontend') {
316
+ content = content.replace(/\| `ai-specs\/specs\/backend-standards\.mdc` \|[^\n]*\n/, '');
317
+ content = content.replace(/\| `docs\/specs\/api-spec\.yaml` \|[^\n]*\n/, '');
318
+ }
319
+ fs.writeFileSync(docStdPath, content, 'utf8');
320
+ touched.push(docStdRelative);
321
+ } catch (e) {
322
+ console.warn(` ⚠ Failed to adapt documentation-standards.mdc: ${e.code || e.message}`);
323
+ }
324
+ }
325
+
326
+ return touched;
327
+ }
328
+
329
+ module.exports = {
330
+ applyStackAdaptations,
331
+ applyStackAdaptationsToContent,
332
+ computeRulesFor,
333
+ applyRulesToContent,
334
+ candidateFilesFor,
335
+ };