bootproof 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +844 -152
  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 +730 -46
  9. package/dist/diagnosis.js +101 -16
  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 +329 -51
  14. package/dist/external-health.d.ts +16 -0
  15. package/dist/external-health.js +214 -0
  16. package/dist/infer.js +238 -39
  17. package/dist/plan.js +2 -0
  18. package/dist/proof.d.ts +78 -2
  19. package/dist/proof.js +265 -12
  20. package/dist/receipt.d.ts +52 -0
  21. package/dist/receipt.js +356 -0
  22. package/dist/redact.d.ts +4 -0
  23. package/dist/redact.js +86 -2
  24. package/dist/registry.d.ts +82 -30
  25. package/dist/registry.js +355 -53
  26. package/dist/remote.js +3 -3
  27. package/dist/repair-playbooks.d.ts +24 -0
  28. package/dist/repair-playbooks.js +593 -0
  29. package/dist/repair-safety.d.ts +130 -0
  30. package/dist/repair-safety.js +766 -0
  31. package/dist/repair.d.ts +43 -11
  32. package/dist/repair.js +716 -7
  33. package/dist/run.d.ts +3 -0
  34. package/dist/run.js +218 -41
  35. package/dist/sbom.d.ts +22 -0
  36. package/dist/sbom.js +99 -0
  37. package/dist/taxonomy.d.ts +8 -3
  38. package/dist/taxonomy.js +404 -8
  39. package/dist/types.d.ts +40 -1
  40. package/docs/AGENT_IN_THE_LOOP.md +171 -0
  41. package/docs/AGENT_RUN_RECEIPTS.md +38 -0
  42. package/docs/CI_ACTION.md +67 -2
  43. package/docs/DETERMINISTIC_REPAIR_SAFETY_MODEL.md +705 -0
  44. package/docs/DISTRIBUTION.md +83 -0
  45. package/docs/FAILURE_TAXONOMY.md +28 -1
  46. package/docs/HONESTY_CONTRACT.md +34 -12
  47. package/docs/LAUNCH_PLAYBOOK.md +232 -0
  48. package/docs/REAL_WORLD_FIXTURES.md +105 -0
  49. package/docs/REGISTRY.md +48 -28
  50. package/docs/REPAIR_RECEIPT.md +54 -8
  51. package/docs/agent-loop-gap-analysis.md +188 -0
  52. package/docs/examples/registry-seeds/advertised-port-mismatch.json +28 -0
  53. package/docs/examples/registry-seeds/airbyte-abctl-external-orchestrator.json +36 -0
  54. package/docs/examples/registry-seeds/go-ollama-service.json +36 -0
  55. package/docs/examples/registry-seeds/laravel-vite-sqlite.json +36 -0
  56. package/docs/examples/registry-seeds/monorepo-ambiguous-health.json +29 -0
  57. package/docs/examples/registry-seeds/php-composer.json +33 -0
  58. package/docs/examples/registry-seeds/rails-bundler.json +32 -0
  59. package/docs/examples/registry-seeds/sentry-devenv-direnv.json +41 -0
  60. package/docs/schemas/action-verdict-v1.schema.json +64 -0
  61. package/docs/schemas/agent-plan-v1.schema.json +148 -0
  62. package/docs/schemas/agent-run-receipts-v1.schema.json +192 -0
  63. package/docs/schemas/ai-repair-suggestion-v1.schema.json +70 -0
  64. package/docs/schemas/ci-context-v1.schema.json +63 -0
  65. package/docs/schemas/diff-result-v1.schema.json +66 -0
  66. package/docs/schemas/federated-receipt-v1.schema.json +51 -0
  67. package/docs/schemas/registry-entry-v1.schema.json +95 -0
  68. package/docs/schemas/registry-seed-example-v1.schema.json +102 -0
  69. package/docs/schemas/repair-action-v1.schema.json +136 -0
  70. package/docs/schemas/repair-receipt-v1.schema.json +221 -0
  71. package/package.json +21 -11
package/dist/infer.js CHANGED
@@ -42,6 +42,12 @@ const REPO_COMPOSE_FILES = [
42
42
  "docker/docker-compose.yml",
43
43
  "docker/docker-compose.yaml",
44
44
  ];
