@bendyline/gezel-service 0.1.0 → 1.0.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 (108) hide show
  1. package/dist/NOTICE.md +13 -5
  2. package/dist/bin/gezeld.js +56269 -50686
  3. package/dist/handboek-content/README.md +24 -2
  4. package/dist/handboek-content/conceptual/connected-apps.md +2 -0
  5. package/dist/handboek-content/technical/building-connected-apps-with-gezel-app-sdk.md +144 -0
  6. package/dist/handboek-content/technical/cli-reference.md +11 -0
  7. package/dist/handboek-content/technical/how-we-test-models.md +57 -9
  8. package/dist/handboek-content/technical/model-scorecard.md +27 -9
  9. package/dist/handboek-content/technical/npm-packages.md +67 -0
  10. package/dist/handboek-content/technical/security-model.md +18 -1
  11. package/dist/handboek-content/technical/where-files-live.md +3 -0
  12. package/dist/handboek-content/technical/writing-scripts-with-gezel-sdk.md +106 -0
  13. package/dist/handboek-content/whats-new/1.26224.md +82 -0
  14. package/dist/handboek-content/whats-new/whats-new-index.md +20 -0
  15. package/dist/handboek.d.ts +53 -26
  16. package/dist/handboek.js +109 -27
  17. package/dist/index-store/static-index-worker.js +518 -98
  18. package/dist/index.d.ts +377 -22
  19. package/dist/index.js +56204 -50670
  20. package/dist/ui/assets/{TerminalCodeEditor-CB-AXtKI.js → TerminalCodeEditor-BpkIEKrW.js} +2 -2
  21. package/dist/ui/assets/{_.contribution-C6KlZ1yR.js → _.contribution-DESswd3T.js} +1 -1
  22. package/dist/ui/assets/{chunk-6N2J7C2B-DIF7DJtk.js → chunk-6N2J7C2B-Du7clJkV.js} +1 -1
  23. package/dist/ui/assets/chunk-ILCJ3WFD-fHFoZ9wm.js +4 -0
  24. package/dist/ui/assets/{chunk-S5PCVMKU-CmFF6ff9.js → chunk-S5PCVMKU-DopU3Q9j.js} +2 -2
  25. package/dist/ui/assets/chunk-USU6HTKB-B-ElCUQS.js +1 -0
  26. package/dist/ui/assets/{chunk-WATAOG4Y-perfmDXF.js → chunk-WATAOG4Y-BnCgSncI.js} +1 -1
  27. package/dist/ui/assets/{cpp.contribution-BBBWN3Dm.js → cpp.contribution-DazoHQwn.js} +1 -1
  28. package/dist/ui/assets/{csharp.contribution-jBA6TWhG.js → csharp.contribution-B64MdTmq.js} +1 -1
  29. package/dist/ui/assets/{css.contribution-DQSNU6vC.js → css.contribution-u617lXYH.js} +1 -1
  30. package/dist/ui/assets/{cssMode-68UP_lrS.js → cssMode-Cf_QqRjJ.js} +1 -1
  31. package/dist/ui/assets/{csv-Ctux8fO7.js → csv-RAgECoUg.js} +1 -1
  32. package/dist/ui/assets/dist-DB6xxrlD.js +2 -0
  33. package/dist/ui/assets/dist-DucbO86I.js +222 -0
  34. package/dist/ui/assets/{doc-BdMAKGDm.js → doc-B7NXSLxH.js} +33 -23
  35. package/dist/ui/assets/{dockerfile.contribution-CSpSPKgK.js → dockerfile.contribution-P8T1FxHz.js} +1 -1
  36. package/dist/ui/assets/docx-IZY6hX8A.js +2 -0
  37. package/dist/ui/assets/{epub-oLed1tAU.js → epub-00L2M424.js} +1 -1
  38. package/dist/ui/assets/extract-UTAOG7LV-BZ_L7lLL.js +1 -0
  39. package/dist/ui/assets/{go.contribution-BjibqVfF.js → go.contribution-BqqmPpYF.js} +1 -1
  40. package/dist/ui/assets/{handlebars-Dq5dJ5WJ.js → handlebars-B9qcPqjD.js} +1 -1
  41. package/dist/ui/assets/handlebars.contribution-vZslMuRE.js +2 -0
  42. package/dist/ui/assets/{html-fQ6X2xaa.js → html-Cgejr9YM.js} +24 -24
  43. package/dist/ui/assets/{html-Bgyk9kiZ.js → html-Fp2jEZ7j.js} +1 -1
  44. package/dist/ui/assets/html.contribution-B1GzaEV-.js +2 -0
  45. package/dist/ui/assets/{htmlMode-D5UKInqK.js → htmlMode-B5cPhGim.js} +1 -1
  46. package/dist/ui/assets/{index-CPXp6BEZ.js → index-CQSh1NUQ.js} +193 -205
  47. package/dist/ui/assets/index-Ch0qGtVJ.css +1 -0
  48. package/dist/ui/assets/infer-v-bCXeT9.js +2 -0
  49. package/dist/ui/assets/{ini.contribution-BiJ4T5K-.js → ini.contribution-XkZYQphY.js} +1 -1
  50. package/dist/ui/assets/{java.contribution-B8o1lBqP.js → java.contribution-CUQitE0a.js} +1 -1
  51. package/dist/ui/assets/{javascript-COWkS1tr.js → javascript-CcBUpTjJ.js} +1 -1
  52. package/dist/ui/assets/javascript.contribution-D6DhUpPP.js +2 -0
  53. package/dist/ui/assets/{jsonMode-CWWZvfzr.js → jsonMode-D9SbmDW6.js} +1 -1
  54. package/dist/ui/assets/{kotlin.contribution-CxSOPQ6P.js → kotlin.contribution-B9AYd3Yp.js} +1 -1
  55. package/dist/ui/assets/layouts-2A7QNF6Y-D94iKLLQ.js +1 -0
  56. package/dist/ui/assets/{less.contribution-Cthyflev.js → less.contribution-DtKQK3HF.js} +1 -1
  57. package/dist/ui/assets/{lua.contribution-894DRgPR.js → lua.contribution-DxFFDcBW.js} +1 -1
  58. package/dist/ui/assets/{markdown.contribution-d_TBTsNA.js → markdown.contribution-B7aCxle-.js} +1 -1
  59. package/dist/ui/assets/{monaco-PebRhLpo.js → monaco-BBFILBn6.js} +3 -3
  60. package/dist/ui/assets/{monaco-base-4S0M5BiY.js → monaco-base-CvsKN-Z-.js} +1 -1
  61. package/dist/ui/assets/{monaco-setup-7hp0Z9jn.js → monaco-setup-BeHgXlpG.js} +1 -1
  62. package/dist/ui/assets/{monaco.contribution-CKDvyGVE.js → monaco.contribution-B-c9k_-7.js} +2 -2
  63. package/dist/ui/assets/{monaco.contribution-BL_zgiu2.js → monaco.contribution-BBrbIBfd.js} +2 -2
  64. package/dist/ui/assets/{monaco.contribution-D61txvUB.js → monaco.contribution-Baa3Cv83.js} +2 -2
  65. package/dist/ui/assets/{monaco.contribution-DQjPk7Rx.js → monaco.contribution-DRZnYNPl.js} +2 -2
  66. package/dist/ui/assets/{pdf-CwJ6ZJmB.js → pdf-66KslaXf.js} +1 -1
  67. package/dist/ui/assets/{php.contribution-B8kn8QoR.js → php.contribution-vC08ovbS.js} +1 -1
  68. package/dist/ui/assets/pptx-CsdVR0xn.js +6 -0
  69. package/dist/ui/assets/{python-CHygH1eM.js → python-CevkXHVb.js} +1 -1
  70. package/dist/ui/assets/python.contribution-C6TUOYNZ.js +2 -0
  71. package/dist/ui/assets/{ruby.contribution-BQdPKFM2.js → ruby.contribution-BMZvE4z1.js} +1 -1
  72. package/dist/ui/assets/{rust.contribution-D7wKqShc.js → rust.contribution-DOWKjtrn.js} +1 -1
  73. package/dist/ui/assets/{scss.contribution-kSTLMoty.js → scss.contribution-BXLJWEv0.js} +1 -1
  74. package/dist/ui/assets/{shell.contribution-BQnT3BBJ.js → shell.contribution-gbqcscnj.js} +1 -1
  75. package/dist/ui/assets/{sql.contribution-C9z55nWb.js → sql.contribution-DO39C_-u.js} +1 -1
  76. package/dist/ui/assets/{swift.contribution-BZoW5ZZj.js → swift.contribution-D4Vjb2VA.js} +1 -1
  77. package/dist/ui/assets/{terminal-monaco-setup-DbSXji0d.js → terminal-monaco-setup-Bb4lnoOq.js} +1 -1
  78. package/dist/ui/assets/{tsMode-jPpKNC6E.js → tsMode-BAHQHAnm.js} +1 -1
  79. package/dist/ui/assets/{typescript-Djn_8BHF.js → typescript-15FhCrR2.js} +1 -1
  80. package/dist/ui/assets/typescript.contribution-D30L44Mw.js +2 -0
  81. package/dist/ui/assets/xlsx-CdLmEC42.js +4 -0
  82. package/dist/ui/assets/{xml-DmE4Aqa-.js → xml-BA1mrkDo.js} +1 -1
  83. package/dist/ui/assets/xml.contribution-CN9NFcUA.js +2 -0
  84. package/dist/ui/assets/{yaml-BjmK3C-c.js → yaml-CIqCv-ro.js} +1 -1
  85. package/dist/ui/assets/yaml.contribution-BsLB8vrs.js +2 -0
  86. package/dist/ui/index.html +15 -15
  87. package/package.json +16 -15
  88. package/dist/postinstall.d.ts +0 -2
  89. package/dist/postinstall.js +0 -40
  90. package/dist/ui/assets/chunk-ILCJ3WFD-uwKSpQd8.js +0 -4
  91. package/dist/ui/assets/chunk-USU6HTKB-hbaA8KyS.js +0 -1
  92. package/dist/ui/assets/dist-i9jV8CX8.js +0 -2
  93. package/dist/ui/assets/dist-jh33rACm.js +0 -195
  94. package/dist/ui/assets/docx-CNW-xqHT.js +0 -2
  95. package/dist/ui/assets/extract-UTAOG7LV-WxmEnrrc.js +0 -1
  96. package/dist/ui/assets/handlebars.contribution-DDSQBNPu.js +0 -2
  97. package/dist/ui/assets/html.contribution-DrClfj2L.js +0 -2
  98. package/dist/ui/assets/index-Caz-FRQy.css +0 -1
  99. package/dist/ui/assets/infer-Cc7ks6NX.js +0 -2
  100. package/dist/ui/assets/javascript.contribution-DMEzq0y4.js +0 -2
  101. package/dist/ui/assets/layouts-2A7QNF6Y-DR7-KboS.js +0 -1
  102. package/dist/ui/assets/pptx-Bmj2v9CT.js +0 -6
  103. package/dist/ui/assets/python.contribution-CmwCxp-v.js +0 -2
  104. package/dist/ui/assets/typescript.contribution-D4JmB9tz.js +0 -2
  105. package/dist/ui/assets/xlsx-BCnO44v8.js +0 -4
  106. package/dist/ui/assets/xml.contribution-BepqhMeK.js +0 -2
  107. package/dist/ui/assets/yaml.contribution-BwT8qEVt.js +0 -2
  108. package/postinstall.mjs +0 -8
