agent-rules-init 0.3.0 → 0.5.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,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
  }
@@ -1,44 +1,82 @@
1
1
  import { UI } from "./i18n.js";
2
- function renderSection(entries, lang) {
2
+ import { SOURCE_FILES } from "./canonical-commands.js";
3
+ function renderSection(entries, lang, options = {}) {
3
4
  const ui = UI[lang];
5
+ const { summaries = true } = options;
4
6
  return entries
5
7
  .map(({ detection, ruleSet }) => {
6
- const conventions = ruleSet.conventions.map((c) => `- ${c}`).join("\n");
8
+ const conventionItems = options.operationalConventions === false
9
+ ? ruleSet.conventions.filter((item) => !/^(?:Run the tests|Run the repository|Ejecuta los tests|Ejecuta la suite)/i.test(item))
10
+ : ruleSet.conventions;
11
+ const conventions = conventionItems.map((c) => `- ${c}`).join("\n");
7
12
  const architecture = ruleSet.architectureNotes.map((a) => `- ${a}`).join("\n");
8
- return [
13
+ const parts = [
9
14
  `## ${detection.language} (${detection.packId})`,
10
- "",
11
- ruleSet.summary,
12
- "",
13
- `### ${ui.sections.conventions}`,
14
- conventions,
15
- "",
16
- `### ${ui.sections.architecture}`,
17
- architecture,
18
- ].join("\n");
15
+ ];
16
+ if (summaries)
17
+ parts.push("", ruleSet.summary);
18
+ if (options.conventions !== false && conventions)
19
+ parts.push("", `### ${ui.sections.conventions}`, conventions);
20
+ if (options.architecture !== false && architecture)
21
+ parts.push("", `### ${ui.sections.architecture}`, architecture);
22
+ return parts.join("\n");
19
23
  })
20
24
  .join("\n\n");
21
25
  }
22
- const SOURCE_FILES = {
23
- npm: "package.json",
24
- composer: "composer.json",
25
- make: "Makefile",
26
- mix: "mix.exs",
27
- tox: "tox.ini",
28
- };
26
+ function evidenceLabel(lang) {
27
+ return lang === "es" ? "evidencia" : "evidence";
28
+ }
29
+ function renderEvidenceSection(title, facts, lang) {
30
+ const high = facts.filter((fact) => fact.confidence === "high");
31
+ if (high.length === 0)
32
+ return "";
33
+ const lines = high.map((fact) => `- ${fact.statement} (${evidenceLabel(lang)}: ${fact.evidence.map((item) => `\`${item}\``).join(", ")})`);
34
+ return [`## ${title}`, "", ...lines].join("\n");
35
+ }
36
+ function architectureFactsTitle(lang) {
37
+ return lang === "es" ? "Arquitectura observada" : "Observed architecture";
38
+ }
39
+ function localConventionsTitle(lang) {
40
+ return lang === "es" ? "Convenciones locales verificadas" : "Verified local conventions";
41
+ }
42
+ function renderCanonical(facts, lang) {
43
+ const commands = facts.canonical.filter((command) => command.confidence === "high");
44
+ if (commands.length === 0)
45
+ return "";
46
+ return [
47
+ `## ${UI[lang].sections.canonical}`,
48
+ "",
49
+ ...commands.map((command) => `- ${command.kind}: \`${command.command}\` (${command.source})`),
50
+ ].join("\n");
51
+ }
29
52
  export function renderRepoFacts(facts, lang) {
30
53
  const ui = UI[lang];
31
54
  const sections = [];
55
+ const canonical = facts.canonical.filter((c) => c.confidence === "high");
56
+ if (canonical.length > 0) {
57
+ const lines = canonical.map((c) => `- ${c.kind}: \`${c.command}\` (${c.source})`);
58
+ sections.push([`## ${ui.sections.canonical}`, "", ...lines].join("\n"));
59
+ }
32
60
  if (facts.commands.length > 0) {
33
- const lines = facts.commands.map((c) => c.detail && c.detail !== c.invocation
34
- ? `- \`${c.invocation}\` \`${c.detail}\` (${SOURCE_FILES[c.source]})`
35
- : `- \`${c.invocation}\` (${SOURCE_FILES[c.source]})`);
61
+ const lines = facts.commands.map((c) => {
62
+ const sourceFile = c.manifestPath ?? SOURCE_FILES[c.source];
63
+ return c.detail && c.detail !== c.invocation
64
+ ? `- \`${c.invocation}\` → \`${c.detail}\` (${sourceFile})`
65
+ : `- \`${c.invocation}\` (${sourceFile})`;
66
+ });
36
67
  for (const o of facts.omittedCommands)
37
68
  lines.push(`- ${ui.andMore(o.count, SOURCE_FILES[o.source])}`);
38
69
  sections.push([`## ${ui.sections.commands}`, "", ...lines].join("\n"));
39
70
  }
40
- if (facts.structure.length > 0) {
71
+ if (facts.structure.length > 0 || facts.testDirs.length > 0 || facts.entrypoints.length > 0) {
41
72
  const lines = facts.structure.map((d) => (d.note ? `- \`${d.dir}\` — ${d.note}` : `- \`${d.dir}\``));
73
+ for (const dir of facts.testDirs) {
74
+ if (!facts.structure.some((d) => d.dir === dir))
75
+ lines.push(`- \`${dir}\` — ${ui.testDirNote}`);
76
+ }
77
+ for (const entry of facts.entrypoints) {
78
+ lines.push(`- ${ui.entrypointNote}: \`${entry.target}\` (${entry.source} "${entry.label}")`);
79
+ }
42
80
  sections.push([`## ${ui.sections.structure}`, "", ...lines].join("\n"));
43
81
  }
44
82
  if (facts.ciCommands.length > 0) {
@@ -47,6 +85,12 @@ export function renderRepoFacts(facts, lang) {
47
85
  lines.push(`- ${ui.andMore(facts.omittedCiCount)}`);
48
86
  sections.push([`## ${ui.sections.ci}`, "", ...lines].join("\n"));
49
87
  }
88
+ const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
89
+ if (architecture)
90
+ sections.push(architecture);
91
+ const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
92
+ if (conventions)
93
+ sections.push(conventions);
50
94
  return sections.join("\n\n");
51
95
  }
52
96
  function renderDocument(title, entries, facts, lang) {
@@ -56,7 +100,7 @@ function renderDocument(title, entries, facts, lang) {
56
100
  "",
57
101
  UI[lang].generatedHeader,
58
102
  "",
59
- renderSection(entries, lang),
103
+ renderSection(entries, lang, { architecture: !(facts?.architectureFacts?.length) }),
60
104
  ...(factsBlock ? ["", factsBlock] : []),
61
105
  ].join("\n");
62
106
  }
@@ -64,10 +108,39 @@ export function renderClaudeMd(entries, facts, lang) {
64
108
  return renderDocument("# CLAUDE.md", entries, facts, lang);
65
109
  }
66
110
  export function renderAgentsMd(entries, facts, lang) {
67
- return renderDocument("# AGENTS.md", entries, facts, lang);
111
+ const intro = lang === "es"
112
+ ? "Reglas operativas para modificar este repositorio. Respeta el alcance y valida los cambios con los comandos indicados."
113
+ : "Operational rules for modifying this repository. Respect scope and validate changes with the commands below.";
114
+ const blocks = [
115
+ "# AGENTS.md", "", UI[lang].generatedHeader, "", intro, "",
116
+ renderSection(entries, lang, { architecture: !(facts?.architectureFacts?.length) }),
117
+ ];
118
+ if (facts) {
119
+ const canonical = renderCanonical(facts, lang);
120
+ const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
121
+ const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
122
+ for (const block of [canonical, architecture, conventions])
123
+ if (block)
124
+ blocks.push("", block);
125
+ }
126
+ return blocks.join("\n");
68
127
  }
69
128
  export function renderCopilotInstructions(entries, facts, lang) {
70
- return renderDocument("# Copilot Instructions", entries, facts, lang);
129
+ const intro = lang === "es"
130
+ ? "Aplica estas convenciones al completar y modificar código. Omite tareas operativas de terminal salvo que sean necesarias para el cambio."
131
+ : "Apply these conventions when completing and modifying code. Omit terminal operations unless the change requires them.";
132
+ const blocks = [
133
+ "# Copilot Instructions", "", UI[lang].generatedHeader, "", intro, "",
134
+ renderSection(entries, lang, { architecture: false, operationalConventions: false }),
135
+ ];
136
+ if (facts) {
137
+ const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
138
+ const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
139
+ for (const block of [conventions, architecture])
140
+ if (block)
141
+ blocks.push("", block);
142
+ }
143
+ return blocks.join("\n");
71
144
  }
72
145
  export function renderPromptFiles(packId, templates) {
73
146
  return templates.flatMap((template) => [
@@ -1,10 +1,17 @@
1
1
  import type { Lang } from "./i18n.js";
2
+ export type JsPackageManager = "npm" | "pnpm" | "yarn" | "bun";
2
3
  export interface PackageJsonManifest {
3
4
  name?: string;
5
+ main?: string;
4
6
  dependencies: Record<string, string>;
5
7
  devDependencies: Record<string, string>;
6
8
  scripts: Record<string, string>;
7
9
  moduleType: "module" | "commonjs";
10
+ /** Gestor aplicable al manifiesto, declarado en packageManager o inferido por el lock más cercano. */
11
+ packageManager?: JsPackageManager;
12
+ }
13
+ export interface LocatedPackageJsonManifest extends PackageJsonManifest {
14
+ path: string;
8
15
  }
9
16
  export interface ComposerJsonManifest {
10
17
  require: Record<string, string>;
@@ -17,6 +24,7 @@ export interface RepoSignals {
17
24
  hasFile: (relativePath: string) => boolean;
18
25
  hasDir: (relativeDir: string) => boolean;
19
26
  packageJson?: PackageJsonManifest;
27
+ packageJsons?: LocatedPackageJsonManifest[];
20
28
  pyprojectToml?: string;
21
29
  requirementsTxt?: string;
22
30
  environmentYml?: string;
@@ -40,6 +48,11 @@ export interface RepoSignals {
40
48
  path: string;
41
49
  content: string;
42
50
  }[];
51
+ /** Archivos locales de guía/configuración seleccionados de forma conservadora. */
52
+ guidanceFiles?: {
53
+ path: string;
54
+ content: string;
55
+ }[];
43
56
  }
44
57
  export type Confidence = "high" | "low";
45
58
  export interface DetectionField<T> {
@@ -53,6 +66,8 @@ export interface DetectionResult {
53
66
  packageManager?: DetectionField<string>;
54
67
  testRunner?: DetectionField<string>;
55
68
  linter?: DetectionField<string>;
69
+ /** Framework implementado por el propio repositorio; no implica una dependencia. */
70
+ frameworkSource?: string;
56
71
  usesTypeScript?: boolean;
57
72
  moduleFormat?: "module" | "commonjs";
58
73
  }
@@ -66,11 +81,12 @@ export interface PromptTemplate {
66
81
  title: string;
67
82
  body: string;
68
83
  }
69
- export type CommandSource = "npm" | "composer" | "make" | "mix" | "tox";
84
+ export type CommandSource = JsPackageManager | "composer" | "make" | "mix" | "tox";
70
85
  export interface CommandEntry {
71
86
  source: CommandSource;
72
87
  invocation: string;
73
88
  detail?: string;
89
+ manifestPath?: string;
74
90
  }
75
91
  export interface DirEntry {
76
92
  dir: string;
@@ -80,6 +96,27 @@ export interface CiCommand {
80
96
  command: string;
81
97
  workflow: string;
82
98
  }
99
+ export interface CanonicalCommand {
100
+ kind: "test" | "lint" | "build" | "format" | "typecheck";
101
+ command: string;
102
+ /** Archivo o señal del que procede el comando, p. ej. `package.json`, `ci: ci.yml`, `mvnw`. */
103
+ source: string;
104
+ confidence: Confidence;
105
+ /** "." para la raíz del repo; ruta del workspace para unidades anidadas. */
106
+ scope: string;
107
+ }
108
+ export interface EvidenceFact {
109
+ statement: string;
110
+ evidence: string[];
111
+ scope: string;
112
+ confidence: Confidence;
113
+ }
114
+ export interface ArchitectureFact extends EvidenceFact {
115
+ kind: "entrypoint" | "tests" | "layers" | "workspace" | "source-layout";
116
+ }
117
+ export interface ConventionFact extends EvidenceFact {
118
+ kind: "formatting" | "typescript" | "python-style" | "contributing";
119
+ }
83
120
  export interface RepoFacts {
84
121
  commands: CommandEntry[];
85
122
  omittedCommands: {
@@ -89,10 +126,24 @@ export interface RepoFacts {
89
126
  structure: DirEntry[];
90
127
  ciCommands: CiCommand[];
91
128
  omittedCiCount: number;
129
+ canonical: CanonicalCommand[];
130
+ testDirs: string[];
131
+ entrypoints: {
132
+ label: string;
133
+ target: string;
134
+ source: string;
135
+ }[];
136
+ architectureFacts: ArchitectureFact[];
137
+ conventionFacts: ConventionFact[];
138
+ }
139
+ export interface PackContext {
140
+ facts: RepoFacts;
141
+ /** Señales del mismo alcance que facts; permite evitar canónicos de otro stack. */
142
+ signals?: RepoSignals;
92
143
  }
93
144
  export interface Pack {
94
145
  id: string;
95
146
  detect(signals: RepoSignals): DetectionResult | null;
96
- rules(detection: DetectionResult, lang: Lang): RuleSet;
97
- promptTemplates(detection: DetectionResult, lang: Lang): PromptTemplate[];
147
+ rules(detection: DetectionResult, lang: Lang, ctx?: PackContext): RuleSet;
148
+ promptTemplates(detection: DetectionResult, lang: Lang, ctx?: PackContext): PromptTemplate[];
98
149
  }
@@ -4,16 +4,16 @@ export function writeGeneratedFiles(rootPath, files) {
4
4
  return files.map(({ path: relativePath, content }) => {
5
5
  const absolutePath = path.join(rootPath, relativePath);
6
6
  try {
7
- // An already-existing file is the expected outcome on a re-run (the tool's
8
- // no-overwrite guarantee), not a failure — report it as skipped, exit 0.
9
- if (fs.existsSync(absolutePath)) {
10
- return { path: relativePath, status: "skipped" };
11
- }
12
7
  fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
13
- fs.writeFileSync(absolutePath, content);
8
+ // `wx` makes the no-overwrite guarantee atomic: even if another process creates
9
+ // the file between directory creation and this call, Node returns EEXIST.
10
+ fs.writeFileSync(absolutePath, content, { flag: "wx" });
14
11
  return { path: relativePath, status: "written" };
15
12
  }
16
13
  catch (err) {
14
+ if (err.code === "EEXIST") {
15
+ return { path: relativePath, status: "skipped" };
16
+ }
17
17
  return { path: relativePath, status: "error", error: err.message };
18
18
  }
19
19
  });
package/dist/packs/cpp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, unknownRunnerLabel, } from "../core/i18n.js";
1
+ import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, } from "../core/i18n.js";
2
2
  const FRAMEWORKS = [
3
3
  ["find_package(qt", "qt"],
4
4
  ["find_package(boost", "boost"],
@@ -79,8 +79,8 @@ function rules(detection, lang) {
79
79
  }
80
80
  function promptTemplates(detection, lang) {
81
81
  const t = TEXTS[lang];
82
- const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
83
- const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner.value : unknownRunnerLabel(lang);
82
+ const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
83
+ const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : undefined;
84
84
  return [
85
85
  { id: "review", title: "Code Review (C/C++)", body: reviewBody(lang, t.reviewFocus, framework) },
86
86
  { id: "refactor", title: "Refactor (C/C++)", body: refactorBody(lang) },
@@ -1,4 +1,4 @@
1
- import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, unknownRunnerLabel, } from "../core/i18n.js";
1
+ import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, } from "../core/i18n.js";
2
2
  function detect(signals) {
3
3
  const source = signals.csproj;
4
4
  if (!source)
@@ -53,8 +53,8 @@ function rules(detection, lang) {
53
53
  }
54
54
  function promptTemplates(detection, lang) {
55
55
  const t = TEXTS[lang];
56
- const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
57
- const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner.value : unknownRunnerLabel(lang);
56
+ const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
57
+ const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : undefined;
58
58
  return [
59
59
  { id: "review", title: "Code Review (C#/.NET)", body: reviewBody(lang, t.reviewFocus, framework) },
60
60
  { id: "refactor", title: "Refactor (C#/.NET)", body: refactorBody(lang) },
@@ -1,4 +1,4 @@
1
- import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, unknownRunnerLabel, } from "../core/i18n.js";
1
+ import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, } from "../core/i18n.js";
2
2
  function detect(signals) {
3
3
  const source = signals.pubspecYaml;
4
4
  if (!source)
@@ -44,13 +44,13 @@ function rules(detection, lang) {
44
44
  const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : undefined;
45
45
  return {
46
46
  summary: summarySentence(lang, "Dart", framework, "pub"),
47
- conventions: [t.style, runTestsConvention(lang, runner ?? unknownRunnerLabel(lang)), t.deps],
47
+ conventions: [t.style, runTestsConvention(lang, runner), t.deps],
48
48
  architectureNotes: [framework === "flutter" ? t.archFlutter : t.archPlain, t.immutability],
49
49
  };
50
50
  }
51
51
  function promptTemplates(detection, lang) {
52
- const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
53
- const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner.value : unknownRunnerLabel(lang);
52
+ const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
53
+ const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : undefined;
54
54
  return [
55
55
  { id: "review", title: "Code Review (Dart)", body: reviewBody(lang, "", framework) },
56
56
  { id: "refactor", title: "Refactor (Dart)", body: refactorBody(lang) },
@@ -1,4 +1,4 @@
1
- import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, } from "../core/i18n.js";
1
+ import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, } from "../core/i18n.js";
2
2
  function detect(signals) {
3
3
  const source = signals.mixExs;
4
4
  if (!source)
@@ -50,7 +50,7 @@ function rules(detection, lang) {
50
50
  }
51
51
  function promptTemplates(detection, lang) {
52
52
  const t = TEXTS[lang];
53
- const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
53
+ const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
54
54
  return [
55
55
  { id: "review", title: "Code Review (Elixir)", body: reviewBody(lang, t.reviewFocus, framework) },
56
56
  { id: "refactor", title: "Refactor (Elixir)", body: refactorBody(lang) },
package/dist/packs/go.js CHANGED
@@ -1,4 +1,4 @@
1
- import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, } from "../core/i18n.js";
1
+ import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, } from "../core/i18n.js";
2
2
  const FRAMEWORKS = [
3
3
  ["gin-gonic/gin", "gin"],
4
4
  ["labstack/echo", "echo"],
@@ -68,7 +68,7 @@ function rules(detection, lang) {
68
68
  }
69
69
  function promptTemplates(detection, lang) {
70
70
  const t = TEXTS[lang];
71
- const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
71
+ const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
72
72
  return [
73
73
  { id: "review", title: "Code Review (Go)", body: reviewBody(lang, t.reviewFocus, framework) },
74
74
  { id: "refactor", title: "Refactor (Go)", body: refactorBody(lang) },