45
+ const VITE_CONFIG_FILES = [
46
+ "vite.config.js",
47
+ "vite.config.ts",
48
+ "vite.config.mjs",
49
+ "vite.config.cjs",
50
+ ];
45
51
  function detectRepoComposeFile(repo) {
46
52
  return REPO_COMPOSE_FILES.find(file => exists(repo, file)) ?? null;
47
53
  }
@@ -192,6 +198,45 @@ function pickAppCommand(pkg, pm) {
192
198
  }
193
199
  return { command: null, source: "no dev/start/serve/preview script found", script: null };
194
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
+ }
195
240
  function detectNestedFrontend(repo) {
196
241
  const preferred = ["superset-frontend", "frontend", "web", "ui", "client"];
197
242
  for (const dir of preferred) {
@@ -233,6 +278,52 @@ function detectGoEntrypoint(repo) {
233
278
  portFlag: /(?:Int|IntVar)\(\s*["']port["']/m.test(sourceText),
234
279
  };
235
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
+ }
236
327
  function detectRubyCommand(repo) {
237
328
  if (!exists(repo, "Gemfile") || !exists(repo, "bin/rails"))
238
329
  return null;
@@ -244,12 +335,13 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
244
335
  const setupPy = readText(repo, "setup.py");
245
336
  const requirements = readText(repo, "requirements.txt");
246
337
  const compose = repoComposeFile ? readText(repo, repoComposeFile) : "";
338
+ const composer = readJson(path.join(repo, "composer.json"));
247
339
  const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
248
340
  const nestedDeps = { ...(nestedFrontend?.pkg?.dependencies ?? {}), ...(nestedFrontend?.pkg?.devDependencies ?? {}) };
249
- 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"]);
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"]);
250
342
  if (isDirectory(repo, "pkg"))
251
343
  backendMarkers.push("pkg/");
252
- 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]);
253
345
  if (isDirectory(repo, "public"))
254
346
  frontendMarkers.push("public/");
255
347
  if (isDirectory(repo, "packages"))
@@ -261,18 +353,50 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
261
353
  (exists(repo, "superset/app.py") || exists(repo, "superset/config.py") || /\bflask\b/i.test(pyproject + setupPy + makefile));
262
354
  const hasFlask = hasFlaskBackend && (/\bflask\b/i.test(pyproject + setupPy + makefile) || exists(repo, "superset/app.py"));
263
355
  const hasDjango = exists(repo, "manage.py") && /\bdjango\b/i.test(pyproject + requirements);
264
- const hasPythonBackend = hasFlaskBackend || hasDjango;
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;
265
361
  const hasGoBackend = exists(repo, "go.mod") || exists(repo, "go.work");
266
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);
267
367
  const makeCommand = detectMakeCommand(repo, makefile);
268
368
  const goEntrypoint = detectGoEntrypoint(repo);
369
+ const goService = detectGoService(repo);
269
370
  const rubyCommand = detectRubyCommand(repo);
270
371
  const hasMakeDrivenBackend = Boolean(makefile && /^[A-Za-z0-9_.-]+:\s*(?:[^=]|$)/m.test(makefile));
271
- const hasNodeFrontend = Boolean(pkg) && (isDirectory(repo, "public") || isDirectory(repo, "packages") || exists(repo, "nx.json") || hasGoBackend || hasRubyBackend);
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);
272
379
  const hasReact = Boolean(rootDeps.react || nestedDeps.react);
273
380
  const hasReactFrontend = Boolean(nestedFrontend && hasReact);
274
381
  const hasCelery = /\bcelery\b/i.test(pyproject + setupPy + makefile + compose);
275
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
+ }
276
400
  const stack = [];
277
401
  if (hasPythonBackend)
278
402
  stack.push("python-backend");
@@ -284,8 +408,14 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
284
408
  stack.push("go-backend");
285
409
  if (hasRubyBackend)
286
410
  stack.push("ruby-backend");
287
- if (hasMakeDrivenBackend && !hasPythonBackend && !hasGoBackend && !hasRubyBackend)
411
+ if (hasPhpBackend)
412
+ stack.push("php-backend");
413
+ if (hasLaravel)
414
+ stack.push("laravel");
415
+ if (hasLargePythonNodeHybrid
416
+ || (hasMakeDrivenBackend && !hasPythonBackend && !hasGoBackend && !hasRubyBackend && !hasPhpBackend)) {
288
417
  stack.push("make-driven");
418
+ }
289
419
  if (hasNodeFrontend)