@@ -4,10 +4,10 @@
4
4
  import { parentPort } from "worker_threads";
5
5
 
6
6
  // src/index-store/content-indexer.ts
7
- import { readFile as readFile3 } from "fs/promises";
8
- import { extname as extname3 } from "path";
7
+ import { readFile as readFile3, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
8
+ import { extname as extname3, join as join10 } from "path";
9
9
  import { nowIso as nowIso2 } from "@bendyline/gezel";
10
- import { projectLocalIndexDbFile } from "@bendyline/gezel/paths";
10
+ import { PROJECT_SHADOW_DIR_NAME as PROJECT_SHADOW_DIR_NAME2, projectLocalIndexDbFile } from "@bendyline/gezel/paths";
11
11
 
12
12
  // src/security/code-patterns.ts
13
13
  var EVIDENCE_CAP = 160;
@@ -560,7 +560,17 @@ var CODE_LANGS = {
560
560
  bash: "bash",
561
561
  zsh: "bash",
562
562
  lua: "lua",
563
- sql: "sql"
563
+ sql: "sql",
564
+ // Web-source formats. Without these a project's index.html fell through to
565
+ // kind 'other' — no review rubric, no security scan, excluded from every
566
+ // coverage denominator — while the status popover read "complete". Symbol
567
+ // extraction still gates on GRAMMAR_FILE separately, so a lang here without
568
+ // a wired grammar degrades to zero symbols, not an error.
569
+ html: "html",
570
+ htm: "html",
571
+ css: "css",
572
+ vue: "vue",
573
+ svelte: "svelte"
564
574
  };
565
575
  var MARKDOWN_EXTS = /* @__PURE__ */ new Set(["md", "mdx", "markdown"]);
566
576
  var DOC_EXTS = /* @__PURE__ */ new Set(["docx", "pptx", "xlsx", "pdf", "odt", "odp", "ods", "rtf"]);
@@ -612,14 +622,11 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
612
622
  "avi",
613
623
  "mkv",
614
624
  "webm",
615
- "mp3",
616
- "wav",
617
- "flac",
618
- "ogg",
619
625
  "db",
620
626
  "sqlite",
621
627
  "lock"
622
628
  ]);
629
+ var AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "flac", "ogg", "oga", "m4a", "aac", "opus"]);
623
630
  var MAX_INDEXABLE_BYTES = 512 * 1024;
624
631
  var DENSE_BLOB_MIN_BYTES = 100 * 1024;
625
632
  var DENSE_BLOB_BYTES_PER_LINE = 1e3;
@@ -645,6 +652,7 @@ function classifyFile(path, size) {
645
652
  }
646
653
  if (DOC_EXTS.has(ext)) return { lang: null, kind: "doc", modality: "doc", trivial: false };
647
654
  if (IMAGE_EXTS.has(ext)) return { lang: null, kind: "image", modality: "image", trivial: false };
655
+ if (AUDIO_EXTS.has(ext)) return { lang: null, kind: "audio", modality: "audio", trivial: false };
648
656
  if (CONFIG_EXTS.has(ext)) {
649
657
  return { lang: ext, kind: "config", modality: "text", trivial: size > MAX_INDEXABLE_BYTES };
650
658
  }
@@ -663,8 +671,8 @@ function triv(kind) {
663
671
 
664
672
  // src/index-store/docs.ts
665
673
  import { mkdir, readFile as readFile2, stat as stat2 } from "fs/promises";
666
- import { basename as basename2, dirname as dirname4, extname as extname2, join as join5, relative as relative2 } from "path";
667
- import { projectLocalFilesDir } from "@bendyline/gezel/paths";
674
+ import { basename as basename3, dirname as dirname5, extname as extname2, join as join6, relative as relative2 } from "path";
675
+ import { PROJECT_SHADOW_DIR_NAME } from "@bendyline/gezel/paths";
668
676
 
669
677
  // src/fs/atomic.ts
670
678
  import { randomUUID } from "crypto";
@@ -737,12 +745,101 @@ async function renameWithWindowsRetry(from, to) {
737
745
  }
738
746
  }
739
747
 
748
+ // src/fs/safe-paths.ts
749
+ import { realpath as realpath2 } from "fs/promises";
750
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join2, normalize, sep } from "path";
751
+ function pathEq(a, b) {
752
+ if (process.platform === "win32" || process.platform === "darwin") {
753
+ return a.toLowerCase() === b.toLowerCase();
754
+ }
755
+ return a === b;
756
+ }
757
+ function pathStartsWithDir(candidate, base) {
758
+ if (process.platform === "win32" || process.platform === "darwin") {
759
+ return candidate.toLowerCase().startsWith(base.toLowerCase() + sep.toLowerCase());
760
+ }
761
+ return candidate.startsWith(base + sep);
762
+ }
763
+ function safeJoin(base, relPath) {
764
+ if (typeof relPath !== "string") return null;
765
+ if (hasUnsafePortableSegment(relPath)) return null;
766
+ if (isAbsolute(relPath)) return null;
767
+ const normalizedBase = normalize(base);
768
+ const candidate = normalize(join2(normalizedBase, relPath));
769
+ if (pathEq(candidate, normalizedBase)) return candidate;
770
+ if (pathStartsWithDir(candidate, normalizedBase)) return candidate;
771
+ return null;
772
+ }
773
+ async function realpathContained(base, fullPath) {
774
+ const realBase = await realpathSafe(base);
775
+ if (!realBase) return false;
776
+ const realTarget = await realpathNearest(fullPath);
777
+ if (!realTarget) return false;
778
+ if (pathEq(realTarget, realBase)) return true;
779
+ return pathStartsWithDir(realTarget, realBase);
780
+ }
781
+ async function realpathSafe(p) {
782
+ try {
783
+ return normalize(await realpath2(p));
784
+ } catch {
785
+ return null;
786
+ }
787
+ }
788
+ async function realpathNearest(p) {
789
+ const direct = await realpathSafe(p);
790
+ if (direct) return direct;
791
+ const parent = dirname2(p);
792
+ if (parent === p) return null;
793
+ const realParent = await realpathNearest(parent);
794
+ if (!realParent) return null;
795
+ return normalize(join2(realParent, basename2(p)));
796
+ }
797
+ var RESERVED_WINDOWS_BASENAMES = /* @__PURE__ */ new Set([
798
+ "CON",
799
+ "PRN",
800
+ "AUX",
801
+ "NUL",
802
+ "COM1",
803
+ "COM2",
804
+ "COM3",
805
+ "COM4",
806
+ "COM5",
807
+ "COM6",
808
+ "COM7",
809
+ "COM8",
810
+ "COM9",
811
+ "LPT1",
812
+ "LPT2",
813
+ "LPT3",
814
+ "LPT4",
815
+ "LPT5",
816
+ "LPT6",
817
+ "LPT7",
818
+ "LPT8",
819
+ "LPT9"
820
+ ]);
821
+ function isReservedWindowsName(name) {
822
+ if (!name) return false;
823
+ const normalized = name.replace(/[ .]+$/g, "");
824
+ const base = (normalized.split(".")[0] ?? "").replace(/[ ]+$/g, "");
825
+ return RESERVED_WINDOWS_BASENAMES.has(base.toUpperCase());
826
+ }
827
+ function hasUnsafePortableSegment(relPath) {
828
+ if (relPath.includes("\0") || relPath.startsWith("\\\\")) return true;
829
+ const segments = relPath.split(/[\\/]+/);
830
+ return segments.some((segment) => {
831
+ if (!segment || segment === "." || segment === "..") return false;
832
+ if (segment.includes(":") || /[ .]$/.test(segment)) return true;
833
+ return isReservedWindowsName(segment);
834
+ });
835
+ }
836
+
740
837
  // src/index-store/sandbox-convert.ts
