bootproof 0.1.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.
Files changed (74) hide show
  1. package/README.md +873 -109
  2. package/dist/agent-plan.d.ts +44 -0
  3. package/dist/agent-plan.js +826 -0
  4. package/dist/agent-run.d.ts +117 -0
  5. package/dist/agent-run.js +459 -0
  6. package/dist/ai-repair.d.ts +58 -0
  7. package/dist/ai-repair.js +380 -0
  8. package/dist/cli.js +936 -38
  9. package/dist/diagnosis.js +114 -17
  10. package/dist/diff.d.ts +29 -0
  11. package/dist/diff.js +569 -0
  12. package/dist/exec.d.ts +30 -2
  13. package/dist/exec.js +332 -37
  14. package/dist/external-health.d.ts +16 -0
  15. package/dist/external-health.js +214 -0
  16. package/dist/infer.js +489 -41
  17. package/dist/plan.d.ts +2 -0
  18. package/dist/plan.js +49 -7
  19. package/dist/proof.d.ts +78 -2
  20. package/dist/proof.js +266 -13
  21. package/dist/receipt.d.ts +52 -0
  22. package/dist/receipt.js +356 -0
  23. package/dist/redact.d.ts +4 -0
  24. package/dist/redact.js +86 -2
  25. package/dist/registry.d.ts +82 -30
  26. package/dist/registry.js +355 -53
  27. package/dist/remote.d.ts +12 -1
  28. package/dist/remote.js +62 -18
  29. package/dist/repair-playbooks.d.ts +24 -0
  30. package/dist/repair-playbooks.js +593 -0
  31. package/dist/repair-safety.d.ts +130 -0
  32. package/dist/repair-safety.js +766 -0
  33. package/dist/repair.d.ts +142 -0
  34. package/dist/repair.js +1566 -0
  35. package/dist/run.d.ts +6 -1
  36. package/dist/run.js +385 -46
  37. package/dist/sbom.d.ts +22 -0
  38. package/dist/sbom.js +99 -0
  39. package/dist/taxonomy.d.ts +8 -2
  40. package/dist/taxonomy.js +428 -8
  41. package/dist/types.d.ts +57 -2
  42. package/docs/AGENT_IN_THE_LOOP.md +171 -0
  43. package/docs/AGENT_RUN_RECEIPTS.md +38 -0
  44. package/docs/CI_ACTION.md +71 -5
  45. package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
  46. package/docs/FAILURE_TAXONOMY.md +30 -1
  47. package/docs/HONESTY_CONTRACT.md +55 -4
  48. package/docs/LAUNCH_PLAYBOOK.md +232 -0
  49. package/docs/REAL_REPO_EVIDENCE.md +77 -0
  50. package/docs/REAL_WORLD_FIXTURES.md +105 -0
  51. package/docs/REGISTRY.md +48 -28
  52. package/docs/RELEASE_CHECKLIST.md +9 -1
  53. package/docs/REPAIR_RECEIPT.md +224 -0
  54. package/docs/agent-loop-gap-analysis.md +188 -0
  55. package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
  56. package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
  57. package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
  58. package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
  59. package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
  60. package/docs/examples/registry-seeds/php-composer.json +33 -0
  61. package/docs/examples/registry-seeds/rails-bundler.json +32 -0
  62. package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
  63. package/docs/schemas/action-verdict-v1.schema.json +64 -0
  64. package/docs/schemas/agent-plan-v1.schema.json +148 -0
  65. package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
  66. package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
  67. package/docs/schemas/ci-context-v1.schema.json +63 -0
  68. package/docs/schemas/diff-result-v1.schema.json +66 -0
  69. package/docs/schemas/federated-receipt-v1.schema.json +51 -0
  70. package/docs/schemas/registry-entry-v1.schema.json +95 -0
  71. package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
  72. package/docs/schemas/repair-action-v1.schema.json +136 -0
  73. package/docs/schemas/repair-receipt-v1.schema.json +221 -0
  74. package/package.json +13 -6