290
420
  stack.push("node-frontend");
291
421
  if (hasReactFrontend)
@@ -294,7 +424,7 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
294
424
  stack.push("react");
295
425
  if (rootDeps.next)
296
426
  stack.push("nextjs");
297
- if (rootDeps.vite)
427
+ if (rootDeps.vite || viteConfig)
298
428
  stack.push("vite");
299
429
  if (rootDeps.express)
300
430
  stack.push("express");
@@ -308,6 +438,10 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
308
438
  stack.push("docker-compose");
309
439
  if (hasCelery)
310
440
  stack.push("celery");
441
+ if (hasLargePythonNodeHybrid)
442
+ stack.push("large-hybrid-app");
443
+ if (hasLargePythonNodeHybrid && isDirectory(repo, "devservices"))
444
+ stack.push("devservices-backed");
311
445
  const setupSteps = [];
312
446
  if (/^\s*superset db upgrade\s*$/m.test(makefile))
313
447
  setupSteps.push("superset db upgrade");
@@ -327,18 +461,28 @@ function detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile) {
327
461
  workerCommand,
328
462
  makeCommand,
329
463
  goEntrypoint,
464
+ goService,
330
465
  rubyCommand,
331
466
  hasPythonBackend,
467
+ hasLargePythonNodeHybrid,
332
468
  hasFlask,
333
469
  hasDjango,
334
470
  hasGoBackend,
335
471
  hasRubyBackend,
472
+ hasPhpBackend,
473
+ hasLaravel,
474
+ hasLaravelViteFrontend,
475
+ sailCommand: hasLaravel && exists(repo, "vendor/bin/sail") ? "./vendor/bin/sail up" : null,
336
476
  hasMakeDrivenBackend,
337
477
  hasNodeFrontend,
338
478
  };
339
479
  }
