agent-rules-init 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,55 @@
1
1
  import path from "node:path";
2
2
  import { parse } from "yaml";
3
3
  import { UI } from "./i18n.js";
4
+ import { selectCanonicalCommands } from "./canonical-commands.js";
4
5
  const NPM_DIRECT_LIFECYCLE = new Set(["test", "start", "stop", "restart"]);
5
- export function extractNpmCommands(signals) {
6
- const scripts = signals.packageJson?.scripts ?? {};
6
+ export function extractJsPackageCommands(signals) {
7
7
  const entries = [];
8
- for (const [name, body] of Object.entries(scripts)) {
9
- if (typeof body !== "string" || body.trim() === "")
10
- continue;
11
- entries.push({
12
- source: "npm",
13
- invocation: NPM_DIRECT_LIFECYCLE.has(name) ? `npm ${name}` : `npm run ${name}`,
14
- detail: body.trim(),
15
- });
8
+ const locatedManifests = signals.packageJsons ?? [];
9
+ const hasLocatedManifests = locatedManifests.length > 0;
10
+ const manifests = hasLocatedManifests
11
+ ? locatedManifests
12
+ : signals.packageJson
13
+ ? [{ ...signals.packageJson, path: "package.json" }]
14
+ : [];
15
+ for (const manifest of manifests) {
16
+ const packageDir = path.posix.dirname(manifest.path);
17
+ const manager = manifest.packageManager ?? signals.packageJson?.packageManager ?? "npm";
18
+ for (const [name, body] of Object.entries(manifest.scripts)) {
19
+ if (typeof body !== "string" || body.trim() === "")
20
+ continue;
21
+ entries.push({
22
+ source: manager,
23
+ invocation: jsScriptInvocation(manager, packageDir, name),
24
+ detail: body.trim(),
25
+ ...(hasLocatedManifests ? { manifestPath: manifest.path } : {}),
26
+ });
27
+ }
16
28
  }
17
29
  return entries;
18
30
  }
31
+ /** @deprecated Conservado como alias de API; también devuelve comandos pnpm/Yarn/Bun. */
32
+ export const extractNpmCommands = extractJsPackageCommands;
33
+ function jsScriptInvocation(manager, packageDir, script) {
34
+ const name = quoteShellArg(script);
35
+ if (manager === "npm") {
36
+ const prefix = packageDir === "." ? "npm" : `npm --prefix ${quoteShellArg(packageDir)}`;
37
+ return NPM_DIRECT_LIFECYCLE.has(script) ? `${prefix} ${name}` : `${prefix} run ${name}`;
38
+ }
39
+ if (manager === "pnpm") {
40
+ const prefix = packageDir === "." ? "pnpm" : `pnpm --dir ${quoteShellArg(packageDir)}`;
41
+ return NPM_DIRECT_LIFECYCLE.has(script) ? `${prefix} ${name}` : `${prefix} run ${name}`;
42
+ }
43
+ if (manager === "yarn") {
44
+ const prefix = packageDir === "." ? "yarn" : `yarn --cwd ${quoteShellArg(packageDir)}`;
45
+ return `${prefix} run ${name}`;
46
+ }
47
+ const prefix = packageDir === "." ? "bun" : `bun --cwd ${quoteShellArg(packageDir)}`;
48
+ return `${prefix} run ${name}`;
49
+ }
50
+ function quoteShellArg(value) {
51
+ return /\s/.test(value) ? JSON.stringify(value) : value;
52
+ }
19
53
  export function extractMakeTargets(signals) {
20
54
  const makefile = signals.makefile;
21
55
  if (!makefile)
@@ -178,9 +212,190 @@ export function extractStructure(signals, lang) {
178
212
  return note ? { dir: `${dir}/`, note } : { dir: `${dir}/` };
179
213
  });
180
214
  }
215
+ const TEST_DIR_NAMES = new Set(["test", "tests", "spec", "specs", "__tests__"]);
216
+ const MEANINGFUL_TEST_CHILDREN = new Set(["acceptance", "integration", "unit", "e2e"]);
217
+ const AUXILIARY_FACT_DIRS = new Set([
218
+ "docs", "doc", "tools", "tool", "scripts", "script", "examples", "example",
219
+ "benchmark", "benchmarks", "fixture", "fixtures",
220
+ ]);
221
+ function isAuxiliaryFactPath(file) {
222
+ return file.split(/[\\/]/).some((segment) => AUXILIARY_FACT_DIRS.has(segment.toLowerCase()));
223
+ }
224
+ export function detectTestDirs(files) {
225
+ const dirs = new Set();
226
+ for (const file of files) {
227
+ const segments = file.split(/[\\/]/).slice(0, -1);
228
+ for (let i = 0; i < segments.length; i++) {
229
+ if (TEST_DIR_NAMES.has(segments[i].toLowerCase())) {
230
+ dirs.add(segments.slice(0, i + 1).join("/") + "/");
231
+ // Solo conserva sublayouts inequívocos. Directorios como tests/static o
232
+ // tests/templates suelen ser fixtures, no ubicaciones independientes de tests.
233
+ const child = segments[i + 1]?.toLowerCase();
234
+ const languageLayout = segments[0]?.toLowerCase() === "src" && segments[1]?.toLowerCase() === "test";
235
+ if (child && (languageLayout || MEANINGFUL_TEST_CHILDREN.has(child))) {
236
+ dirs.add(segments.slice(0, i + 2).join("/") + "/");
237
+ }
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ return [...dirs].sort();
243
+ }
244
+ export function detectEntrypoints(signals) {
245
+ const out = [];
246
+ if (signals.packageJson?.main) {
247
+ out.push({ label: "main", target: signals.packageJson.main, source: "package.json" });
248
+ }
249
+ const scriptsSection = signals.pyprojectToml?.match(/\[project\.scripts\]([\s\S]*?)(?:\n\[|$)/)?.[1];
250
+ if (scriptsSection) {
251
+ for (const match of scriptsSection.matchAll(/^\s*([\w.-]+)\s*=\s*["']([^"']+)["']/gm)) {
252
+ out.push({ label: match[1], target: match[2], source: "pyproject.toml" });
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+ function evidencePath(file) {
258
+ return file.split(path.sep).join("/");
259
+ }
260
+ export function extractArchitectureFacts(signals, lang, testDirs = detectTestDirs(signals.files.filter((file) => !isAuxiliaryFactPath(file))), entrypoints = detectEntrypoints(signals)) {
261
+ const facts = [];
262
+ const normalized = signals.files.filter((file) => !isAuxiliaryFactPath(file)).map(evidencePath);
263
+ const add = (fact) => facts.push({ ...fact, scope: ".", confidence: "high" });
264
+ if (entrypoints.length > 0) {
265
+ const rendered = entrypoints.map((entry) => `${entry.label} -> ${entry.target}`).join(", ");
266
+ add({
267
+ kind: "entrypoint",
268
+ statement: lang === "es" ? `Puntos de entrada declarados: ${rendered}.` : `Declared entry points: ${rendered}.`,
269
+ evidence: [...new Set(entrypoints.map((entry) => entry.source))],
270
+ });
271
+ }
272
+ if (testDirs.length > 0) {
273
+ const testEvidence = [...new Set(testDirs.map((dir) => {
274
+ const directory = dir.replace(/\/$/, "");
275
+ return signals.hasDir(directory) ? dir : normalized.find((file) => file.startsWith(dir));
276
+ }).filter((f) => Boolean(f)))];
277
+ add({
278
+ kind: "tests",
279
+ statement: lang === "es"
280
+ ? `Los tests se colocan en ${testDirs.join(", ")}.`
281
+ : `Tests are placed under ${testDirs.join(", ")}.`,
282
+ evidence: testEvidence,
283
+ });
284
+ }
285
+ const sourceRoots = ["src", "lib"].filter((dir) => signals.hasDir(dir) || normalized.some((file) => file.startsWith(`${dir}/`)));
286
+ const sourceLocations = sourceRoots.flatMap((dir) => dir === "src" && signals.hasDir("src/main") ? ["src/main/"] : [`${dir}/`]);
287
+ if (sourceLocations.length > 0) {
288
+ add({
289
+ kind: "source-layout",
290
+ statement: lang === "es"
291
+ ? `El código principal vive en ${sourceLocations.join(", ")}.`
292
+ : `Primary source code lives under ${sourceLocations.join(", ")}.`,
293
+ evidence: sourceLocations.map((location) => {
294
+ const directory = location.replace(/\/$/, "");
295
+ return signals.hasDir(directory) ? location : normalized.find((file) => file.startsWith(location));
296
+ }).filter(Boolean),
297
+ });
298
+ }
299
+ const layerPatterns = [
300
+ ["controller", /(?:^|\/)(?:controllers?)(?:\/|$)/i],
301
+ ["service", /(?:^|\/)(?:services?)(?:\/|$)/i],
302
+ ["repository", /(?:^|\/)(?:repositories|repository)(?:\/|$)/i],
303
+ ];
304
+ const layers = layerPatterns.map(([name, pattern]) => ({ name, file: normalized.find((file) => pattern.test(file)) }));
305
+ if (layers.every((layer) => layer.file)) {
306
+ add({
307
+ kind: "layers",
308
+ statement: lang === "es"
309
+ ? "El repositorio separa controller, service y repository en capas distintas."
310
+ : "The repository separates controller, service and repository into distinct layers.",
311
+ evidence: layers.map((layer) => layer.file),
312
+ });
313
+ }
314
+ const workspaceManifests = (signals.packageJsons ?? []).filter((manifest) => manifest.path !== "package.json");
315
+ if (workspaceManifests.length > 0) {
316
+ add({
317
+ kind: "workspace",
318
+ statement: lang === "es"
319
+ ? `El workspace contiene ${workspaceManifests.length} paquete(s) JavaScript/TypeScript anidado(s).`
320
+ : `The workspace contains ${workspaceManifests.length} nested JavaScript/TypeScript package(s).`,
321
+ evidence: workspaceManifests.map((manifest) => manifest.path),
322
+ });
323
+ }
324
+ return facts;
325
+ }
326
+ function uniqueConfigValue(content, key) {
327
+ const values = [...content.matchAll(new RegExp(`^\\s*${key}\\s*=\\s*([^#;\\r\\n]+)`, "gmi"))]
328
+ .map((match) => match[1].trim().toLowerCase());
329
+ const unique = [...new Set(values)];
330
+ return unique.length === 1 ? unique[0] : undefined;
331
+ }
332
+ export function extractConventionFacts(signals, lang) {
333
+ const facts = [];
334
+ const rootFiles = (signals.guidanceFiles ?? []).filter((file) => !evidencePath(file.path).includes("/"));
335
+ const get = (name) => rootFiles.find((file) => file.path.toLowerCase() === name.toLowerCase())?.content;
336
+ const add = (fact) => facts.push({ ...fact, scope: ".", confidence: "high" });
337
+ const editorConfig = get(".editorconfig");
338
+ if (editorConfig) {
339
+ const style = uniqueConfigValue(editorConfig, "indent_style");
340
+ const rawSize = uniqueConfigValue(editorConfig, "indent_size");
341
+ const size = rawSize && /^\d+$/.test(rawSize) ? rawSize : undefined;
342
+ if (style)
343
+ add({
344
+ kind: "formatting",
345
+ statement: lang === "es"
346
+ ? `La indentación usa ${style === "space" ? "espacios" : style === "tab" ? "tabuladores" : style}${size ? ` con tamaño ${size}` : ""}.`
347
+ : `Indentation uses ${style === "space" ? "spaces" : style === "tab" ? "tabs" : style}${size ? ` with size ${size}` : ""}.`,
348
+ evidence: [".editorconfig"],
349
+ });
350
+ const finalNewline = uniqueConfigValue(editorConfig, "insert_final_newline");
351
+ if (finalNewline === "true")
352
+ add({
353
+ kind: "formatting",
354
+ statement: lang === "es" ? "Los archivos deben terminar con una línea nueva." : "Files must end with a final newline.",
355
+ evidence: [".editorconfig"],
356
+ });
357
+ }
358
+ const tsconfig = get("tsconfig.json");
359
+ if (tsconfig && /["']strict["']\s*:\s*true\b/i.test(tsconfig))
360
+ add({
361
+ kind: "typescript",
362
+ statement: lang === "es" ? "TypeScript tiene activado el modo strict." : "TypeScript strict mode is enabled.",
363
+ evidence: ["tsconfig.json"],
364
+ });
365
+ const pyproject = get("pyproject.toml");
366
+ if (pyproject) {
367
+ for (const tool of ["ruff", "black"]) {
368
+ const section = pyproject.match(new RegExp(`(?:^|\\n)\\[tool\\.${tool}\\]([\\s\\S]*?)(?:\\n\\[|$)`, "i"))?.[1];
369
+ const lineLength = section?.match(/(?:^|\n)\s*line-length\s*=\s*(\d+)/i)?.[1];
370
+ if (lineLength)
371
+ add({
372
+ kind: "python-style",
373
+ statement: lang === "es"
374
+ ? `${tool} configura una longitud máxima de línea de ${lineLength}.`
375
+ : `${tool} configures a maximum line length of ${lineLength}.`,
376
+ evidence: ["pyproject.toml"],
377
+ });
378
+ }
379
+ }
380
+ const contributing = get("CONTRIBUTING.md");
381
+ if (contributing) {
382
+ const directives = contributing.split(/\r?\n/)
383
+ .map((line) => line.match(/^\s*[-*]\s+(.{1,180})$/)?.[1]?.trim())
384
+ .filter((line) => Boolean(line))
385
+ .filter((line) => /\b(?:must|should|required|do not|don't|run|use|follow|debe|debes|ejecuta|usa|sigue|no añadas)\b/i.test(line))
386
+ .slice(0, 5);
387
+ for (const directive of directives)
388
+ add({
389
+ kind: "contributing",
390
+ statement: directive,
391
+ evidence: ["CONTRIBUTING.md"],
392
+ });
393
+ }
394
+ return facts;
395
+ }
181
396
  export function buildRepoFacts(signals, lang) {
182
397
  const allCommands = [
183
- ...extractNpmCommands(signals),
398
+ ...extractJsPackageCommands(signals),
184
399
  ...extractComposerCommands(signals),
185
400
  ...extractMakeTargets(signals),
186
401
  ...extractMixAliases(signals),
@@ -188,11 +403,18 @@ export function buildRepoFacts(signals, lang) {
188
403
  ];
189
404
  const { kept, omitted } = filterCommands(allCommands);
190
405
  const { commands: ciCommands, omittedCount: omittedCiCount } = extractCiCommands(signals);
406
+ const testDirs = detectTestDirs(signals.files.filter((file) => !isAuxiliaryFactPath(file)));
407
+ const entrypoints = detectEntrypoints(signals);
191
408
  return {
192
409
  commands: kept,
193
410
  omittedCommands: omitted,
194
411
  structure: extractStructure(signals, lang),
195
412
  ciCommands,
196
413
  omittedCiCount,
414
+ canonical: selectCanonicalCommands(signals, kept, ciCommands),
415
+ testDirs,
416
+ entrypoints,
417
+ architectureFacts: extractArchitectureFacts(signals, lang, testDirs, entrypoints),
418
+ conventionFacts: extractConventionFacts(signals, lang),
197
419
  };
198
420
  }
@@ -1,6 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".venv", "__pycache__"]);
3
+ const IGNORED_DIRS = new Set([
4
+ "node_modules", ".git", ".hg", ".svn",
5
+ "dist", "build", "out", "target", "coverage", "vendor",
6
+ ".venv", "venv", ".tox", "__pycache__",
7
+ ".next", ".nuxt", ".svelte-kit", ".turbo", ".cache",
8
+ ".gradle", ".dart_tool",
9
+ ]);
4
10
  const MAX_DEPTH = 4;
5
11
  function walk(rootPath) {
6
12
  const results = [];
@@ -14,6 +20,9 @@ function walk(rootPath) {
14
20
  catch {
15
21
  return;
16
22
  }
23
+ // Filesystem enumeration order is not guaranteed. Keep discovery stable so
24
+ // precedence-sensitive facts (for example, the first CI command) are reproducible.
25
+ entries.sort((a, b) => a.name.localeCompare(b.name));
17
26
  for (const entry of entries) {
18
27
  if (entry.isDirectory()) {
19
28
  if (IGNORED_DIRS.has(entry.name))
@@ -21,6 +30,8 @@ function walk(rootPath) {
21
30
  recurse(path.join(dir, entry.name), depth + 1);
22
31
  }
23
32
  else {
33
+ if (entry.name.includes(".generated."))
34
+ continue;
24
35
  results.push(path.relative(rootPath, path.join(dir, entry.name)));
25
36
  }
26
37
  }
@@ -43,6 +54,56 @@ function readJsonIfExists(filePath) {
43
54
  return undefined;
44
55
  }
45
56
  }
57
+ function toPackageJsonManifest(raw, relativePath) {
58
+ return {
59
+ path: relativePath.split(path.sep).join("/"),
60
+ name: raw.name,
61
+ main: typeof raw.main === "string" ? raw.main : undefined,
62
+ dependencies: raw.dependencies ?? {},
63
+ devDependencies: raw.devDependencies ?? {},
64
+ scripts: raw.scripts ?? {},
65
+ moduleType: raw.type === "module" ? "module" : "commonjs",
66
+ packageManager: parsePackageManager(raw.packageManager),
67
+ };
68
+ }
69
+ function parsePackageManager(value) {
70
+ if (typeof value !== "string")
71
+ return undefined;
72
+ const name = value.trim().split("@")[0].toLowerCase();
73
+ return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun" ? name : undefined;
74
+ }
75
+ const LOCK_MANAGERS = {
76
+ "package-lock.json": "npm",
77
+ "npm-shrinkwrap.json": "npm",
78
+ "pnpm-lock.yaml": "pnpm",
79
+ "yarn.lock": "yarn",
80
+ "bun.lock": "bun",
81
+ "bun.lockb": "bun",
82
+ };
83
+ function managerFromClosestLock(manifestPath, normalizedFiles) {
84
+ let dir = path.posix.dirname(manifestPath.split(path.sep).join("/"));
85
+ while (true) {
86
+ for (const [lockName, manager] of Object.entries(LOCK_MANAGERS)) {
87
+ const candidate = dir === "." ? lockName : `${dir}/${lockName}`;
88
+ if (normalizedFiles.has(candidate))
89
+ return manager;
90
+ }
91
+ if (dir === ".")
92
+ return undefined;
93
+ dir = path.posix.dirname(dir);
94
+ }
95
+ }
96
+ function managerFromClosestDeclaration(manifestPath, declarations) {
97
+ let dir = path.posix.dirname(manifestPath);
98
+ while (true) {
99
+ const manager = declarations.get(dir);
100
+ if (manager)
101
+ return manager;
102
+ if (dir === ".")
103
+ return undefined;
104
+ dir = path.posix.dirname(dir);
105
+ }
106
+ }
46
107
  function readTextIfExists(filePath) {
47
108
  if (!fs.existsSync(filePath))
48
109
  return undefined;
@@ -76,6 +137,8 @@ const NON_PROJECT_DIRS = new Set([
76
137
  "example",
77
138
  "benchmark",
78
139
  "benchmarks",
140
+ "fixture",
141
+ "fixtures",
79
142
  ]);
80
143
  function isUnderNonProjectDir(relativePath) {
81
144
  const segments = relativePath.split(path.sep).slice(0, -1);
@@ -105,17 +168,45 @@ function pickShallowest(paths) {
105
168
  export function scanRepo(rootPath) {
106
169
  const files = walk(rootPath);
107
170
  const fileSet = new Set(files.map((f) => f.split(path.sep).join("/")));
108
- const packageJsonPath = findFirst(files, "package.json");
109
- const rawPackageJson = packageJsonPath
110
- ? readJsonIfExists(path.join(rootPath, packageJsonPath))
111
- : undefined;
112
- const packageJson = rawPackageJson
171
+ // Keep every project package manifest, not only the root workspace manifest. Root
172
+ // package.json files are often workspace glue and contain none of the dependencies
173
+ // that reveal React/Vitest/etc. Manifests under docs/examples/tooling are excluded to
174
+ // avoid treating auxiliary demos, fixtures and documentation builds as the project.
175
+ const packageJsonPaths = files.filter((f) => path.basename(f) === "package.json" && !isUnderNonProjectDir(f));
176
+ const packageJsons = packageJsonPaths
177
+ .map((relativePath) => {
178
+ const raw = readJsonIfExists(path.join(rootPath, relativePath));
179
+ return raw ? toPackageJsonManifest(raw, relativePath) : undefined;
180
+ })
181
+ .filter((manifest) => manifest !== undefined)
182
+ .sort((a, b) => {
183
+ const depth = a.path.split("/").length - b.path.split("/").length;
184
+ return depth || a.path.localeCompare(b.path);
185
+ });
186
+ const declaredManagers = new Map(packageJsons
187
+ .filter((manifest) => manifest.packageManager !== undefined)
188
+ .map((manifest) => [path.posix.dirname(manifest.path), manifest.packageManager]));
189
+ // A nested lock takes precedence over an ancestor workspace declaration because it
190
+ // denotes an independently installed package. Both lookups are O(path depth).
191
+ for (const manifest of packageJsons) {
192
+ if (manifest.packageManager)
193
+ continue;
194
+ manifest.packageManager =
195
+ managerFromClosestLock(manifest.path, fileSet) ??
196
+ managerFromClosestDeclaration(manifest.path, declaredManagers);
197
+ }
198
+ const primaryPackageJson = packageJsons[0];
199
+ const packageJson = primaryPackageJson
113
200
  ? {
114
- name: rawPackageJson.name,
115
- dependencies: rawPackageJson.dependencies ?? {},
116
- devDependencies: rawPackageJson.devDependencies ?? {},
117
- scripts: rawPackageJson.scripts ?? {},
118
- moduleType: rawPackageJson.type === "module" ? "module" : "commonjs",
201
+ name: primaryPackageJson.name,
202
+ dependencies: Object.assign({}, ...packageJsons.map((p) => p.dependencies)),
203
+ devDependencies: Object.assign({}, ...packageJsons.map((p) => p.devDependencies)),
204
+ // Root scripts remain the default command surface. Repo facts reads packageJsons
205
+ // directly and adds executable --prefix commands for nested packages.
206
+ scripts: primaryPackageJson.scripts,
207
+ moduleType: primaryPackageJson.moduleType,
208
+ packageManager: primaryPackageJson.packageManager,
209
+ main: primaryPackageJson.main,
119
210
  }
120
211
  : undefined;
121
212
  const composerJsonPath = findFirst(files, "composer.json");
@@ -147,37 +238,45 @@ export function scanRepo(rootPath) {
147
238
  // Multi-module Maven/Gradle projects (parent + child modules) each have their own
148
239
  // pom.xml/build.gradle, and the module that actually declares the framework/Kotlin
149
240
  // plugin/test runner isn't necessarily the shallowest one. Aggregate all of them.
150
- const pomPaths = findAllByNames(files, ["pom.xml"]);
151
- const buildGradlePaths = findAllByNames(files, ["build.gradle", "build.gradle.kts"]);
152
- const gemfilePath = findFirst(files, "Gemfile");
153
- const goModPath = findFirst(files, "go.mod");
154
- const cargoTomlPath = findFirst(files, "Cargo.toml");
241
+ const projectFiles = files.filter((file) => !isUnderNonProjectDir(file));
242
+ const pomPaths = findAllByNames(projectFiles, ["pom.xml"]);
243
+ const buildGradlePaths = findAllByNames(projectFiles, ["build.gradle", "build.gradle.kts"]);
244
+ const gemfilePath = findFirst(projectFiles, "Gemfile");
245
+ const goModPath = findFirst(projectFiles, "go.mod");
246
+ const cargoTomlPath = findFirst(projectFiles, "Cargo.toml");
155
247
  // .NET solutions routinely split into several .csproj files (domain/infra/web/tests);
156
248
  // picking just the "shallowest" one is close to arbitrary and often lands on a plain
157
249
  // class library that has neither the web framework nor the test runner reference.
158
250
  // Concatenate all of them so detection can find those references wherever they live.
159
- const csprojPaths = findAllByExtension(files, ".csproj");
160
- const packageSwiftPath = findFirst(files, "Package.swift");
251
+ const csprojPaths = findAllByExtension(projectFiles, ".csproj");
252
+ const packageSwiftPath = findFirst(projectFiles, "Package.swift");
161
253
  // Melos/pub workspaces have a root pubspec.yaml that's just workspace glue (no
162
254
  // `flutter`/`flutter_test` dependency) plus one real pubspec.yaml per package under
163
255
  // packages/*. Aggregate all of them so the actual framework/test runner is found.
164
- const pubspecYamlPaths = findAllByNames(files, ["pubspec.yaml"]);
165
- const cmakeListsPath = findFirst(files, "CMakeLists.txt");
256
+ const pubspecYamlPaths = findAllByNames(projectFiles, ["pubspec.yaml"]);
257
+ const cmakeListsPath = findFirst(projectFiles, "CMakeLists.txt");
166
258
  // Un Makefile que solo existe bajo docs/, tools/, etc. es tooling auxiliar (p. ej. el
167
259
  // Makefile de Sphinx en docs/ de Flask): sus targets no se pueden ejecutar desde la
168
260
  // raíz y no describen el proyecto. Mismo criterio que los manifiestos Python.
169
261
  const makefilePath = findFirstPreferringRealProjectDirs(files, "Makefile") ??
170
262
  findFirstPreferringRealProjectDirs(files, "makefile");
171
- const mixExsPath = findFirst(files, "mix.exs");
172
- const buildSbtPath = findFirst(files, "build.sbt");
173
- const rDescriptionPath = findFirst(files, "DESCRIPTION");
174
- const renvLockPath = findFirst(files, "renv.lock");
175
- const toxIniPath = findFirst(files, "tox.ini");
263
+ const mixExsPath = findFirst(projectFiles, "mix.exs");
264
+ const buildSbtPath = findFirst(projectFiles, "build.sbt");
265
+ const rDescriptionPath = findFirst(projectFiles, "DESCRIPTION");
266
+ const renvLockPath = findFirst(projectFiles, "renv.lock");
267
+ const toxIniPath = findFirst(projectFiles, "tox.ini");
176
268
  const workflowPaths = files.filter((f) => {
177
269
  const normalized = f.split(path.sep).join("/");
178
270
  return (normalized.startsWith(".github/workflows/") &&
179
271
  (normalized.endsWith(".yml") || normalized.endsWith(".yaml")));
180
272
  });
273
+ const guidanceNames = new Set([
274
+ "CONTRIBUTING.md", ".editorconfig", "tsconfig.json", "pyproject.toml",
275
+ ]);
276
+ const guidancePaths = files.filter((file) => {
277
+ const normalized = file.split(path.sep).join("/");
278
+ return guidanceNames.has(path.posix.basename(normalized));
279
+ });
181
280
  return {
182
281
  rootPath,
183
282
  files,
@@ -185,6 +284,7 @@ export function scanRepo(rootPath) {
185
284
  hasDir: (relativeDir) => fs.existsSync(path.join(rootPath, relativeDir)) &&
186
285
  fs.statSync(path.join(rootPath, relativeDir)).isDirectory(),
187
286
  packageJson,
287
+ packageJsons,
188
288
  pyprojectToml: pyprojectPath ? readTextIfExists(path.join(rootPath, pyprojectPath)) : undefined,
189
289
  requirementsTxt: requirementsPath
190
290
  ? readTextIfExists(path.join(rootPath, requirementsPath))
@@ -214,5 +314,8 @@ export function scanRepo(rootPath) {
214
314
  content: readTextIfExists(path.join(rootPath, p)),
215
315
  }))
216
316
  .filter((w) => w.content !== undefined),
317
+ guidanceFiles: guidancePaths
318
+ .map((p) => ({ path: p.split(path.sep).join("/"), content: readTextIfExists(path.join(rootPath, p)) }))
319
+ .filter((f) => f.content !== undefined),
217
320
  };
218
321
  }