package/dist/infer.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { parse } from "yaml";
4
+ import { repoComposeRepairFile } from "./plan.js";
3
5
  function readJson(p) {
4
6
  try {
5
7
  return JSON.parse(fs.readFileSync(p, "utf8"));
@@ -30,6 +32,125 @@ function readText(repo, rel) {
30
32
  function present(repo, paths) {
31
33
  return paths.filter(rel => exists(repo, rel));
32
34
  }
35
+ const REPO_COMPOSE_FILES = [
36
+ "docker-compose.yml",
37
+ "docker-compose.yaml",
38
+ "compose.yaml",
39
+ "compose.yml",
40
+ "docker-compose.dev.yml",
41
+ "docker-compose.dev.yaml",
42
+ "docker/docker-compose.yml",
43
+ "docker/docker-compose.yaml",
44
+ ];
45
+ const VITE_CONFIG_FILES = [
46
+ "vite.config.js",
47
+ "vite.config.ts",
48
+ "vite.config.mjs",
49
+ "vite.config.cjs",
50
+ ];
51
+ function detectRepoComposeFile(repo) {
52
+ return REPO_COMPOSE_FILES.find(file => exists(repo, file)) ?? null;
53
+ }
54
+ function composePublishedPort(value) {
55
+ if (typeof value === "number")
56
+ return null;
57
+ if (typeof value === "object" && value !== null) {
58
+ const item = value;
59
+ const host = Number(item.published);
60
+ const container = Number(item.target);
61
+ return Number.isInteger(host) && Number.isInteger(container) ? { host, container } : null;
62
+ }
63
+ if (typeof value !== "string")
64
+ return null;
65
+ const normalized = value.replace(/\$\{[^}:]+:-?(\d+)\}/g, "$1").split("/")[0];
66
+ const parts = normalized.split(":").map(part => part.trim());
67
+ if (parts.length < 2)
68
+ return null;
69
+ const host = Number(parts.at(-2));
70
+ const container = Number(parts.at(-1));
71
+ return Number.isInteger(host) && Number.isInteger(container) ? { host, container } : null;
72
+ }
73
+ function composeHealthPath(service, containerPort) {
74
+ const test = service.healthcheck?.test;
75
+ const text = Array.isArray(test) ? test.join(" ") : typeof test === "string" ? test : "";
76
+ const url = text.match(/https?:\/\/(?:localhost|127\.0\.0\.1):(\d{2,5})(\/[^\s"'\\]*)?/i);
77
+ if (!url || Number(url[1]) !== containerPort)
78
+ return "/";
79
+ return url[2] || "/";
80
+ }
81
+ function sourceBuildUsesRepo(repo, composeFile, build) {
82
+ const context = typeof build === "string"
83
+ ? build
84
+ : typeof build === "object" && build !== null
85
+ ? String(build.context ?? ".")
86
+ : null;
87
+ if (!context || /^(?:https?|git):/i.test(context))
88
+ return false;
89
+ const composeDir = path.dirname(path.join(repo, composeFile));
90
+ const resolved = path.resolve(composeDir, context);
91
+ const root = path.resolve(repo);
92
+ return resolved === root || resolved.startsWith(`${root}${path.sep}`);
93
+ }
94
+ function detectComposeApplications(repo, composeFile) {
95
+ if (!composeFile)
96
+ return [];
97
+ try {
98
+ const document = parse(readText(repo, composeFile));
99
+ const applications = [];
100
+ for (const [name, service] of Object.entries(document?.services ?? {})) {
101
+ const source = sourceBuildUsesRepo(repo, composeFile, service.build) ? "build" : service.image ? "image" : null;
102
+ if (!source)
103
+ continue;
104
+ const healthCandidates = (Array.isArray(service.ports) ? service.ports : [])
105
+ .map(composePublishedPort)
106
+ .filter((port) => Boolean(port))
107
+ .filter(port => ![3306, 5432, 6379, 27017].includes(port.container))
108
+ .map(port => `http://localhost:${port.host}${composeHealthPath(service, port.container)}`);
109
+ if (healthCandidates.length)
110
+ applications.push({ name, source, healthCandidates: [...new Set(healthCandidates)] });
111
+ }
112
+ return applications;
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ }
118
+ function applyBootProofComposeOverride(repo, repoComposeFile, applications) {
119
+ if (!repoComposeFile)
120
+ return applications;
121
+ const repairFile = repoComposeRepairFile(repoComposeFile);
122
+ const overridePath = path.join(repo, repairFile);
123
+ if (!fs.existsSync(overridePath))
124
+ return applications;
125
+ try {
126
+ const document = parse(readText(repo, repairFile));
127
+ return applications.map(application => {
128
+ const service = document.services?.[application.name];
129
+ const port = (Array.isArray(service?.ports) ? service.ports : [])
130
+ .map(composePublishedPort)
131
+ .find((candidate) => Boolean(candidate));
132
+ if (!port)
133
+ return application;
134
+ return {
135
+ ...application,
136
+ healthCandidates: application.healthCandidates.map(candidate => {
137
+ try {
138
+ const url = new URL(candidate);
139
+ url.hostname = "localhost";
140
+ url.port = String(port.host);
141
+ return url.toString();
142
+ }
143
+ catch {
144
+ return candidate;
145
+ }
146
+ }),
147
+ };
148
+ });
149
+ }
150
+ catch {
151
+ return applications;
152
+ }
153
+ }
33
154
  function packageManagerFromField(field) {
34
155
  if (!field)
35
156
  return null;
@@ -77,6 +198,45 @@ function pickAppCommand(pkg, pm) {
77
198
  }
78
199
  return { command: null, source: "no dev/start/serve/preview script found", script: null };
79
200
  }
201
+ function shellSegmentExecutable(segment) {
202
+ let remaining = segment.trim();
203
+ while (/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/.test(remaining)) {
204
+ remaining = remaining.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+/, "");
205
+ }
206
+ return remaining.match(/^([A-Za-z0-9_.-]+)/)?.[1] ?? null;
207
+ }
208
+ function projectCliFromScript(scriptCommand, packageManager) {
209
+ const standardCommands = new Set([
210
+ "npm", "pnpm", "yarn", "bun", "node", "npx", "corepack",
211
+ "go", "ruby", "bundle", "make", "python", "python3", "php", "composer",
212
+ "cd", "echo", "env", "export", "sh", "bash", "true", "false", "test",
213
+ ]);
214
+ if (packageManager !== "unknown")
215
+ standardCommands.add(packageManager);
216
+ for (const segment of scriptCommand.split(/\s*(?:&&|\|\||;)\s*/)) {
217
+ const executable = shellSegmentExecutable(segment);
218
+ if (executable && !standardCommands.has(executable))
219
+ return executable;
220
+ }
221
+ return null;
222
+ }
223
+ function executableAvailable(repo, executable) {
224
+ const localBin = path.join(repo, "node_modules", ".bin", executable);
225
+ if (exists(repo, path.relative(repo, localBin)))
226
+ return true;
227
+ const extensions = process.platform === "win32"
228
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
229
+ : [""];
230
+ for (const directory of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
231
+ for (const extension of extensions) {
232
+ if (fs.existsSync(path.join(directory, `${executable}${extension.toLowerCase()}`))
233
+ || fs.existsSync(path.join(directory, `${executable}${extension.toUpperCase()}`))) {
234
+ return true;
235
+ }
236
+ }
237
+ }
238
+ return false;
239
+ }
80
240
  function detectNestedFrontend(repo) {
81
241
  const preferred = ["superset-frontend", "frontend", "web", "ui", "client"];
82
242
  for (const dir of preferred) {
@@ -86,40 +246,176 @@ function detectNestedFrontend(repo) {
86
246
  }
87
247
  return null;
88
248
  }
89
- function detectArchitecture(repo, pkg, nestedFrontend) {
249
+ function detectMakeCommand(repo, makefile) {
250
+ for (const target of ["run", "serve", "server", "start", "dev"]) {
251
+ if (new RegExp(`^${target}:`, "m").test(makefile)) {
252
+ return { command: `make ${target}`, source: `Makefile target: ${target}` };
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ function detectGoEntrypoint(repo) {
258
+ const candidates = [];
259
+ if (exists(repo, "main.go"))
260
+ candidates.push("main.go");
261
+ try {
262
+ for (const name of fs.readdirSync(path.join(repo, "cmd"))) {
263
+ if (exists(repo, `cmd/${name}/main.go`))
264
+ candidates.push(`cmd/${name}/main.go`);
265
+ }
266
+ }
267
+ catch { /* no cmd directory */ }
268
+ if (candidates.length !== 1)
269
+ return null;
270
+ const entry = candidates[0];
271
+ const sourceText = readText(repo, entry);
272
+ const packagePath = entry === "main.go" ? "." : `./${path.posix.dirname(entry)}`;
273
+ return {
274
+ commandBase: `go run ${packagePath}`,
275
+ source: `Go main package: ${entry}`,
276
+ sourceText,
277
+ dataDirFlag: /(?:String|StringVar)\(\s*["']data["']/m.test(sourceText),
278
+ portFlag: /(?:Int|IntVar)\(\s*["']port["']/m.test(sourceText),
279
+ };
280
+ }
281
+ function detectGoService(repo) {
282
+ if (!exists(repo, "go.mod")) {
283
+ return {
284
+ ollamaLike: false,
285
+ command: null,
286
+ commandSource: null,
287
+ port: null,
288
+ healthCandidates: [],
289
+ markers: [],
290
+ };
291
+ }
292
+ const moduleFile = readText(repo, "go.mod");
293
+ const mainFile = readText(repo, "main.go");
294
+ const commandFile = readText(repo, "cmd/cmd.go");
295
+ const routesFile = readText(repo, "server/routes.go");
296
+ const envConfigFile = readText(repo, "envconfig/config.go");
297
+ const evidence = [moduleFile, mainFile, commandFile, routesFile, envConfigFile].join("\n");
298
+ const exactModule = /^module\s+github\.com\/ollama\/ollama\s*$/m.test(moduleFile);
299
+ const ollamaModule = /^module\s+\S*ollama(?:\/\S*)?\s*$/mi.test(moduleFile);
300
+ const markers = [
301
+ exactModule || ollamaModule ? "ollama module" : null,
302
+ /\bOLLAMA_HOST\b/.test(evidence) ? "OLLAMA_HOST" : null,
303
+ /\b11434\b/.test(evidence) ? "port 11434" : null,
304
+ /["'`]\/api\/tags["'`]/.test(evidence) ? "/api/tags" : null,
305
+ /\bUse:\s*["']serve["']|\bollama serve\b/i.test(evidence) ? "serve command" : null,
306
+ ].filter((marker) => marker !== null);
307
+ const hasRootEntrypoint = exists(repo, "main.go");
308
+ const hasServeEvidence = markers.includes("serve command");
309
+ const strongOllamaEvidence = exactModule || markers.length >= 3;
310
+ const ollamaLike = hasRootEntrypoint && hasServeEvidence && strongOllamaEvidence;
311
+ return {
312
+ ollamaLike,
313
+ command: ollamaLike ? "go run . serve" : null,
314
+ commandSource: ollamaLike ? "Ollama Go service entrypoint: main.go + serve command" : null,
315
+ port: ollamaLike ? 11434 : null,
316
+ healthCandidates: ollamaLike
317
+ ? [
318
+ "http://127.0.0.1:11434/",
319
+ "http://localhost:11434/",
320
+ "http://127.0.0.1:11434/api/tags",
321
+ "http://localhost:11434/api/tags",
322
+ ]
323
+ : [],
324
+ markers,
325
+ };
326
+ }
327
+ function detectRubyCommand(repo) {
328
+ if (!exists(repo, "Gemfile") || !exists(repo, "bin/rails"))
329
+ return null;
330
+ return { commandBase: "bundle exec rails server -b 127.0.0.1", source: "Rails entrypoint: bin/rails" };
331
+ }
332
+ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
90
333
  const makefile = readText(repo, "Makefile");
91
334
  const pyproject = readText(repo, "pyproject.toml");
92
335
  const setupPy = readText(repo, "setup.py");
93
- const compose = readText(repo, "docker-compose.yml") + readText(repo, "docker-compose.yaml") + readText(repo, "compose.yaml");
336
+ const requirements = readText(repo, "requirements.txt");
337
+ const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
338
+ const composer = readJson(path.join(repo, "composer.json"));
94
339
  const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
95
340
  const nestedDeps = { ...(nestedFrontend?.pkg?.dependencies ?? {}), ...(nestedFrontend?.pkg?.devDependencies ?? {}) };
96
- const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "go.mod", "go.work", "Makefile", "superset/app.py", "superset/config.py"]);
341
+ const backendMarkers = present(repo, ["pyproject.toml", "setup.py", "requirements.txt", "manage.py", "go.mod", "go.work", "Gemfile", "config/database.yml", "Makefile", "superset/app.py", "superset/config.py", "artisan", "composer.json"]);
97
342
  if (isDirectory(repo, "pkg"))
98
343
  backendMarkers.push("pkg/");
99
- const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json"]);
344
+ const frontendMarkers = present(repo, ["package.json", "yarn.lock", "pnpm-lock.yaml", "nx.json", ...VITE_CONFIG_FILES]);
100
345
  if (isDirectory(repo, "public"))
101
346
  frontendMarkers.push("public/");
102
347
  if (isDirectory(repo, "packages"))
103
348
  frontendMarkers.push("packages/");
104
349
  if (nestedFrontend)
105
350
  frontendMarkers.push(`${nestedFrontend.dir}/package.json`);
106
- const serviceMarkers = present(repo, ["docker-compose.yml", "docker-compose.yaml", "compose.yaml", "docker-compose-light.yml"]);
107
- const hasPythonBackend = (exists(repo, "pyproject.toml") || exists(repo, "setup.py")) &&
351
+ const serviceMarkers = present(repo, [...REPO_COMPOSE_FILES, "docker-compose-light.yml"]);
352
+ const hasFlaskBackend = (exists(repo, "pyproject.toml") || exists(repo, "setup.py")) &&
108
353
  (exists(repo, "superset/app.py") || exists(repo, "superset/config.py") || /\bflask\b/i.test(pyproject + setupPy + makefile));
109
- const hasFlask = hasPythonBackend && (/\bflask\b/i.test(pyproject + setupPy + makefile) || exists(repo, "superset/app.py"));
354
+ const hasFlask = hasFlaskBackend && (/\bflask\b/i.test(pyproject + setupPy + makefile) || exists(repo, "superset/app.py"));
355
+ const hasDjango = exists(repo, "manage.py") && /\bdjango\b/i.test(pyproject + requirements);
356
+ const hasLargePythonNodeHybrid = exists(repo, "pyproject.toml")
357
+ && exists(repo, "Makefile")
358
+ && exists(repo, "package.json")
359
+ && exists(repo, "pnpm-lock.yaml");
360
+ const hasPythonBackend = hasFlaskBackend || hasDjango || hasLargePythonNodeHybrid;
110
361
  const hasGoBackend = exists(repo, "go.mod") || exists(repo, "go.work");
111
- const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public") || isDirectory(repo, "packages") || exists(repo, "nx.json") || hasGoBackend);
362
+ const hasRubyBackend = exists(repo, "Gemfile");
363
+ const hasLaravel = exists(repo, "artisan") && Boolean(composer);
364
+ const hasPhpBackend = hasLaravel;
365
+ const viteConfig = VITE_CONFIG_FILES.find(file => exists(repo, file)) ?? null;
366
+ const hasLaravelViteFrontend = hasLaravel && Boolean(pkg) && Boolean(viteConfig);
367
+ const makeCommand = detectMakeCommand(repo, makefile);
368
+ const goEntrypoint = detectGoEntrypoint(repo);
369
+ const goService = detectGoService(repo);
370
+ const rubyCommand = detectRubyCommand(repo);
371
+ const hasMakeDrivenBackend = Boolean(makefile && /^[A-Za-z0-9_.-]+:\s*(?:[^=]|$)/m.test(makefile));
372
+ const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public")
373
+ || isDirectory(repo, "packages")
374
+ || exists(repo, "nx.json")
375
+ || hasGoBackend
376
+ || hasRubyBackend
377
+ || hasLaravelViteFrontend
378
+ || hasLargePythonNodeHybrid);
112
379
  const hasReact = Boolean(rootDeps.react || nestedDeps.react);
113
380
  const hasReactFrontend = Boolean(nestedFrontend && hasReact);
114
381
  const hasCelery = /\bcelery\b/i.test(pyproject + setupPy + makefile + compose);
115
382
  const hasCompose = serviceMarkers.length > 0;
383
+ if (hasLargePythonNodeHybrid) {
384
+ if (isDirectory(repo, "src"))
385
+ backendMarkers.push("src/");
386
+ if (isDirectory(repo, "static"))
387
+ frontendMarkers.push("static/");
388
+ if (isDirectory(repo, "devservices"))
389
+ serviceMarkers.push("devservices/");
390
+ }
391
+ if (hasGoBackend) {
392
+ if (exists(repo, "main.go"))
393
+ backendMarkers.push("main.go");
394
+ if (isDirectory(repo, "cmd"))
395
+ backendMarkers.push("cmd/");
396
+ if (isDirectory(repo, "server"))
397
+ backendMarkers.push("server/");
398
+ backendMarkers.push(...goService.markers);
399
+ }
116
400
  const stack = [];
117
401
  if (hasPythonBackend)
118
402
  stack.push("python-backend");
119
403
  if (hasFlask)
120
404
  stack.push("flask");
405
+ if (hasDjango)
406
+ stack.push("django");
121
407
  if (hasGoBackend)
122
408
  stack.push("go-backend");
409
+ if (hasRubyBackend)
410
+ stack.push("ruby-backend");
411
+ if (hasPhpBackend)
412
+ stack.push("php-backend");
413
+ if (hasLaravel)
414
+ stack.push("laravel");
415
+ if (hasLargePythonNodeHybrid
416
+ || (hasMakeDrivenBackend && !hasPythonBackend && !hasGoBackend && !hasRubyBackend && !hasPhpBackend)) {
417
+ stack.push("make-driven");
418
+ }
123
419
  if (hasNodeFrontend)
124
420
  stack.push("node-frontend");
125
421
  if (hasReactFrontend)
@@ -128,7 +424,7 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
128
424
  stack.push("react");
129
425
  if (rootDeps.next)
130
426
  stack.push("nextjs");
131
- if (rootDeps.vite)
427
+ if (rootDeps.vite || viteConfig)
132
428
  stack.push("vite");
133
429
  if (rootDeps.express)
134
430
  stack.push("express");
@@ -142,6 +438,10 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
142
438
  stack.push("docker-compose");
143
439
  if (hasCelery)
144
440
  stack.push("celery");
441
+ if (hasLargePythonNodeHybrid)
442
+ stack.push("large-hybrid-app");
443
+ if (hasLargePythonNodeHybrid && isDirectory(repo, "devservices"))
444
+ stack.push("devservices-backed");
145
445
  const setupSteps = [];
146
446
  if (/^\s*superset db upgrade\s*$/m.test(makefile))
147
447
  setupSteps.push("superset db upgrade");
@@ -150,7 +450,6 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
150
450
  const flaskCommand = makefile.match(/^\s*(flask run[^\n]*)$/m)?.[1].trim() ?? null;
151
451
  const frontendMakeCommand = makefile.match(/^\s*(cd\s+[^;\n]+;\s*(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev-server|dev|start)[^\n]*)$/m)?.[1].trim() ?? null;
152
452
  const workerCommand = makefile.match(/^\s*(celery\s+--app=[^\n]*\sworker[^\n]*)$/m)?.[1].trim() ?? null;
153
- const makeRunCommand = hasGoBackend && /^run:\s/m.test(makefile) ? "make run" : null;
154
453
  return {
155
454
  backendMarkers: [...new Set(backendMarkers)],
156
455
  frontendMarkers: [...new Set(frontendMarkers)],
@@ -160,29 +459,51 @@ function detectArchitecture(repo, pkg, nestedFrontend) {
160
459
  flaskCommand,
161
460
  frontendMakeCommand,
162
461
  workerCommand,
163
- makeRunCommand,
462
+ makeCommand,
463
+ goEntrypoint,
464
+ goService,
465
+ rubyCommand,
164
466
  hasPythonBackend,
467
+ hasLargePythonNodeHybrid,
165
468
  hasFlask,
469
+ hasDjango,
166
470
  hasGoBackend,
471
+ hasRubyBackend,
472
+ hasPhpBackend,
473
+ hasLaravel,
474
+ hasLaravelViteFrontend,
475
+ sailCommand: hasLaravel && exists(repo, "vendor/bin/sail") ? "./vendor/bin/sail up" : null,
476
+ hasMakeDrivenBackend,
167
477
  hasNodeFrontend,
168
478
  };
169
479
  }
170
- function detectPort(pkg, repo, commands) {
171
- const sources = [JSON.stringify(pkg?.scripts ?? {}), readText(repo, "Makefile"), ...commands.filter((v) => Boolean(v))].join("\n");
480
+ function detectPort(pkg, repo, commands, options = {}) {
481
+ const sources = [
482
+ options.ignorePackageScripts ? "" : JSON.stringify(pkg?.scripts ?? {}),
483
+ readText(repo, "Makefile"),
484
+ ...commands.filter((v) => Boolean(v)),
485
+ ].join("\n");
172
486
  const m = sources.match(/(?:-p|--port)(?:=|\s+|[\\"]+)(\d{2,5})/);
173
487
  if (m)
174
488
  return { port: Number(m[1]), evidence: `port flag in command evidence: ${m[0].replace(/\\"/g, "").trim()}` };
489
+ const goDefault = sources.match(/(?:SetDefault\(\s*["']port["']\s*,|(?:Int|IntVar)\(\s*["']port["']\s*,)\s*(\d{2,5})/);
490
+ if (goDefault)
491
+ return { port: Number(goDefault[1]), evidence: "port default in Go entrypoint" };
492
+ const listen = sources.match(/ListenAndServe\(\s*["']:(\d{2,5})["']/);
493
+ if (listen)
494
+ return { port: Number(listen[1]), evidence: "HTTP listen address in source" };
175
495
  const envEx = readText(repo, ".env.example");
176
- const pm = envEx.match(/^PORT=(\d{2,5})/m);
496
+ const pm = envEx.match(/^(?:APP_)?PORT=(\d{2,5})/m);
177
497
  if (pm)
178
- return { port: Number(pm[1]), evidence: "PORT in .env.example" };
179
- return { port: 3000, evidence: "default assumption (3000); not evidence-based" };
498
+ return { port: Number(pm[1]), evidence: `${pm[0].split("=")[0]} in .env.example` };
499
+ const defaultPort = options.defaultPort ?? 3000;
500
+ return { port: defaultPort, evidence: `default assumption (${defaultPort}); not evidence-based` };
180
501
  }
181
- function detectServices(pkg, repo) {
502
+ function detectServices(pkg, repo, repoComposeFile) {
182
503
  const out = [];
183
504
  const envEx = readText(repo, ".env.example") + readText(repo, ".env.sample");
184
505
  const schema = readText(repo, "prisma/schema.prisma");
185
- const compose = readText(repo, "docker-compose.yml") + readText(repo, "docker-compose.yaml") + readText(repo, "compose.yaml");
506
+ const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
186
507
  const pyproject = readText(repo, "pyproject.toml");
187
508
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
188
509
  if (/postgres(ql)?:\/\//i.test(envEx) || /provider\s*=\s*"postgresql"/.test(schema) || deps.pg || /^\s{0,4}postgres:\s*$/m.test(compose))
@@ -229,15 +550,16 @@ function workspacePatterns(repo, pkg) {
229
550
  return [...new Set(patterns)];
230
551
  }
231
552
  function expandWorkspacePattern(repo, pattern) {
232
- if (!pattern.includes("*"))
233
- return exists(repo, `${pattern}/package.json`) ? [pattern] : [];
234
- const beforeStar = pattern.slice(0, pattern.indexOf("*"));
553
+ const normalizedPattern = pattern.replace(/\\/g, "/");
554
+ if (!normalizedPattern.includes("*"))
555
+ return exists(repo, `${normalizedPattern}/package.json`) ? [normalizedPattern] : [];
556
+ const beforeStar = normalizedPattern.slice(0, normalizedPattern.indexOf("*"));
235
557
  const base = beforeStar.replace(/\/$/, "");
236
- const suffix = pattern.slice(pattern.indexOf("*") + 1).replace(/^\//, "");
558
+ const suffix = normalizedPattern.slice(normalizedPattern.indexOf("*") + 1).replace(/^\//, "");
237
559
  const baseAbs = path.join(repo, base);
238
560
  try {
239
561
  return fs.readdirSync(baseAbs)
240
- .map(name => path.join(base, name, suffix))
562
+ .map(name => path.posix.join(base, name, suffix))
241
563
  .filter(dir => exists(repo, `${dir}/package.json`));
242
564
  }
243
565
  catch {
@@ -245,6 +567,7 @@ function expandWorkspacePattern(repo, pattern) {
245
567
  }
246
568
  }
247
569
  const TEST_PATH = /(^|[-_/])(e2e|tests?|test-plugins|fixtures?|examples?|samples?|demos?|mocks?)([-_/]|$)/i;
570
+ const DOCUMENTATION_PATH = /(^|[\/])(storybook|docs?)([\/]|$)/i;
248
571
  const SCAFFOLD_NAME = /^create-|-(template|example|fixture|sandbox|sample|demo|mock)$/i;
249
572
  function scoreWorkspace(repo, dir, wpkg, isRoot) {
250
573
  const { command } = pickAppCommand(wpkg, "npm");
@@ -293,6 +616,10 @@ function scoreWorkspace(repo, dir, wpkg, isRoot) {
293
616
  score -= 10;
294
617
  reasons.push("test/example path downranked");
295
618
  }
619
+ if (DOCUMENTATION_PATH.test(`${dir}/${wpkg?.name ?? ""}`)) {
620
+ score -= 3;
621
+ reasons.push("documentation/storybook downranked");
622
+ }
296
623
  if (wpkg?.private !== true && (wpkg?.main || wpkg?.exports) && !command) {
297
624
  score -= 2;
298
625
  reasons.push("looks like a publishable library");
@@ -343,41 +670,147 @@ export function inferRepo(repoPath, opts = {}) {
343
670
  const rootRepo = path.resolve(repoPath);
344
671
  const rootPkg = opts.workspace ? readJson(path.join(rootRepo, "package.json")) : pkg;
345
672
  const nestedFrontend = detectNestedFrontend(repo);
346
- const architecture = detectArchitecture(repo, pkg, nestedFrontend);
673
+ const repoComposeFile = detectRepoComposeFile(repo);
674
+ const composeApplicationServices = applyBootProofComposeOverride(repo, repoComposeFile, detectComposeApplications(repo, repoComposeFile));
675
+ const sourceComposeApplications = composeApplicationServices.filter(service => service.source === "build");
676
+ const composeHealthCandidates = sourceComposeApplications.length === 1
677
+ ? sourceComposeApplications[0].healthCandidates
678
+ : [];
679
+ const architecture = detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile);
347
680
  const pm = detectPackageManager(repo, pkg, nestedFrontend?.dir ?? null);
348
681
  const rootApp = pickAppCommand(pkg, pm.pm);
349
- const backendCommand = architecture.flaskCommand ?? architecture.makeRunCommand;
682
+ const rootScriptText = rootApp.script ? String(pkg?.scripts?.[rootApp.script] ?? "") : "";
683
+ const projectCliCommand = architecture.hasLargePythonNodeHybrid && rootScriptText
684
+ ? projectCliFromScript(rootScriptText, pm.pm)
685
+ : null;
686
+ const projectCliReady = projectCliCommand === null
687
+ ? null
688
+ : executableAvailable(repo, projectCliCommand);
689
+ const assetDevServerCommand = architecture.hasLaravelViteFrontend && /\bvite\b/i.test(rootScriptText)
690
+ ? rootApp.command
691
+ : null;
692
+ const commandEvidence = [
693
+ architecture.flaskCommand,
694
+ architecture.makeCommand?.command ?? null,
695
+ architecture.goEntrypoint?.sourceText ?? null,
696
+ architecture.goService.command,
697
+ architecture.rubyCommand?.commandBase ?? null,
698
+ architecture.sailCommand,
699
+ ];
700
+ const { port, evidence: portEvidence } = detectPort(pkg, repo, commandEvidence, {
701
+ ignorePackageScripts: architecture.hasLaravel,
702
+ defaultPort: architecture.goService.port
703
+ ?? (architecture.sailCommand ? 80 : architecture.hasLaravel ? 8000 : 3000),
704
+ });
705
+ const goCommand = architecture.goService.command ?? (architecture.goEntrypoint
706
+ ? [
707
+ architecture.goEntrypoint.commandBase,
708
+ architecture.goEntrypoint.portFlag ? `--port ${port}` : "",
709
+ architecture.goEntrypoint.dataDirFlag ? "--data .bootproof/runtime/go-app" : "",
710
+ ].filter(Boolean).join(" ")
711
+ : null);
712
+ const rubyCommand = architecture.rubyCommand ? `${architecture.rubyCommand.commandBase} -p ${port}` : null;
713
+ const djangoCommand = architecture.hasDjango ? `python manage.py runserver 127.0.0.1:${port}` : null;
714
+ const laravelCommand = architecture.hasLaravel
715
+ ? architecture.sailCommand ?? `php artisan serve --host=127.0.0.1 --port=${port}`
716
+ : null;
717
+ const repositoryBackendCommand = architecture.makeCommand?.command ?? goCommand ?? rubyCommand;
718
+ const backendCommand = architecture.flaskCommand ?? djangoCommand ?? laravelCommand ?? repositoryBackendCommand;
350
719
  const nestedFrontendCommand = architecture.frontendMakeCommand;
351
720
  const frontendCommand = nestedFrontendCommand ?? rootApp.command;
352
- const appCommand = architecture.flaskCommand ?? rootApp.command;
721
+ const appCommand = architecture.flaskCommand
722
+ ?? djangoCommand
723
+ ?? laravelCommand
724
+ ?? (architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command ? rootApp.command : null)
725
+ ?? repositoryBackendCommand
726
+ ?? rootApp.command;
727
+ const selectedProjectCliCommand = rootApp.command === appCommand ? projectCliCommand : null;
728
+ const selectedProjectCliReady = selectedProjectCliCommand === null ? null : projectCliReady;
353
729
  const appCommandSource = architecture.flaskCommand
354
730
  ? `Makefile Flask command: ${architecture.flaskCommand}`
355
- : rootApp.source;
356
- const recognizedApplication = architecture.hasPythonBackend || architecture.hasGoBackend;
731
+ : djangoCommand && appCommand === djangoCommand
732
+ ? "Django entrypoint: manage.py"
733
+ : laravelCommand && appCommand === laravelCommand
734
+ ? architecture.sailCommand
735
+ ? "Laravel Sail entrypoint: vendor/bin/sail"
736
+ : "Laravel entrypoint: artisan"
737
+ : architecture.makeCommand && appCommand === architecture.makeCommand.command
738
+ ? architecture.makeCommand.source
739
+ : goCommand && appCommand === goCommand
740
+ ? architecture.goService.commandSource ?? architecture.goEntrypoint?.source ?? rootApp.source
741
+ : rubyCommand && appCommand === rubyCommand
742
+ ? architecture.rubyCommand?.source ?? rootApp.source
743
+ : selectedProjectCliCommand && selectedProjectCliReady === false
744
+ ? `${rootApp.source}; project CLI ${selectedProjectCliCommand} readiness not established`
745
+ : rootApp.source;
746
+ const recognizedApplication = architecture.hasPythonBackend ||
747
+ architecture.hasGoBackend ||
748
+ architecture.hasRubyBackend ||
749
+ architecture.hasPhpBackend ||
750
+ architecture.hasMakeDrivenBackend ||
751
+ composeApplicationServices.some(service => service.source === "build");
357
752
  const workspaces = opts.workspace ? [] : rankWorkspaces(rootRepo, rootPkg);
358
753
  const notApp = looksLikeLibrary(pkg, appCommand, workspaces.some(candidate => candidate.dir !== "."), recognizedApplication);
359
- const { port, evidence: portEvidence } = detectPort(pkg, repo, [backendCommand, frontendCommand]);
360
754
  const env = detectEnv(repo);
361
- const services = detectServices(pkg, repo);
755
+ const services = detectServices(pkg, repo, repoComposeFile);
362
756
  const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
363
- const dependencyInstallRequired = Boolean(rootApp.command &&
757
+ const preparationCommands = [];
758
+ const nodePreparationRequired = Boolean(rootApp.command &&
759
+ !architecture.hasLaravel &&
364
760
  (Object.keys(rootDeps).length > 0 || exists(repo, "yarn.lock") || exists(repo, "pnpm-lock.yaml") || exists(repo, "package-lock.json") || exists(repo, "nx.json")));
761
+ if (nodePreparationRequired) {
762
+ const command = installCommand(pm.pm, pm.packageDir);
763
+ if (command)
764
+ preparationCommands.push({ id: "install", kind: "install", command, description: "install Node dependencies", source: pm.evidence });
765
+ }
766
+ if (goCommand && appCommand === goCommand && exists(repo, "go.sum")) {
767
+ preparationCommands.push({ id: "go-modules", kind: "install", command: "go mod download", description: "download declared Go modules", source: "go.sum present" });
768
+ }
769
+ if (rubyCommand && appCommand === rubyCommand) {
770
+ preparationCommands.push({ id: "bundle-install", kind: "install", command: "bundle install", description: "install declared Ruby gems", source: "Gemfile and bin/rails present" });
771
+ }
772
+ if (laravelCommand && appCommand === laravelCommand) {
773
+ preparationCommands.push({ id: "composer-install", kind: "install", command: "composer install", description: "install declared PHP dependencies", source: "composer.json and artisan present" });
774
+ }
775
+ const dependencyInstallRequired = preparationCommands.length > 0;
365
776
  const incompleteAppCommand = Boolean(architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command);
366
777
  const multiAppCommand = Boolean(rootApp.command && /\b(?:turbo|nx)\s+run\s+dev\b[^\n]*--parallel\b/i.test(rootApp.source));
367
778
  const commandScope = multiAppCommand
368
779
  ? "multi-workspace development pipeline; no single application health target selected"
369
780
  : incompleteAppCommand
370
781
  ? "frontend/dev pipeline only; Go backend markers also detected"
371
- : architecture.hasPythonBackend && nestedFrontend
372
- ? "Python/Flask backend command; React frontend and worker require separate orchestration"
373
- : appCommand
374
- ? "application command"
375
- : "no runnable command selected";
782
+ : architecture.hasLargePythonNodeHybrid && rootApp.command === appCommand
783
+ ? selectedProjectCliCommand && selectedProjectCliReady === false
784
+ ? `large Python/Node hybrid package script; project CLI ${selectedProjectCliCommand} readiness not established`
785
+ : "large Python/Node hybrid package script"
786
+ : architecture.hasFlask && nestedFrontend
787
+ ? "Python/Flask backend command; React frontend and worker require separate orchestration"
788
+ : djangoCommand && appCommand === djangoCommand
789
+ ? "Django application command"
790
+ : laravelCommand && appCommand === laravelCommand
791
+ ? architecture.sailCommand
792
+ ? "Laravel application through Sail"
793
+ : "Laravel application command; Vite is an asset development server only"
794
+ : goCommand && appCommand === goCommand && nestedFrontend
795
+ ? "Go application command serving repository-embedded frontend assets"
796
+ : rubyCommand && appCommand === rubyCommand
797
+ ? "Rails application command"
798
+ : architecture.makeCommand && appCommand === architecture.makeCommand.command
799
+ ? "repository-defined Make target"
800
+ : appCommand
801
+ ? "application command"
802
+ : "no runnable command selected";
376
803
  const healthCandidates = notApp
377
804
  ? []
378
- : architecture.hasGoBackend && architecture.hasNodeFrontend
379
- ? [`http://localhost:${port}/api/health`, `http://localhost:${port}/`]
380
- : [`http://localhost:${port}/`];
805
+ : !appCommand && composeHealthCandidates.length
806
+ ? composeHealthCandidates
807
+ : !appCommand
808
+ ? []
809
+ : architecture.goService.ollamaLike
810
+ ? architecture.goService.healthCandidates
811
+ : architecture.hasGoBackend && architecture.hasNodeFrontend
812
+ ? [`http://localhost:${port}/api/health`, `http://localhost:${port}/`]
813
+ : [`http://localhost:${port}/`];
381
814
  let confidence = 0;
382
815
  if (appCommand)
383
816
  confidence += 35;
@@ -391,6 +824,8 @@ export function inferRepo(repoPath, opts = {}) {
391
824
  confidence += 10;
392
825
  if (services.length || env.required.length)
393
826
  confidence += 5;
827
+ if (selectedProjectCliCommand && selectedProjectCliReady === false)
828
+ confidence = Math.min(confidence, 60);
394
829
  return {
395
830
  repoPath: repo,
396
831
  isApplication: !notApp,
@@ -399,22 +834,35 @@ export function inferRepo(repoPath, opts = {}) {
399
834
  backendMarkers: architecture.backendMarkers,
400
835
  frontendMarkers: architecture.frontendMarkers,
401
836
  serviceMarkers: architecture.serviceMarkers,
837
+ repoComposeFile,
838
+ composeApplicationServices,
839
+ composeHealthCandidates,
402
840
  setupSteps: architecture.setupSteps,
403
841
  packageManager: pm.pm,
404
842
  packageManagerEvidence: pm.evidence,
405
843
  packageManagerVersion: pm.version ?? pkg?.engines?.[pm.pm] ?? rootPkg?.engines?.[pm.pm] ?? null,
406
- installCommand: installCommand(pm.pm, pm.packageDir),
844
+ installCommand: preparationCommands.find(command => command.kind === "install")?.command ?? null,
845
+ preparationCommands,
407
846
  dependencyInstallRequired,
408
847
  appCommand,
409
848
  appCommandSource,
849
+ selectedPackageScriptName: rootApp.command === appCommand ? rootApp.script : null,
850
+ selectedPackageScriptCommand: rootApp.command === appCommand ? rootScriptText || null : null,
851
+ projectCliCommand: selectedProjectCliCommand,
852
+ projectCliReady: selectedProjectCliReady,
410
853
  backendCommand,
411
854
  frontendCommand,
855
+ asset_dev_server_command: assetDevServerCommand,
412
856
  workerCommand: architecture.workerCommand,
413
857
  commandScope,
414
858
  incompleteAppCommand,
415
859
  multiAppCommand,
416
860
  port,
417
- portEvidence,
861
+ portEvidence: architecture.goService.port
862
+ ? "known Ollama service port from repository evidence"
863
+ : portEvidence,
864
+ observedPort: null,
865
+ healthCandidateSource: architecture.goService.ollamaLike ? "known_service" : "inferred",
418
866
  healthCandidates,
419
867
  services,
420
868
  requiredEnv: env.required,