340
- function detectPort(pkg, repo, commands) {
341
- 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");
342
486
  const m = sources.match(/(?:-p|--port)(?:=|\s+|[\\"]+)(\d{2,5})/);
343
487
  if (m)
344
488
  return { port: Number(m[1]), evidence: `port flag in command evidence: ${m[0].replace(/\\"/g, "").trim()}` };
@@ -349,10 +493,11 @@ function detectPort(pkg, repo, commands) {
349
493
  if (listen)
350
494
  return { port: Number(listen[1]), evidence: "HTTP listen address in source" };
351
495
  const envEx = readText(repo, ".env.example");
352
- const pm = envEx.match(/^PORT=(\d{2,5})/m);
496
+ const pm = envEx.match(/^(?:APP_)?PORT=(\d{2,5})/m);
353
497
  if (pm)
354
- return { port: Number(pm[1]), evidence: "PORT in .env.example" };
355
- 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` };
356
501
  }
357
502
  function detectServices(pkg, repo, repoComposeFile) {
358
503
  const out = [];
@@ -534,45 +679,74 @@ export function inferRepo(repoPath, opts = {}) {
534
679
  const architecture = detectArchitecture(repo, pkg, nestedFrontend, repoComposeFile);
535
680
  const pm = detectPackageManager(repo, pkg, nestedFrontend?.dir ?? null);
536
681
  const rootApp = pickAppCommand(pkg, pm.pm);
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;
537
692
  const commandEvidence = [
538
693
  architecture.flaskCommand,
539
694
  architecture.makeCommand?.command ?? null,
540
695
  architecture.goEntrypoint?.sourceText ?? null,
696
+ architecture.goService.command,
541
697
  architecture.rubyCommand?.commandBase ?? null,
698
+ architecture.sailCommand,
542
699
  ];
543
- const { port, evidence: portEvidence } = detectPort(pkg, repo, commandEvidence);
544
- const goCommand = architecture.goEntrypoint
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
545
706
  ? [
546
707
  architecture.goEntrypoint.commandBase,
547
708
  architecture.goEntrypoint.portFlag ? `--port ${port}` : "",
548
709
  architecture.goEntrypoint.dataDirFlag ? "--data .bootproof/runtime/go-app" : "",
549
710
  ].filter(Boolean).join(" ")
550
- : null;
711
+ : null);
551
712
  const rubyCommand = architecture.rubyCommand ? `${architecture.rubyCommand.commandBase} -p ${port}` : null;
552
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;
553
717
  const repositoryBackendCommand = architecture.makeCommand?.command ?? goCommand ?? rubyCommand;
554
- const backendCommand = architecture.flaskCommand ?? djangoCommand ?? repositoryBackendCommand;
718
+ const backendCommand = architecture.flaskCommand ?? djangoCommand ?? laravelCommand ?? repositoryBackendCommand;
555
719
  const nestedFrontendCommand = architecture.frontendMakeCommand;
556
720
  const frontendCommand = nestedFrontendCommand ?? rootApp.command;
557
721
  const appCommand = architecture.flaskCommand
558
722
  ?? djangoCommand
723
+ ?? laravelCommand
559
724
  ?? (architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command ? rootApp.command : null)
560
725
  ?? repositoryBackendCommand
561
726
  ?? rootApp.command;
727
+ const selectedProjectCliCommand = rootApp.command === appCommand ? projectCliCommand : null;
728
+ const selectedProjectCliReady = selectedProjectCliCommand === null ? null : projectCliReady;
562
729
  const appCommandSource = architecture.flaskCommand
563
730
  ? `Makefile Flask command: ${architecture.flaskCommand}`
564
731
  : djangoCommand && appCommand === djangoCommand
565
732
  ? "Django entrypoint: manage.py"
566
- : architecture.makeCommand && appCommand === architecture.makeCommand.command
567
- ? architecture.makeCommand.source
568
- : goCommand && appCommand === goCommand
569
- ? architecture.goEntrypoint?.source ?? rootApp.source
570
- : rubyCommand && appCommand === rubyCommand
571
- ? architecture.rubyCommand?.source ?? rootApp.source
572
- : rootApp.source;
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;
573
746
  const recognizedApplication = architecture.hasPythonBackend ||
574
747
  architecture.hasGoBackend ||
575
748
  architecture.hasRubyBackend ||
749
+ architecture.hasPhpBackend ||
576
750
  architecture.hasMakeDrivenBackend ||
577
751
  composeApplicationServices.some(service => service.source === "build");
578
752
  const workspaces = opts.workspace ? [] : rankWorkspaces(rootRepo, rootPkg);
@@ -582,6 +756,7 @@ export function inferRepo(repoPath, opts = {}) {
582
756
  const rootDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
583
757
  const preparationCommands = [];
584
758
  const nodePreparationRequired = Boolean(rootApp.command &&
759
+ !architecture.hasLaravel &&
585
760
  (Object.keys(rootDeps).length > 0 || exists(repo, "yarn.lock") || exists(repo, "pnpm-lock.yaml") || exists(repo, "package-lock.json") || exists(repo, "nx.json")));
586
761
  if (nodePreparationRequired) {
587
762
  const command = installCommand(pm.pm, pm.packageDir);
@@ -594,6 +769,9 @@ export function inferRepo(repoPath, opts = {}) {
594
769
  if (rubyCommand && appCommand === rubyCommand) {
595
770
  preparationCommands.push({ id: "bundle-install", kind: "install", command: "bundle install", description: "install declared Ruby gems", source: "Gemfile and bin/rails present" });
596
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
+ }
597
775
  const dependencyInstallRequired = preparationCommands.length > 0;
598
776
  const incompleteAppCommand = Boolean(architecture.hasGoBackend && architecture.hasNodeFrontend && rootApp.command);
599
777
  const multiAppCommand = Boolean(rootApp.command && /\b(?:turbo|nx)\s+run\s+dev\b[^\n]*--parallel\b/i.test(rootApp.source));
@@ -601,28 +779,38 @@ export function inferRepo(repoPath, opts = {}) {
601
779
  ? "multi-workspace development pipeline; no single application health target selected"
602
780
  : incompleteAppCommand
603
781
  ? "frontend/dev pipeline only; Go backend markers also detected"
604
- : architecture.hasFlask && nestedFrontend
605
- ? "Python/Flask backend command; React frontend and worker require separate orchestration"
606
- : djangoCommand && appCommand === djangoCommand
607
- ? "Django application command"
608
- : goCommand && appCommand === goCommand && nestedFrontend
609
- ? "Go application command serving repository-embedded frontend assets"
610
- : rubyCommand && appCommand === rubyCommand
611
- ? "Rails application command"
612
- : architecture.makeCommand && appCommand === architecture.makeCommand.command
613
- ? "repository-defined Make target"
614
- : appCommand
615
- ? "application command"
616
- : "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";
617
803
  const healthCandidates = notApp
618
804
  ? []
619
805
  : !appCommand && composeHealthCandidates.length
620
806
  ? composeHealthCandidates
621
807
  : !appCommand
622
808
  ? []
623
- : architecture.hasGoBackend && architecture.hasNodeFrontend
624
- ? [`http://localhost:${port}/api/health`, `http://localhost:${port}/`]
625
- : [`http://localhost:${port}/`];
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}/`];
626
814
  let confidence = 0;
627
815
  if (appCommand)
628
816
  confidence += 35;
@@ -636,6 +824,8 @@ export function inferRepo(repoPath, opts = {}) {
636
824
  confidence += 10;
637
825
  if (services.length || env.required.length)
638
826
  confidence += 5;
827
+ if (selectedProjectCliCommand && selectedProjectCliReady === false)
828
+ confidence = Math.min(confidence, 60);
639
829
  return {
640
830
  repoPath: repo,
641
831
  isApplication: !notApp,
@@ -656,14 +846,23 @@ export function inferRepo(repoPath, opts = {}) {
656
846
  dependencyInstallRequired,
657
847
  appCommand,
658
848
  appCommandSource,
849
+ selectedPackageScriptName: rootApp.command === appCommand ? rootApp.script : null,
850
+ selectedPackageScriptCommand: rootApp.command === appCommand ? rootScriptText || null : null,
851
+ projectCliCommand: selectedProjectCliCommand,
852
+ projectCliReady: selectedProjectCliReady,
659
853
  backendCommand,
660
854
  frontendCommand,
855
+ asset_dev_server_command: assetDevServerCommand,
661
856
  workerCommand: architecture.workerCommand,
662
857
  commandScope,
663
858
  incompleteAppCommand,
664
859
  multiAppCommand,
665
860
  port,
666
- 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",
667
866
  healthCandidates,
668
867
  services,
669
868
  requiredEnv: env.required,
package/dist/plan.js CHANGED
@@ -106,6 +106,8 @@ export function buildPlan(inf, provider) {
106
106
  steps,
107
107
  healthUrl,
108
108
  healthCandidates,
109
+ observedPort: inf.observedPort,
110
+ healthCandidateSource: inf.healthCandidateSource,
109
111
  generatedFiles: [
110
112
  ...(composeFileFor(inf) ? [{ path: "docker-compose.bootproof.yml", purpose: "service containers" }] : []),
111
113
  ...(envExampleFor(inf) ? [{ path: ".env.bootproof.example", purpose: "suggested local env values (never auto-applied)" }] : []),
package/dist/proof.d.ts CHANGED
@@ -1,6 +1,45 @@
1
- import type { Attestation, ObservedStep, RunPlan, FailureClass } from "./types.js";
2
- export declare const TOOL_ID = "bootproof@0.3.0";
1
+ import type { Attestation, AttestationTrust, ObservedStep, RunPlan, FailureClass, HealthEvidence, VerificationMode, ExternalVerificationClassification } from "./types.js";
2
+ export declare const TOOL_ID = "bootproof@0.4.1";
3
+ export type { AttestationTrust } from "./types.js";
4
+ export type SignerTrustTier = "invalid" | "self" | "known" | "unknown-foreign";
5
+ export interface SignatureTrustResult {
6
+ integrityValid: boolean;
7
+ tier: SignerTrustTier;
8
+ fingerprint: string | null;
9
+ label: string | null;
10
+ }
3
11
  export declare function gitInfo(repo: string): Attestation["repo"];
12
+ export declare function knownSignersPath(): string;
13
+ export interface RotationResult {
14
+ schema: "bootproof/key-rotation/v1";
15
+ rotatedAt: string;
16
+ oldPublicKey: string;
17
+ newPublicKey: string;
18
+ backedUpTo: string | null;
19
+ reSignedAttestation: boolean;
20
+ }
21
+ /**
22
+ * Rotate the local ed25519 signing key. The old key's public key is archived
23
+ * (so existing attestations remain independently verifiable), a new keypair is
24
+ * generated, and the latest attestation in the given repo is optionally
25
+ * re-signed with the new key.
26
+ *
27
+ * Rotation does NOT invalidate existing attestations — they still carry the
28
+ * old public key inline and verify with it. Rotation only affects what key
29
+ * future attestations will be signed with.
30
+ */
31
+ export declare function rotateSigner(opts?: {
32
+ repo?: string;
33
+ resignAttestation?: boolean;
34
+ backup?: boolean;
35
+ }): RotationResult;
36
+ export declare function signerFingerprint(publicKeyPem: string): string;
37
+ export declare function trustSigner(publicKeyPem: string, label?: string): {
38
+ fingerprint: string;
39
+ firstSeenAt: string;
40
+ label: string | null;
41
+ };
42
+ export declare function evaluateDetachedSignature(body: Buffer, signature: string | null | undefined, publicKeyPem: string | null | undefined): SignatureTrustResult;
4
43
  export declare function buildAttestation(input: {
5
44
  repo: string;
6
45
  plan: RunPlan;
@@ -9,16 +48,53 @@ export declare function buildAttestation(input: {
9
48
  booted: boolean;
10
49
  healthVerified: boolean;
11
50
  healthObservation: string | null;
51
+ healthEvidence?: HealthEvidence | null;
12
52
  observedHealthCandidates?: string[];
13
53
  failureClass: FailureClass | null;
14
54
  failureEvidence: string | null;
15
55
  explanation: string;
56
+ verificationMode?: VerificationMode;
57
+ bootproofOrchestrated?: boolean;
58
+ externalHealthUrl?: string | null;
59
+ observedStatus?: number | null;
60
+ observedFinalUrl?: string | null;
61
+ observedAt?: string | null;
62
+ responseSnippet?: string;
63
+ classification?: ExternalVerificationClassification | null;
64
+ trust?: AttestationTrust;
16
65
  }): Attestation;
66
+ /**
67
+ * Detect GitHub Actions OIDC environment. Present only when the workflow has
68
+ * `permissions: id-token: write`. The presence of these env vars IS the consent —
69
+ * the workflow author explicitly granted the OIDC scope.
70
+ */
71
+ export declare function detectOidcEnv(env?: NodeJS.ProcessEnv): {
72
+ requestUrl: string;
73
+ requestToken: string;
74
+ } | null;
75
+ /**
76
+ * Fetch the OIDC JWT from GitHub Actions and decode its claims (without verification —
77
+ * verification is the receiver's job, not the signer's). Returns a compact record of
78
+ * claims suitable for embedding in the attestation trust block.
79
+ */
80
+ export declare function fetchOidcClaims(requestUrl: string, requestToken: string, fetchImpl?: typeof fetch): Promise<Record<string, string>>;
81
+ /**
82
+ * Resolve the trust block for an attestation. When `--ci-oidc` is requested and
83
+ * the GitHub Actions OIDC environment is present, fetch the OIDC token and return
84
+ * a ci_oidc_signed trust block. Otherwise return local_developer_signed.
85
+ */
86
+ export declare function resolveTrust(opts?: {
87
+ ciOidc?: boolean;
88
+ env?: NodeJS.ProcessEnv;
89
+ fetchImpl?: typeof fetch;
90
+ }): Promise<AttestationTrust>;
17
91
  export declare function signDetached(body: Buffer): {
18
92
  signature: string;
19
93
  publicKeyPem: string;
20
94
  };
21
95
  export declare function verifyDetached(body: Buffer, signature: string, publicKeyPem: string): boolean;
22
96
  export declare function verifySignature(att: Attestation): boolean;
97
+ export declare function evaluateAttestationSignature(att: Attestation): SignatureTrustResult;
98
+ export declare function currentGitHead(repo: string): string | null;
23
99
  export declare function attestationPath(repo: string): string;
24
100
  export declare function writeAttestation(repo: string, att: Attestation): string;