741
838
  import { existsSync as existsSync2 } from "fs";
742
- import { mkdtemp as mkdtemp2, readFile, realpath as realpath3, rm as rm3, stat, writeFile as writeFile3 } from "fs/promises";
839
+ import { mkdtemp as mkdtemp2, readFile, realpath as realpath4, rm as rm3, stat, writeFile as writeFile3 } from "fs/promises";
743
840
  import { createRequire } from "module";
744
841
  import { tmpdir as tmpdir2 } from "os";
745
- import { dirname as dirname3, join as join4 } from "path";
842
+ import { dirname as dirname4, join as join5 } from "path";
746
843
  import { fileURLToPath as fileURLToPath2 } from "url";
747
844
  import { createLogger } from "@bendyline/gezel";
748
845
 
@@ -911,11 +1008,11 @@ function sniffContainer(bytes, ext) {
911
1008
 
912
1009
  // src/sandbox/runner.ts
913
1010
  import { spawn as spawn2 } from "child_process";
914
- import { mkdtemp, realpath as realpath2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1011
+ import { mkdtemp, realpath as realpath3, rm as rm2, writeFile as writeFile2 } from "fs/promises";
915
1012
  import { tmpdir } from "os";
916
- import { isAbsolute, join as join2 } from "path";
1013
+ import { isAbsolute as isAbsolute2, join as join3 } from "path";
917
1014
  import { pathToFileURL } from "url";
918
- import { windowsDetachedSpawnOptions } from "@bendyline/gezel/native";
1015
+ import { windowsHeadlessSpawnOptions } from "@bendyline/gezel/native";
919
1016
 
920
1017
  // src/sandbox/macos.ts
921
1018
  function runUnderMacSandbox(command, args, ctx, opts = {}) {
@@ -966,11 +1063,11 @@ async function runInSandbox(opts) {
966
1063
  for (const line of splitLines(result.stderr)) opts.onStderr?.(line);
967
1064
  return result;
968
1065
  }
969
- const workdir = await realpath2(await mkdtemp(join2(tmpdir(), "gezel-task-")));
1066
+ const workdir = await realpath3(await mkdtemp(join3(tmpdir(), "gezel-task-")));
970
1067
  try {
971
- const cwd = await realpath2(opts.cwd).catch(() => opts.cwd);
972
- const entryCandidate = isAbsolute(opts.entry) ? opts.entry : join2(cwd, opts.entry);
973
- const entry = await realpath2(entryCandidate).catch(() => opts.entry);
1068
+ const cwd = await realpath3(opts.cwd).catch(() => opts.cwd);
1069
+ const entryCandidate = isAbsolute2(opts.entry) ? opts.entry : join3(cwd, opts.entry);
1070
+ const entry = await realpath3(entryCandidate).catch(() => opts.entry);
974
1071
  const readPaths = await expandPermissionPaths([
975
1072
  opts.cwd,
976
1073
  workdir,
@@ -979,7 +1076,7 @@ async function runInSandbox(opts) {
979
1076
  const writePaths = await expandPermissionPaths([opts.cwd, workdir]);
980
1077
  const netBlockArgs = [];
981
1078
  if (opts.denyNet) {
982
- const preloadPath = join2(workdir, "__net_block.mjs");
1079
+ const preloadPath = join3(workdir, "__net_block.mjs");
983
1080
  await writeFile2(preloadPath, NET_BLOCK_PRELOAD, "utf8");
984
1081
  netBlockArgs.push(`--import=${pathToFileURL(preloadPath).href}`);
985
1082
  }
@@ -1064,11 +1161,9 @@ async function readNodeHelp(nodeBin) {
1064
1161
  const child = spawn2(nodeBin, ["--help"], {
1065
1162
  stdio: ["ignore", "pipe", "pipe"],
1066
1163
  env: sandboxEnv(process.env),
1067
- // Bundled Node is a console-subsystem executable and the Session 0
1068
- // machine service can allocate no console so even this capability
1069
- // probe starts with DETACHED_PROCESS. `windowsHide`
1070
- // (CREATE_NO_WINDOW) does not do that; it still allocates.
1071
- ...windowsDetachedSpawnOptions()
1164
+ // The capability probe is owned and awaited. Hide its Windows console
1165
+ // rather than giving the short-lived process a detached one.
1166
+ ...windowsHeadlessSpawnOptions()
1072
1167
  });
1073
1168
  let stdout = "";
1074
1169
  let stderr = "";
@@ -1090,16 +1185,11 @@ async function runSandboxChild(command, args, opts) {
1090
1185
  const child = spawn2(command, args, {
1091
1186
  cwd: opts.cwd,
1092
1187
  stdio: opts.rpcChannel ? ["pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
1093
- // Keep sandbox children out of the Vitest/service process
1094
- // group. Workspace commands already do this; doing the same
1095
- // here prevents an unrelated process-group kill from closing a
1096
- // short-lived derive/script child with a null exit code.
1097
- // On Windows the same option is DETACHED_PROCESS, which also keeps a
1098
- // console-subsystem child from asking the Session 0 service for a
1099
- // console it cannot allocate — `windowsHide` (CREATE_NO_WINDOW) does
1100
- // not, because it still allocates one. `taskkill /T` is unaffected:
1101
- // detaching changes console and process group, not the recorded parent.
1102
- detached: true,
1188
+ // A separate process group is useful on POSIX for group termination.
1189
+ // Windows tree cleanup follows parent PIDs instead, so detaching there
1190
+ // only creates a new console window; keep it owned and hidden.
1191
+ detached: process.platform !== "win32",
1192
+ ...windowsHeadlessSpawnOptions(),
1103
1193
  // Allowlist-scrub the env — inheriting everything leaked tokens
1104
1194
  // (GEZEL_TOKEN, OPENAI_API_KEY, GITHUB_PERSONAL_ACCESS_TOKEN,
1105
1195
  // etc.) into the sandboxed script, letting it `fetch` them
@@ -1210,7 +1300,7 @@ async function expandPermissionPaths(paths) {
1210
1300
  const expanded = [];
1211
1301
  const seen = /* @__PURE__ */ new Set();
1212
1302
  for (const p of paths) {
1213
- for (const candidate of [p, await realpath2(p).catch(() => p)]) {
1303
+ for (const candidate of [p, await realpath3(p).catch(() => p)]) {
1214
1304
  if (seen.has(candidate)) continue;
1215
1305
  seen.add(candidate);
1216
1306
  expanded.push(candidate);
@@ -1448,7 +1538,7 @@ syncBuiltinESMExports();
1448
1538
 
1449
1539
  // src/utils/service-worker-entry.ts
1450
1540
  import { existsSync } from "fs";
1451
- import { dirname as dirname2, join as join3, resolve } from "path";
1541
+ import { dirname as dirname3, join as join4, resolve } from "path";
1452
1542
  import { fileURLToPath } from "url";
1453
1543
  var SERVICE_WORKER_ENTRIES = {
1454
1544
  embeddings: { built: "memory/embed-worker.js" },
@@ -1461,10 +1551,10 @@ var SERVICE_WORKER_ENTRIES = {
1461
1551
  };
1462
1552
  var MAX_ANCESTOR_DEPTH = 8;
1463
1553
  function findServiceBundleResource(moduleUrl, relativePaths) {
1464
- let dir = dirname2(fileURLToPath(moduleUrl));
1554
+ let dir = dirname3(fileURLToPath(moduleUrl));
1465
1555
  const visited = /* @__PURE__ */ new Set();
1466
1556
  for (let depth = 0; depth < MAX_ANCESTOR_DEPTH; depth++) {
1467
- for (const root of [dir, join3(dir, "dist")]) {
1557
+ for (const root of [dir, join4(dir, "dist")]) {
1468
1558
  for (const relativePath of relativePaths) {
1469
1559
  const candidate = resolve(root, relativePath);
1470
1560
  if (visited.has(candidate)) continue;
@@ -1472,7 +1562,7 @@ function findServiceBundleResource(moduleUrl, relativePaths) {
1472
1562
  if (existsSync(candidate)) return candidate;
1473
1563
  }
1474
1564
  }
1475
- const parent = dirname2(dir);
1565
+ const parent = dirname3(dir);
1476
1566
  if (parent === dir) break;
1477
1567
  dir = parent;
1478
1568
  }
@@ -1494,8 +1584,8 @@ function findRepoRoot(start) {
1494
1584
  let dir = start;
1495
1585
  let highest = null;
1496
1586
  for (; ; ) {
1497
- if (existsSync2(join4(dir, "node_modules"))) highest = dir;
1498
- const parent = dirname3(dir);
1587
+ if (existsSync2(join5(dir, "node_modules"))) highest = dir;
1588
+ const parent = dirname4(dir);
1499
1589
  if (parent === dir) break;
1500
1590
  dir = parent;
1501
1591
  }
@@ -1505,12 +1595,12 @@ var readRootsCache;
1505
1595
  function sandboxReadRoots() {
1506
1596
  if (readRootsCache !== void 0) return readRootsCache;
1507
1597
  const roots = /* @__PURE__ */ new Set();
1508
- const repoRoot = findRepoRoot(dirname3(fileURLToPath2(import.meta.url)));
1598
+ const repoRoot = findRepoRoot(dirname4(fileURLToPath2(import.meta.url)));
1509
1599
  if (repoRoot) roots.add(repoRoot);
1510
1600
  try {
1511
1601
  const require2 = createRequire(import.meta.url);
1512
1602
  const resolved = require2.resolve("@bendyline/squisq-formats");
1513
- const linkedRoot = findRepoRoot(dirname3(resolved));
1603
+ const linkedRoot = findRepoRoot(dirname4(resolved));
1514
1604
  if (linkedRoot) roots.add(linkedRoot);
1515
1605
  } catch {
1516
1606
  readRootsCache = null;
@@ -1557,10 +1647,10 @@ async function convertInSandbox(absPath, ext) {
1557
1647
  log.warn("sandbox unavailable (worker or node_modules root unresolved); skipping conversion");
1558
1648
  return { markdown: null };
1559
1649
  }
1560
- const scratch = await realpath3(await mkdtemp2(join4(tmpdir2(), "gezel-convert-")));
1650
+ const scratch = await realpath4(await mkdtemp2(join5(tmpdir2(), "gezel-convert-")));
1561
1651
  try {
1562
1652
  const inputName = `input.${ext}`;
1563
- await writeFile3(join4(scratch, inputName), bytes);
1653
+ await writeFile3(join5(scratch, inputName), bytes);
1564
1654
  const res = await runInSandbox({
1565
1655
  entry: worker.path,
1566
1656
  cwd: scratch,
@@ -1585,7 +1675,7 @@ async function convertInSandbox(absPath, ext) {
1585
1675
  );
1586
1676
  return { markdown: null };
1587
1677
  }
1588
- const md = await readFile(join4(scratch, "output.md"), "utf8").catch(() => null);
1678
+ const md = await readFile(join5(scratch, "output.md"), "utf8").catch(() => null);
1589
1679
  return { markdown: md };
1590
1680
  } finally {
1591
1681
  await rm3(scratch, { recursive: true, force: true });
@@ -1603,16 +1693,19 @@ async function convertDocToMarkdown(absPath) {
1603
1693
  if (!isConvertibleDoc(ext)) return { markdown: null };
1604
1694
  return convertInSandbox(absPath, ext);
1605
1695
  }
1606
- function filesPaths(filesRoot, relPath, relativeRoot) {
1607
- const base = basename2(relPath);
1608
- const nameNoExt = base.replace(/\.[^.]+$/, "");
1609
- const parent = dirname4(relPath);
1610
- const dir = join5(filesRoot, parent === "." ? "" : parent, `${nameNoExt}_files`);
1611
- const mdPath = join5(dir, `${nameNoExt}.md`);
1612
- return { dir, mdPath, mdRel: relative2(relativeRoot, mdPath).replaceAll("\\", "/") };
1613
- }
1614
- function docFilesPaths(workspaceDir, relPath) {
1615
- return filesPaths(projectLocalFilesDir(workspaceDir), relPath, workspaceDir);
1696
+ function shadowDocFilesPaths(artifactsDir, relPath) {
1697
+ const base = basename3(relPath);
1698
+ const nameNoExt = base.replace(/\.[^.]+$/, "") || base;
1699
+ const parent = dirname5(relPath);
1700
+ const relMd = join6(parent === "." ? "" : parent, `${base}_files`, `${nameNoExt}.md`);
1701
+ const shadowRoot = join6(artifactsDir, PROJECT_SHADOW_DIR_NAME);
1702
+ const mdPath = safeJoin(shadowRoot, relMd);
1703
+ if (!mdPath) return null;
1704
+ return {
1705
+ dir: dirname5(mdPath),
1706
+ mdPath,
1707
+ mdRel: relative2(artifactsDir, mdPath).replaceAll("\\", "/")
1708
+ };
1616
1709
  }
1617
1710
  async function writeConvertedMarkdownAt(paths, markdown) {
1618
1711
  await mkdir(paths.dir, { recursive: true });
@@ -1637,6 +1730,15 @@ async function ensureConvertedMarkdownSidecar(sourcePath, paths) {
1637
1730
  }
1638
1731
  return { ...converted, paths };
1639
1732
  }
1733
+ async function ensureShadowDocSidecar(sourceAbs, artifactsDir, relPath) {
1734
+ const paths = shadowDocFilesPaths(artifactsDir, relPath);
1735
+ if (!paths) return null;
1736
+ const shadowRoot = join6(artifactsDir, PROJECT_SHADOW_DIR_NAME);
1737
+ await mkdir(shadowRoot, { recursive: true }).catch(() => {
1738
+ });
1739
+ if (!await realpathContained(shadowRoot, paths.mdPath)) return null;
1740
+ return ensureConvertedMarkdownSidecar(sourceAbs, paths);
1741
+ }
1640
1742
  function chunkMarkdown(md) {
1641
1743
  const lines = md.split(/\r?\n/);
1642
1744
  const headIdx = [];
@@ -1684,8 +1786,8 @@ function parseFrontmatter(content) {
1684
1786
 
1685
1787
  // src/index-store/gitignore.ts
1686
1788
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
1687
- import { join as join6 } from "path";
1688
- import { projectLocalFilesDir as projectLocalFilesDir2, projectLocalIndexDir } from "@bendyline/gezel/paths";
1789
+ import { join as join7 } from "path";
1790
+ import { projectLocalIndexDir } from "@bendyline/gezel/paths";
1689
1791
 
1690
1792
  // src/index-store/hash.ts
1691
1793
  import { createHash } from "crypto";
@@ -1994,7 +2096,7 @@ import { createLogger as createLogger3 } from "@bendyline/gezel";
1994
2096
 
1995
2097
  // src/transformers-cache.ts
1996
2098
  import { mkdir as mkdir3 } from "fs/promises";
1997
- import { join as join7 } from "path";
2099
+ import { join as join8 } from "path";
1998
2100
  import { createLogger as createLogger2 } from "@bendyline/gezel";
1999
2101
  var log2 = createLogger2("service");
2000
2102
 
@@ -2103,6 +2205,10 @@ CREATE TABLE IF NOT EXISTS summaries (
2103
2205
  summary_md TEXT,
2104
2206
  tags TEXT,
2105
2207
  model TEXT,
2208
+ provider TEXT,
2209
+ gezel_id TEXT,
2210
+ gezel_name TEXT,
2211
+ app_version TEXT,
2106
2212
  created_at TEXT
2107
2213
  );
2108
2214
 
@@ -2219,6 +2325,10 @@ CREATE TABLE IF NOT EXISTS symbol_summaries (
2219
2325
  symbol_name TEXT NOT NULL,
2220
2326
  summary TEXT NOT NULL,
2221
2327
  model TEXT,
2328
+ provider TEXT,
2329
+ gezel_id TEXT,
2330
+ gezel_name TEXT,
2331
+ app_version TEXT,
2222
2332
  created_at TEXT,
2223
2333
  PRIMARY KEY (collection_id, file_path, file_hash, symbol_name)
2224
2334
  );
@@ -2235,6 +2345,10 @@ CREATE TABLE IF NOT EXISTS area_summaries (
2235
2345
  input_hash TEXT NOT NULL,
2236
2346
  summary_md TEXT NOT NULL,
2237
2347
  model TEXT,
2348
+ provider TEXT,
2349
+ gezel_id TEXT,
2350
+ gezel_name TEXT,
2351
+ app_version TEXT,
2238
2352
  created_at TEXT,
2239
2353
  PRIMARY KEY (collection_id, area_path)
2240
2354
  );
@@ -2258,12 +2372,30 @@ CREATE TABLE IF NOT EXISTS file_reviews (
2258
2372
  health INTEGER,
2259
2373
  health_reason TEXT,
2260
2374
  model TEXT,
2375
+ provider TEXT,
2376
+ gezel_id TEXT,
2377
+ gezel_name TEXT,
2378
+ app_version TEXT,
2261
2379
  reviewed_at TEXT,
2262
2380
  attempt_rubric_hash TEXT,
2263
2381
  attempts INTEGER DEFAULT 0,
2264
2382
  last_attempt_at TEXT
2265
2383
  );
2266
2384
 
2385
+ -- AI-shadow gate (images described by the vision model, audio transcribed by
2386
+ -- STT into artifacts/shadow markdown). Same content-addressed discipline as
2387
+ -- enrichments: 'ok' rows survive renames/reverts; failures retry up to a cap
2388
+ -- per hash. The sidecar file is the artifact; this row is only the work gate.
2389
+ CREATE TABLE IF NOT EXISTS shadow_state (
2390
+ content_hash TEXT PRIMARY KEY,
2391
+ collection_id TEXT,
2392
+ file_path TEXT,
2393
+ state TEXT,
2394
+ attempts INTEGER DEFAULT 0,
2395
+ model TEXT,
2396
+ updated_at TEXT
2397
+ );
2398
+
2267
2399
  -- Mirror of the append-only history JSONL (the audit log). kind/project/gezel/at
2268
2400
  -- are real columns so HistoryFilter fields stay SQL-filterable; only the q text
2269
2401
  -- goes through fts_history. INSERT OR IGNORE on the event id makes JSONL
@@ -2292,7 +2424,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS fts_metadata
2292
2424
  CREATE VIRTUAL TABLE IF NOT EXISTS fts_history
2293
2425
  USING fts5(body, event_id UNINDEXED, collection_id UNINDEXED);
2294
2426
  `;
2295
- var SCHEMA_VERSION = 9;
2427
+ var SCHEMA_VERSION = 10;
2296
2428
  function addColumnIfMissing(db, table, column, decl) {
2297
2429
  try {
2298
2430
  const cols = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -2316,6 +2448,12 @@ function applySchema(db) {
2316
2448
  WHERE fingerprint IS NULL OR fingerprint = ''`);
2317
2449
  } catch {
2318
2450
  }
2451
+ for (const table of ["summaries", "symbol_summaries", "area_summaries", "file_reviews"]) {
2452
+ addColumnIfMissing(db, table, "provider", "TEXT");
2453
+ addColumnIfMissing(db, table, "gezel_id", "TEXT");
2454
+ addColumnIfMissing(db, table, "gezel_name", "TEXT");
2455
+ addColumnIfMissing(db, table, "app_version", "TEXT");
2456
+ }
2319
2457
  let fts = true;
2320
2458
  try {
2321
2459
  db.exec(FTS_DDL);
@@ -2343,7 +2481,7 @@ function applySchema(db) {
2343
2481
  // src/index-store/sqlite-driver.ts
2344
2482
  import { mkdir as mkdir4, rename as rename2 } from "fs/promises";
2345
2483
  import { createRequire as createRequire2 } from "module";
2346
- import { dirname as dirname5 } from "path";
2484
+ import { dirname as dirname6 } from "path";
2347
2485
  import { createLogger as createLogger4 } from "@bendyline/gezel";
2348
2486
  var nodeRequire = createRequire2(import.meta.url);
2349
2487
  var log4 = createLogger4("sqlite");
@@ -2366,7 +2504,7 @@ async function openIndexDatabase(dbPath) {
2366
2504
  }
2367
2505
  if (dbPath !== ":memory:") {
2368
2506
  try {
2369
- await mkdir4(dirname5(dbPath), { recursive: true });
2507
+ await mkdir4(dirname6(dbPath), { recursive: true });
2370
2508
  } catch (error) {
2371
2509
  if (isUnavailableIndexError(error)) return null;
2372
2510
  throw error;
@@ -2375,6 +2513,7 @@ async function openIndexDatabase(dbPath) {
2375
2513
  let db;
2376
2514
  try {
2377
2515
  db = new DatabaseSync(dbPath, { allowExtension: true });
2516
+ setBusyTimeout(db);
2378
2517
  assertReadableHeader(db);
2379
2518
  } catch (error) {
2380
2519
  if (dbPath === ":memory:" || !isCorruptDatabaseError(error)) {
@@ -2391,6 +2530,7 @@ async function openIndexDatabase(dbPath) {
2391
2530
  await quarantineCorruptDatabase(dbPath);
2392
2531
  log4.warn(`[sqlite] quarantined corrupt rebuildable index ${dbPath}`);
2393
2532
  db = new DatabaseSync(dbPath, { allowExtension: true });
2533
+ setBusyTimeout(db);
2394
2534
  }
2395
2535
  if (!db) throw new Error("failed to open sqlite index");
2396
2536
  let vecAvailable = false;
@@ -2439,6 +2579,12 @@ async function openIndexDatabase(dbPath) {
2439
2579
  function assertReadableHeader(db) {
2440
2580
  db.prepare("PRAGMA schema_version").get();
2441
2581
  }
2582
+ function setBusyTimeout(db) {
2583
+ try {
2584
+ db.exec("PRAGMA busy_timeout=5000;");
2585
+ } catch {
2586
+ }
2587
+ }
2442
2588
  function isCorruptDatabaseError(error) {
2443
2589
  const message = error instanceof Error ? error.message : String(error);
2444
2590
  return /SQLITE_CORRUPT|SQLITE_NOTADB|database disk image is malformed|file is not a database/i.test(
@@ -2447,9 +2593,7 @@ function isCorruptDatabaseError(error) {
2447
2593
  }
2448
2594
  function isUnavailableIndexError(error) {
2449
2595
  const message = error instanceof Error ? error.message : String(error);
2450
- return /SQLITE_READONLY|SQLITE_CANTOPEN|SQLITE_BUSY|SQLITE_LOCKED|EACCES|EPERM|read-only/i.test(
2451
- message
2452
- );
2596
+ return /SQLITE_READONLY|SQLITE_CANTOPEN|EACCES|EPERM|read-only/i.test(message);
2453
2597
  }
2454
2598
  async function quarantineCorruptDatabase(dbPath) {
2455
2599
  const suffix = `.corrupt-${Date.now()}`;
@@ -2728,7 +2872,9 @@ var IndexStore = class _IndexStore {
2728
2872
  `SELECT f.* FROM files f
2729
2873
  LEFT JOIN enrichments e ON e.content_hash = f.hash
2730
2874
  WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2731
- AND f.modality IN ('code','text','doc')
2875
+ AND (f.modality IN ('code','text','doc')
2876
+ OR EXISTS (SELECT 1 FROM shadow_state s
2877
+ WHERE s.content_hash = f.hash AND s.state = 'ok'))
2732
2878
  AND (e.content_hash IS NULL
2733
2879
  OR (e.embedded_at IS NULL AND COALESCE(e.attempts, 0) < ${MAX_ENRICH_ATTEMPTS}))
2734
2880
  ORDER BY f.path LIMIT ?`
@@ -2742,12 +2888,60 @@ var IndexStore = class _IndexStore {
2742
2888
  `SELECT COUNT(*) AS n FROM files f
2743
2889
  LEFT JOIN enrichments e ON e.content_hash = f.hash
2744
2890
  WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2745
- AND f.modality IN ('code','text','doc')
2891
+ AND (f.modality IN ('code','text','doc')
2892
+ OR EXISTS (SELECT 1 FROM shadow_state s
2893
+ WHERE s.content_hash = f.hash AND s.state = 'ok'))
2746
2894
  AND (e.content_hash IS NULL
2747
2895
  OR (e.embedded_at IS NULL AND COALESCE(e.attempts, 0) < ${MAX_ENRICH_ATTEMPTS}))`
2748
2896
  ).get(this.collectionId);
2749
2897
  return row?.n ?? 0;
2750
2898
  }
2899
+ /**
2900
+ * Image/audio files whose AI shadow (description / transcript) is missing
2901
+ * or stale for the current content hash. Same capped-retry discipline as
2902
+ * {@link filesNeedingEnrichment}; an 'ok' row is the terminal success.
2903
+ */
2904
+ filesNeedingAiShadow(limit = 5, maxAttempts = MAX_ENRICH_ATTEMPTS) {
2905
+ return this.db.prepare(
2906
+ `SELECT f.* FROM files f
2907
+ LEFT JOIN shadow_state s ON s.content_hash = f.hash
2908
+ WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2909
+ AND f.modality IN ('image','audio')
2910
+ AND (s.content_hash IS NULL
2911
+ OR (s.state <> 'ok' AND COALESCE(s.attempts, 0) < ?))
2912
+ ORDER BY f.path LIMIT ?`
2913
+ ).all(this.collectionId, maxAttempts, limit).map(rowToFile);
2914
+ }
2915
+ markAiShadowOk(contentHash, filePath, model) {
2916
+ this.db.prepare(
2917
+ `INSERT OR REPLACE INTO shadow_state (content_hash, collection_id, file_path, state, attempts, model, updated_at)
2918
+ VALUES (?, ?, ?, 'ok', 0, ?, ?)`
2919
+ ).run(contentHash, this.collectionId, filePath, model ?? null, nowIso());
2920
+ }
2921
+ markAiShadowAttempt(contentHash, filePath) {
2922
+ this.db.prepare(
2923
+ `INSERT INTO shadow_state (content_hash, collection_id, file_path, state, attempts, updated_at)
2924
+ VALUES (?, ?, ?, 'failed', 1, ?)
2925
+ ON CONFLICT(content_hash) DO UPDATE SET
2926
+ state='failed', attempts=COALESCE(shadow_state.attempts, 0) + 1,
2927
+ updated_at=excluded.updated_at`
2928
+ ).run(contentHash, this.collectionId, filePath, nowIso());
2929
+ return Number(
2930
+ this.db.prepare("SELECT attempts FROM shadow_state WHERE content_hash = ?").get(contentHash)?.attempts ?? 1
2931
+ );
2932
+ }
2933
+ /**
2934
+ * Terminally skip a media hash the AI-shadow tier can never process — a
2935
+ * format with no raster decoder in the vision stack (SVG is vector, ICO is
2936
+ * multi-frame). Jumps straight to the attempt cap under a distinct state so
2937
+ * the row reads as "wrong tool", not "engine flaked three times".
2938
+ */
2939
+ markAiShadowUnsupported(contentHash, filePath) {
2940
+ this.db.prepare(
2941
+ `INSERT OR REPLACE INTO shadow_state (content_hash, collection_id, file_path, state, attempts, updated_at)
2942
+ VALUES (?, ?, ?, 'unsupported', ${MAX_ENRICH_ATTEMPTS}, ?)`
2943
+ ).run(contentHash, this.collectionId, filePath, nowIso());
2944
+ }
2751
2945
  /**
2752
2946
  * Enrichment coverage counts. `summarized` counts real summaries (join on
2753
2947
  * the summaries table), deliberately NOT the enrichments gate — the gate also
@@ -2766,15 +2960,39 @@ var IndexStore = class _IndexStore {
2766
2960
  `SELECT COUNT(*) AS n FROM files f JOIN enrichments e ON e.content_hash = f.hash
2767
2961
  WHERE e.embedded_at IS NOT NULL AND ${ELIGIBLE}`
2768
2962
  );
2963
+ const skipped = count(
2964
+ `SELECT COUNT(*) AS n FROM files f JOIN enrichments e ON e.content_hash = f.hash
2965
+ WHERE e.embedded_at IS NULL AND COALESCE(e.attempts, 0) >= ${MAX_ENRICH_ATTEMPTS}
2966
+ AND ${ELIGIBLE}`
2967
+ );
2769
2968
  const embedModel = this.getMeta("embed_model");
2770
2969
  return {
2771
2970
  eligible,
2772
2971
  summarized,
2773
2972
  embedded,
2774
2973
  pending: this.countNeedingEnrichment(),
2974
+ skipped,
2975
+ shadowsPending: this.countNeedingAiShadow(),
2775
2976
  ...embedModel ? { embedModel } : {}
2776
2977
  };
2777
2978
  }
2979
+ /**
2980
+ * COUNT twin of `filesNeedingAiShadow` — media files still awaiting an AI
2981
+ * description, attempt-capped ones excluded. The drive works this tier
2982
+ * before summaries, so status must surface it or a fresh scan looks stuck.
2983
+ */
2984
+ countNeedingAiShadow(maxAttempts = MAX_ENRICH_ATTEMPTS) {
2985
+ return Number(
2986
+ this.db.prepare(
2987
+ `SELECT COUNT(*) AS n FROM files f
2988
+ LEFT JOIN shadow_state s ON s.content_hash = f.hash
2989
+ WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2990
+ AND f.modality IN ('image','audio')
2991
+ AND (s.content_hash IS NULL
2992
+ OR (s.state <> 'ok' AND COALESCE(s.attempts, 0) < ?))`
2993
+ ).get(this.collectionId, maxAttempts)?.n ?? 0
2994
+ );
2995
+ }
2778
2996
  /** Mark a content hash as enriched (vectors built) so it isn't reprocessed. */
2779
2997
  markEnriched(contentHash) {
2780
2998
  this.db.prepare("INSERT OR REPLACE INTO enrichments (content_hash, embedded_at) VALUES (?, ?)").run(contentHash, nowIso());
@@ -2790,6 +3008,23 @@ var IndexStore = class _IndexStore {
2790
3008
  `INSERT INTO enrichments (content_hash, embedded_at, attempts) VALUES (?, NULL, 1)
2791
3009
  ON CONFLICT(content_hash) DO UPDATE SET attempts = COALESCE(enrichments.attempts, 0) + 1`
2792
3010
  ).run(contentHash);
3011
+ return Number(
3012
+ this.db.prepare("SELECT attempts FROM enrichments WHERE content_hash = ?").get(contentHash)?.attempts ?? 1
3013
+ );
3014
+ }
3015
+ /**
3016
+ * Jump a hash straight to the attempt cap — for deterministic failures
3017
+ * (a provider policy-blocking the file's content) where every retry would
3018
+ * fail identically. The file drops off the work list and counts as
3019
+ * `skipped` in {@link enrichmentCounts} until its content changes.
3020
+ */
3021
+ markEnrichSkipped(contentHash) {
3022
+ this.db.prepare(
3023
+ `INSERT INTO enrichments (content_hash, embedded_at, attempts)
3024
+ VALUES (?, NULL, ${MAX_ENRICH_ATTEMPTS})
3025
+ ON CONFLICT(content_hash) DO UPDATE
3026
+ SET attempts = MAX(COALESCE(enrichments.attempts, 0), ${MAX_ENRICH_ATTEMPTS})`
3027
+ ).run(contentHash);
2793
3028
  }
2794
3029
  /**
2795
3030
  * Keyword search over enrichment summaries. fts_summaries rows are
@@ -2942,11 +3177,15 @@ var IndexStore = class _IndexStore {
2942
3177
  upsertSummary(rec) {
2943
3178
  this.db.transaction(() => {
2944
3179
  this.db.prepare(
2945
- `INSERT INTO summaries (content_hash, collection_id, file_path, summary_md, tags, model, created_at)
2946
- VALUES (?, ?, ?, ?, ?, ?, ?)
3180
+ `INSERT INTO summaries (content_hash, collection_id, file_path, summary_md, tags, model,
3181
+ provider, gezel_id, gezel_name, app_version, created_at)
3182
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2947
3183
  ON CONFLICT(content_hash) DO UPDATE SET
2948
3184
  file_path=excluded.file_path, summary_md=excluded.summary_md,
2949
- tags=excluded.tags, model=excluded.model, created_at=excluded.created_at`
3185
+ tags=excluded.tags, model=excluded.model,
3186
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3187
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3188
+ created_at=excluded.created_at`
2950
3189
  ).run(
2951
3190
  rec.contentHash,
2952
3191
  this.collectionId,
@@ -2954,6 +3193,10 @@ var IndexStore = class _IndexStore {
2954
3193
  rec.summaryMd,
2955
3194
  rec.tags ?? null,
2956
3195
  rec.model,
3196
+ rec.provenance?.provider ?? null,
3197
+ rec.provenance?.gezelId ?? null,
3198
+ rec.provenance?.gezelName ?? null,
3199
+ rec.provenance?.appVersion ?? null,
2957
3200
  nowIso()
2958
3201
  );
2959
3202
  if (this.caps.fts) {
@@ -2982,16 +3225,29 @@ var IndexStore = class _IndexStore {
2982
3225
  * for any other (stale) hash of the same file. Keyed by symbol name only —
2983
3226
  * overloads/same-name symbols share a line (accepted v1 simplification).
2984
3227
  */
2985
- putSymbolSummaries(filePath, fileHash, entries, model) {
3228
+ putSymbolSummaries(filePath, fileHash, entries, model, provenance) {
2986
3229
  this.db.transaction(() => {
2987
3230
  this.db.prepare("DELETE FROM symbol_summaries WHERE collection_id = ? AND file_path = ?").run(this.collectionId, filePath);
2988
3231
  const ins = this.db.prepare(
2989
- `INSERT OR REPLACE INTO symbol_summaries (collection_id, file_path, file_hash, symbol_name, summary, model, created_at)
2990
- VALUES (?, ?, ?, ?, ?, ?, ?)`
3232
+ `INSERT OR REPLACE INTO symbol_summaries (collection_id, file_path, file_hash, symbol_name, summary, model,
3233
+ provider, gezel_id, gezel_name, app_version, created_at)
3234
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2991
3235
  );
2992
3236
  const at = nowIso();
2993
3237
  for (const e of entries) {
2994
- ins.run(this.collectionId, filePath, fileHash, e.name, e.summary, model ?? null, at);
3238
+ ins.run(
3239
+ this.collectionId,
3240
+ filePath,
3241
+ fileHash,
3242
+ e.name,
3243
+ e.summary,
3244
+ model ?? null,
3245
+ provenance?.provider ?? null,
3246
+ provenance?.gezelId ?? null,
3247
+ provenance?.gezelName ?? null,
3248
+ provenance?.appVersion ?? null,
3249
+ at
3250
+ );
2995
3251
  }
2996
3252
  });
2997
3253
  }
@@ -3009,14 +3265,17 @@ var IndexStore = class _IndexStore {
3009
3265
  upsertFileReview(rec) {
3010
3266
  this.db.prepare(
3011
3267
  `INSERT INTO file_reviews (content_hash, collection_id, file_path, rubric_hash, notes_md,
3012
- issues, health, health_reason, model, reviewed_at,
3268
+ issues, health, health_reason, model,
3269
+ provider, gezel_id, gezel_name, app_version, reviewed_at,
3013
3270
  attempt_rubric_hash, attempts, last_attempt_at)
3014
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, NULL)
3271
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, NULL)
3015
3272
  ON CONFLICT(content_hash) DO UPDATE SET
3016
3273
  collection_id=excluded.collection_id, file_path=excluded.file_path,
3017
3274
  rubric_hash=excluded.rubric_hash, notes_md=excluded.notes_md,
3018
3275
  issues=excluded.issues, health=excluded.health,
3019
3276
  health_reason=excluded.health_reason, model=excluded.model,
3277
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3278
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3020
3279
  reviewed_at=excluded.reviewed_at,
3021
3280
  attempt_rubric_hash=NULL, attempts=0, last_attempt_at=NULL`
3022
3281
  ).run(
@@ -3029,9 +3288,32 @@ var IndexStore = class _IndexStore {
3029
3288
  rec.health,
3030
3289
  rec.healthReason,
3031
3290
  rec.model,
3291
+ rec.provenance?.provider ?? null,
3292
+ rec.provenance?.gezelId ?? null,
3293
+ rec.provenance?.gezelName ?? null,
3294
+ rec.provenance?.appVersion ?? null,
3032
3295
  nowIso()
3033
3296
  );
3034
3297
  }
3298
+ /**
3299
+ * Terminal skip for content that CANNOT be reviewed under its current hash
3300
+ * — a doc whose conversion was blocked/failed, an empty file, a source
3301
+ * deleted between scan and review. Burns the whole attempt budget in one
3302
+ * write (success fields untouched) so `filesNeedingReview` stops selecting
3303
+ * the file; without this, one blocked doc permanently steals a batch slot
3304
+ * from its kind on every sweep. A content change (new hash) or a rubric
3305
+ * edit re-admits it exactly like any capped-out attempt.
3306
+ */
3307
+ markReviewUnavailable(contentHash, filePath, rubricHash, maxAttempts) {
3308
+ this.db.prepare(
3309
+ `INSERT INTO file_reviews (content_hash, collection_id, file_path, attempt_rubric_hash, attempts, last_attempt_at)
3310
+ VALUES (?, ?, ?, ?, ?, ?)
3311
+ ON CONFLICT(content_hash) DO UPDATE SET
3312
+ attempts = excluded.attempts,
3313
+ attempt_rubric_hash = excluded.attempt_rubric_hash,
3314
+ last_attempt_at = excluded.last_attempt_at`
3315
+ ).run(contentHash, this.collectionId, filePath, rubricHash, maxAttempts, nowIso());
3316
+ }
3035
3317
  /**
3036
3318
  * Record a non-empty-but-unparseable review reply WITHOUT touching any
3037
3319
  * success field — a stale-rubric review keeps serving until a new one
@@ -3052,7 +3334,8 @@ var IndexStore = class _IndexStore {
3052
3334
  /** The successful review for a content hash (attempt-only rows excluded). */
3053
3335
  getFileReview(contentHash) {
3054
3336
  const row = this.db.prepare(
3055
- `SELECT notes_md, issues, health, health_reason, rubric_hash, model, reviewed_at
3337
+ `SELECT notes_md, issues, health, health_reason, rubric_hash, model,
3338
+ provider, gezel_id, gezel_name, app_version, reviewed_at
3056
3339
  FROM file_reviews
3057
3340
  WHERE content_hash = ? AND notes_md IS NOT NULL AND health IS NOT NULL`
3058
3341
  ).get(contentHash);
@@ -3064,9 +3347,31 @@ var IndexStore = class _IndexStore {
3064
3347
  healthReason: row.health_reason ?? "",
3065
3348
  rubricHash: row.rubric_hash ?? "",
3066
3349
  model: row.model,
3350
+ provider: row.provider,
3351
+ gezelId: row.gezel_id,
3352
+ gezelName: row.gezel_name,
3353
+ appVersion: row.app_version,
3067
3354
  reviewedAt: row.reviewed_at
3068
3355
  };
3069
3356
  }
3357
+ /**
3358
+ * Current file-hash review observations, including clean reviews. The
3359
+ * durable Boekwachter registry consumes these so a later file edit cannot
3360
+ * erase identity/lifecycle merely by invalidating `file_reviews`.
3361
+ */
3362
+ currentFileReviewIssues() {
3363
+ return this.db.prepare(
3364
+ `SELECT f.path, f.hash AS content_hash, r.issues
3365
+ FROM files f
3366
+ JOIN file_reviews r ON r.content_hash = f.hash
3367
+ WHERE f.collection_id = ? AND f.hash IS NOT NULL AND r.notes_md IS NOT NULL
3368
+ ORDER BY f.path`
3369
+ ).all(this.collectionId).map((row) => ({
3370
+ path: row.path,
3371
+ contentHash: row.content_hash,
3372
+ issues: parseIssuesJson(row.issues)
3373
+ }));
3374
+ }
3070
3375
  /**
3071
3376
  * Files of one kind whose current hash needs a review under the given
3072
3377
  * rubric: no row yet, or covered by a DIFFERENT rubric and still holding
@@ -3236,6 +3541,17 @@ var IndexStore = class _IndexStore {
3236
3541
  return out;
3237
3542
  };
3238
3543
  const bySeverity = tally(`COALESCE(json_extract(j.value, '$.severity'), 'info')`);
3544
+ const reviewers = this.db.prepare(
3545
+ `SELECT r.model, r.provider, r.gezel_name, COUNT(DISTINCT r.content_hash) AS files
3546
+ ${FROM}
3547
+ GROUP BY r.model, r.provider, r.gezel_name
3548
+ ORDER BY files DESC LIMIT 5`
3549
+ ).all(...params).map((r) => ({
3550
+ model: r.model,
3551
+ provider: r.provider,
3552
+ gezelName: r.gezel_name,
3553
+ files: Number(r.files)
3554
+ }));
3239
3555
  return {
3240
3556
  issues: rows.filter((r) => r.message).map((r) => ({
3241
3557
  path: r.path,
@@ -3248,18 +3564,34 @@ var IndexStore = class _IndexStore {
3248
3564
  total: Object.values(bySeverity).reduce((a, b) => a + b, 0),
3249
3565
  bySeverity,
3250
3566
  byCategory: tally(`COALESCE(json_extract(j.value, '$.category'), 'general')`)
3251
- }
3567
+ },
3568
+ reviewers
3252
3569
  };
3253
3570
  }
3254
3571
  // ── area summaries (deep pass) ─────────────────────────────────────────
3255
3572
  upsertAreaSummary(rec) {
3256
3573
  this.db.prepare(
3257
- `INSERT INTO area_summaries (collection_id, area_path, input_hash, summary_md, model, created_at)
3258
- VALUES (?, ?, ?, ?, ?, ?)
3574
+ `INSERT INTO area_summaries (collection_id, area_path, input_hash, summary_md, model,
3575
+ provider, gezel_id, gezel_name, app_version, created_at)
3576
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3259
3577
  ON CONFLICT(collection_id, area_path) DO UPDATE SET
3260
3578
  input_hash=excluded.input_hash, summary_md=excluded.summary_md,
3261
- model=excluded.model, created_at=excluded.created_at`
3262
- ).run(this.collectionId, rec.areaPath, rec.inputHash, rec.summaryMd, rec.model, nowIso());
3579
+ model=excluded.model,
3580
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3581
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3582
+ created_at=excluded.created_at`
3583
+ ).run(
3584
+ this.collectionId,
3585
+ rec.areaPath,
3586
+ rec.inputHash,
3587
+ rec.summaryMd,
3588
+ rec.model,
3589
+ rec.provenance?.provider ?? null,
3590
+ rec.provenance?.gezelId ?? null,
3591
+ rec.provenance?.gezelName ?? null,
3592
+ rec.provenance?.appVersion ?? null,
3593
+ nowIso()
3594
+ );
3263
3595
  }
3264
3596
  getAreaSummary(areaPath) {
3265
3597
  const row = this.db.prepare(
@@ -4025,7 +4357,7 @@ function parseBindings(raw) {
4025
4357
 
4026
4358
  // src/index-store/symbols.ts
4027
4359
  import { createRequire as createRequire3 } from "module";
4028
- import { dirname as dirname6, join as join8 } from "path";
4360
+ import { dirname as dirname7, join as join9 } from "path";
4029
4361
  var nodeRequire2 = createRequire3(import.meta.url);
4030
4362
  var SIG_CAP = 160;
4031
4363
  var GRAMMAR_FILE = {
@@ -4098,8 +4430,8 @@ async function ensureInit() {
4098
4430
  initPromise = (async () => {
4099
4431
  try {
4100
4432
  const mod = nodeRequire2("web-tree-sitter");
4101
- const wtsDir = dirname6(nodeRequire2.resolve("web-tree-sitter"));
4102
- await mod.Parser.init({ locateFile: (f) => join8(wtsDir, f) });
4433
+ const wtsDir = dirname7(nodeRequire2.resolve("web-tree-sitter"));
4434
+ await mod.Parser.init({ locateFile: (f) => join9(wtsDir, f) });
4103
4435
  ParserCtor = mod.Parser;
4104
4436
  LanguageMod = mod.Language;
4105
4437
  return true;
@@ -4110,7 +4442,7 @@ async function ensureInit() {
4110
4442
  return initPromise;
4111
4443
  }
4112
4444
  function grammarDir() {
4113
- return join8(dirname6(nodeRequire2.resolve("tree-sitter-wasms/package.json")), "out");
4445
+ return join9(dirname7(nodeRequire2.resolve("tree-sitter-wasms/package.json")), "out");
4114
4446
  }
4115
4447
  async function loadLanguage(langId) {
4116
4448
  const file = GRAMMAR_FILE[langId];
@@ -4118,7 +4450,7 @@ async function loadLanguage(langId) {
4118
4450
  if (langCache.has(langId)) return langCache.get(langId) ?? null;
4119
4451
  let lang = null;
4120
4452
  try {
4121
- lang = await LanguageMod.load(join8(grammarDir(), file));
4453
+ lang = await LanguageMod.load(join9(grammarDir(), file));
4122
4454
  } catch {
4123
4455
  lang = null;
4124
4456
  }
@@ -4387,7 +4719,7 @@ function firstLine(text) {
4387
4719
 
4388
4720
  // src/index-store/content-indexer.ts
4389
4721
  var MAX_FILES = 5e4;
4390
- var EXTRACTOR_VERSION = 2;
4722
+ var EXTRACTOR_VERSION = 3;
4391
4723
  var INDEXED_PNG_TEXT_KEYS = ["parameters", "prompt", "Description", "Comment", "Title"];
4392
4724
  var MAX_INDEXED_PNG_TEXT = 2e3;
4393
4725
  function embeddablePngText(text) {
@@ -4402,7 +4734,7 @@ function embeddablePngText(text) {
4402
4734
  }
4403
4735
  return parts.length > 0 ? ` ${parts.join(" ")}` : "";
4404
4736
  }
4405
- async function indexWorkspaceContent(store, workspaceDir) {
4737
+ async function indexWorkspaceContent(store, workspaceDir, artifactsDir) {
4406
4738
  const stats = {
4407
4739
  scanned: 0,
4408
4740
  changed: 0,
@@ -4490,6 +4822,39 @@ async function indexWorkspaceContent(store, workspaceDir) {
4490
4822
  ]);
4491
4823
  continue;
4492
4824
  }
4825
+ if (cls.modality === "audio") {
4826
+ let bytes;
4827
+ try {
4828
+ bytes = await readFile3(file.abs);
4829
+ } catch {
4830
+ stats.skipped++;
4831
+ continue;
4832
+ }
4833
+ const hash2 = sha256(bytes);
4834
+ const record = {
4835
+ path: file.path,
4836
+ hash: hash2,
4837
+ size: file.size,
4838
+ mtimeMs: file.mtimeMs,
4839
+ lang: cls.lang,
4840
+ kind: cls.kind,
4841
+ modality: cls.modality,
4842
+ trivial: cls.trivial,
4843
+ indexedAt,
4844
+ loc: null
4845
+ };
4846
+ if (existing && existing.hash === hash2) {
4847
+ store.upsertFile(record);
4848
+ continue;
4849
+ }
4850
+ stats.changed++;
4851
+ store.upsertFile(record);
4852
+ const nameWords = file.path.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).join(" ");
4853
+ store.putChunks(file.path, hash2, [
4854
+ { kind: "audio", lineStart: 1, lineEnd: 1, text: `${nameWords} audio recording` }
4855
+ ]);
4856
+ continue;
4857
+ }
4493
4858
  if (cls.modality === "doc") {
4494
4859
  let bytes;
4495
4860
  try {
@@ -4511,21 +4876,32 @@ async function indexWorkspaceContent(store, workspaceDir) {
4511
4876
  indexedAt,
4512
4877
  loc: null
4513
4878
  };
4879
+ const convertible = isConvertibleDoc(extname3(file.path));
4514
4880
  if (existing && existing.hash === hash2) {
4515
4881
  store.upsertFile(record);
4882
+ if (!convertible) continue;
4883
+ const paths = shadowDocFilesPaths(artifactsDir, file.path);
4884
+ const sidecarPresent = paths ? await stat3(paths.mdPath).then(
4885
+ (s) => s.isFile(),
4886
+ () => false
4887
+ ) : true;
4888
+ if (sidecarPresent) continue;
4889
+ const state = store.getMetadata(file.path)["doc:convert"];
4890
+ if (state && state !== "ok") continue;
4891
+ const conv = await ensureShadowDocSidecar(file.abs, artifactsDir, file.path);
4892
+ store.setMetadata(file.path, [{ key: "doc:convert", value: convState(conv) }]);
4893
+ if (conv?.markdown != null) stats.docsConverted++;
4516
4894
  continue;
4517
4895
  }
4518
4896
  stats.changed++;
4519
4897
  store.upsertFile(record);
4520
- if (isConvertibleDoc(extname3(file.path))) {
4521
- const conv = await ensureConvertedMarkdownSidecar(
4522
- file.abs,
4523
- docFilesPaths(workspaceDir, file.path)
4524
- );
4525
- if (conv.markdown != null) {
4898
+ if (convertible) {
4899
+ const conv = await ensureShadowDocSidecar(file.abs, artifactsDir, file.path);
4900
+ store.setMetadata(file.path, [{ key: "doc:convert", value: convState(conv) }]);
4901
+ if (conv?.markdown != null) {
4526
4902
  store.putChunks(file.path, hash2, chunkMarkdown(conv.markdown));
4527
4903
  stats.docsConverted++;
4528
- } else if (conv.blocked) {
4904
+ } else if (conv?.blocked) {
4529
4905
  store.putChunks(file.path, hash2, [
4530
4906
  {
4531
4907
  kind: "doc",
@@ -4604,11 +4980,51 @@ async function indexWorkspaceContent(store, workspaceDir) {
4604
4980
  if (!seen.has(f.path)) {
4605
4981
  store.deleteFile(f.path);
4606
4982
  stats.removed++;
4983
+ if (isConvertibleDoc(extname3(f.path))) {
4984
+ const paths = shadowDocFilesPaths(artifactsDir, f.path);
4985
+ if (paths) await rm4(paths.dir, { recursive: true, force: true }).catch(() => {
4986
+ });
4987
+ }
4607
4988
  }
4608
4989
  }
4990
+ await sweepShadowOrphans(artifactsDir, seen);
4609
4991
  if (forceCode) store.setMeta("extractor_version", String(EXTRACTOR_VERSION));
4610
4992
  return stats;
4611
4993
  }
4994
+ var SHADOW_SWEEP_MAX_DIRS = 5e4;
4995
+ async function sweepShadowOrphans(artifactsDir, seen) {
4996
+ const shadowRoot = join10(artifactsDir, PROJECT_SHADOW_DIR_NAME2);
4997
+ const queue = [""];
4998
+ let visited = 0;
4999
+ while (queue.length > 0) {
5000
+ const rel = queue.shift();
5001
+ let entries;
5002
+ try {
5003
+ entries = await readdir2(join10(shadowRoot, rel), { withFileTypes: true });
5004
+ } catch {
5005
+ continue;
5006
+ }
5007
+ for (const e of entries) {
5008
+ if (!e.isDirectory()) continue;
5009
+ if (++visited > SHADOW_SWEEP_MAX_DIRS) return;
5010
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
5011
+ if (e.name.endsWith("_files")) {
5012
+ const sourceRel = childRel.slice(0, -"_files".length);
5013
+ if (!seen.has(sourceRel)) {
5014
+ await rm4(join10(shadowRoot, childRel), { recursive: true, force: true }).catch(() => {
5015
+ });
5016
+ }
5017
+ } else {
5018
+ queue.push(childRel);
5019
+ }
5020
+ }
5021
+ }
5022
+ }
5023
+ function convState(conv) {
5024
+ if (conv?.markdown != null) return "ok";
5025
+ if (conv?.blocked) return "blocked";
5026
+ return "failed";
5027
+ }
4612
5028
 
4613
5029
  // src/index-store/static-index-worker.ts
4614
5030
  if (!parentPort) throw new Error("static-index-worker must be run as a worker thread");
@@ -4624,7 +5040,11 @@ port.on("message", (request) => {
4624
5040
  });
4625
5041
  if (!index) throw new Error(`content index unavailable at ${request.dbPath}`);
4626
5042
  try {
4627
- const stats = await indexWorkspaceContent(index, request.workspaceDir);
5043
+ const stats = await indexWorkspaceContent(
5044
+ index,
5045
+ request.workspaceDir,
5046
+ request.artifactsDir
5047
+ );
4628
5048
  port.postMessage({ id: request.id, stats });
4629
5049
  } finally {
4630
5050
  index.close();