@bendyline/gezel-service 0.1.0 → 1.0.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 (108) hide show
  1. package/dist/NOTICE.md +1 -1
  2. package/dist/bin/gezeld.js +51635 -50059
  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.26223.md +82 -0
  14. package/dist/handboek-content/whats-new/whats-new-index.md +28 -0
  15. package/dist/handboek.d.ts +53 -26
  16. package/dist/handboek.js +103 -26
  17. package/dist/index-store/static-index-worker.js +476 -82
  18. package/dist/index.d.ts +206 -11
  19. package/dist/index.js +51615 -50051
  20. package/dist/ui/assets/{TerminalCodeEditor-CB-AXtKI.js → TerminalCodeEditor-DQICIlaI.js} +2 -2
  21. package/dist/ui/assets/{_.contribution-C6KlZ1yR.js → _.contribution-BXBGjh98.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-UC-hXlnG.js} +1 -1
  28. package/dist/ui/assets/{csharp.contribution-jBA6TWhG.js → csharp.contribution-VSLdDbxa.js} +1 -1
  29. package/dist/ui/assets/{css.contribution-DQSNU6vC.js → css.contribution-DC9uQgZx.js} +1 -1
  30. package/dist/ui/assets/{cssMode-68UP_lrS.js → cssMode-DZsXawIN.js} +1 -1
  31. package/dist/ui/assets/{csv-Ctux8fO7.js → csv-RAgECoUg.js} +1 -1
  32. package/dist/ui/assets/dist-D6I_6t8i.js +195 -0
  33. package/dist/ui/assets/dist-a57vyaRu.js +2 -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-PUE2MRJF.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-CsrBAk1W.js} +1 -1
  40. package/dist/ui/assets/{handlebars-Dq5dJ5WJ.js → handlebars-Bqyi6xyi.js} +1 -1
  41. package/dist/ui/assets/handlebars.contribution-C6VLDI51.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-NLb64cMt.js} +1 -1
  44. package/dist/ui/assets/html.contribution-BP2a6izy.js +2 -0
  45. package/dist/ui/assets/{htmlMode-D5UKInqK.js → htmlMode-Utzysyes.js} +1 -1
  46. package/dist/ui/assets/{index-CPXp6BEZ.js → index-B_NFDPxp.js} +197 -195
  47. package/dist/ui/assets/index-CgP6VwNS.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-CfehzoVj.js} +1 -1
  50. package/dist/ui/assets/{java.contribution-B8o1lBqP.js → java.contribution-CXk8bwAp.js} +1 -1
  51. package/dist/ui/assets/{javascript-COWkS1tr.js → javascript-BcbxETop.js} +1 -1
  52. package/dist/ui/assets/javascript.contribution-BRDuHf7J.js +2 -0
  53. package/dist/ui/assets/{jsonMode-CWWZvfzr.js → jsonMode-BORbF5qz.js} +1 -1
  54. package/dist/ui/assets/{kotlin.contribution-CxSOPQ6P.js → kotlin.contribution-CbKeqa_3.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-BgNGBZAo.js} +1 -1
  57. package/dist/ui/assets/{lua.contribution-894DRgPR.js → lua.contribution-CtZNsqOj.js} +1 -1
  58. package/dist/ui/assets/{markdown.contribution-d_TBTsNA.js → markdown.contribution-CNyxmHy1.js} +1 -1
  59. package/dist/ui/assets/{monaco-PebRhLpo.js → monaco-Ryh-SF_b.js} +3 -3
  60. package/dist/ui/assets/{monaco-base-4S0M5BiY.js → monaco-base-C-4W0je-.js} +1 -1
  61. package/dist/ui/assets/{monaco-setup-7hp0Z9jn.js → monaco-setup-BW0DQ8MV.js} +1 -1
  62. package/dist/ui/assets/{monaco.contribution-DQjPk7Rx.js → monaco.contribution-279M9-Uu.js} +2 -2
  63. package/dist/ui/assets/{monaco.contribution-CKDvyGVE.js → monaco.contribution-Bu1oyef5.js} +2 -2
  64. package/dist/ui/assets/{monaco.contribution-BL_zgiu2.js → monaco.contribution-C57BAnnI.js} +2 -2
  65. package/dist/ui/assets/{monaco.contribution-D61txvUB.js → monaco.contribution-o3cuP4Wp.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-BulxzIpe.js} +1 -1
  68. package/dist/ui/assets/pptx-CsdVR0xn.js +6 -0
  69. package/dist/ui/assets/{python-CHygH1eM.js → python-D4zGC71G.js} +1 -1
  70. package/dist/ui/assets/python.contribution-CDI1t73R.js +2 -0
  71. package/dist/ui/assets/{ruby.contribution-BQdPKFM2.js → ruby.contribution-DVOdhLt8.js} +1 -1
  72. package/dist/ui/assets/{rust.contribution-D7wKqShc.js → rust.contribution-DLR9kg2m.js} +1 -1
  73. package/dist/ui/assets/{scss.contribution-kSTLMoty.js → scss.contribution-CYIIxBxH.js} +1 -1
  74. package/dist/ui/assets/{shell.contribution-BQnT3BBJ.js → shell.contribution-D0KjHQ5E.js} +1 -1
  75. package/dist/ui/assets/{sql.contribution-C9z55nWb.js → sql.contribution-CKhLelqr.js} +1 -1
  76. package/dist/ui/assets/{swift.contribution-BZoW5ZZj.js → swift.contribution-DiAAMfJp.js} +1 -1
  77. package/dist/ui/assets/{terminal-monaco-setup-DbSXji0d.js → terminal-monaco-setup-Dhxpm7Z8.js} +1 -1
  78. package/dist/ui/assets/{tsMode-jPpKNC6E.js → tsMode-DFEqg3P9.js} +1 -1
  79. package/dist/ui/assets/{typescript-Djn_8BHF.js → typescript-X_x8S3Dp.js} +1 -1
  80. package/dist/ui/assets/typescript.contribution-jI3BakLa.js +2 -0
  81. package/dist/ui/assets/xlsx-CdLmEC42.js +4 -0
  82. package/dist/ui/assets/{xml-DmE4Aqa-.js → xml-ANmRi8ZZ.js} +1 -1
  83. package/dist/ui/assets/xml.contribution-D5t83zAi.js +2 -0
  84. package/dist/ui/assets/{yaml-BjmK3C-c.js → yaml-R4WpJved.js} +1 -1
  85. package/dist/ui/assets/yaml.contribution-DV8dAjKa.js +2 -0
  86. package/dist/ui/index.html +15 -15
  87. package/package.json +13 -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,9 +1008,9 @@ 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
1015
  import { windowsDetachedSpawnOptions } from "@bendyline/gezel/native";
919
1016
 
@@ -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
  }
@@ -1210,7 +1307,7 @@ async function expandPermissionPaths(paths) {
1210
1307
  const expanded = [];
1211
1308
  const seen = /* @__PURE__ */ new Set();
1212
1309
  for (const p of paths) {
1213
- for (const candidate of [p, await realpath2(p).catch(() => p)]) {
1310
+ for (const candidate of [p, await realpath3(p).catch(() => p)]) {
1214
1311
  if (seen.has(candidate)) continue;
1215
1312
  seen.add(candidate);
1216
1313
  expanded.push(candidate);
@@ -1448,7 +1545,7 @@ syncBuiltinESMExports();
1448
1545
 
1449
1546
  // src/utils/service-worker-entry.ts
1450
1547
  import { existsSync } from "fs";
1451
- import { dirname as dirname2, join as join3, resolve } from "path";
1548
+ import { dirname as dirname3, join as join4, resolve } from "path";
1452
1549
  import { fileURLToPath } from "url";
1453
1550
  var SERVICE_WORKER_ENTRIES = {
1454
1551
  embeddings: { built: "memory/embed-worker.js" },
@@ -1461,10 +1558,10 @@ var SERVICE_WORKER_ENTRIES = {
1461
1558
  };
1462
1559
  var MAX_ANCESTOR_DEPTH = 8;
1463
1560
  function findServiceBundleResource(moduleUrl, relativePaths) {
1464
- let dir = dirname2(fileURLToPath(moduleUrl));
1561
+ let dir = dirname3(fileURLToPath(moduleUrl));
1465
1562
  const visited = /* @__PURE__ */ new Set();
1466
1563
  for (let depth = 0; depth < MAX_ANCESTOR_DEPTH; depth++) {
1467
- for (const root of [dir, join3(dir, "dist")]) {
1564
+ for (const root of [dir, join4(dir, "dist")]) {
1468
1565
  for (const relativePath of relativePaths) {
1469
1566
  const candidate = resolve(root, relativePath);
1470
1567
  if (visited.has(candidate)) continue;
@@ -1472,7 +1569,7 @@ function findServiceBundleResource(moduleUrl, relativePaths) {
1472
1569
  if (existsSync(candidate)) return candidate;
1473
1570
  }
1474
1571
  }
1475
- const parent = dirname2(dir);
1572
+ const parent = dirname3(dir);
1476
1573
  if (parent === dir) break;
1477
1574
  dir = parent;
1478
1575
  }
@@ -1494,8 +1591,8 @@ function findRepoRoot(start) {
1494
1591
  let dir = start;
1495
1592
  let highest = null;
1496
1593
  for (; ; ) {
1497
- if (existsSync2(join4(dir, "node_modules"))) highest = dir;
1498
- const parent = dirname3(dir);
1594
+ if (existsSync2(join5(dir, "node_modules"))) highest = dir;
1595
+ const parent = dirname4(dir);
1499
1596
  if (parent === dir) break;
1500
1597
  dir = parent;
1501
1598
  }
@@ -1505,12 +1602,12 @@ var readRootsCache;
1505
1602
  function sandboxReadRoots() {
1506
1603
  if (readRootsCache !== void 0) return readRootsCache;
1507
1604
  const roots = /* @__PURE__ */ new Set();
1508
- const repoRoot = findRepoRoot(dirname3(fileURLToPath2(import.meta.url)));
1605
+ const repoRoot = findRepoRoot(dirname4(fileURLToPath2(import.meta.url)));
1509
1606
  if (repoRoot) roots.add(repoRoot);
1510
1607
  try {
1511
1608
  const require2 = createRequire(import.meta.url);
1512
1609
  const resolved = require2.resolve("@bendyline/squisq-formats");
1513
- const linkedRoot = findRepoRoot(dirname3(resolved));
1610
+ const linkedRoot = findRepoRoot(dirname4(resolved));
1514
1611
  if (linkedRoot) roots.add(linkedRoot);
1515
1612
  } catch {
1516
1613
  readRootsCache = null;
@@ -1557,10 +1654,10 @@ async function convertInSandbox(absPath, ext) {
1557
1654
  log.warn("sandbox unavailable (worker or node_modules root unresolved); skipping conversion");
1558
1655
  return { markdown: null };
1559
1656
  }
1560
- const scratch = await realpath3(await mkdtemp2(join4(tmpdir2(), "gezel-convert-")));
1657
+ const scratch = await realpath4(await mkdtemp2(join5(tmpdir2(), "gezel-convert-")));
1561
1658
  try {
1562
1659
  const inputName = `input.${ext}`;
1563
- await writeFile3(join4(scratch, inputName), bytes);
1660
+ await writeFile3(join5(scratch, inputName), bytes);
1564
1661
  const res = await runInSandbox({
1565
1662
  entry: worker.path,
1566
1663
  cwd: scratch,
@@ -1585,7 +1682,7 @@ async function convertInSandbox(absPath, ext) {
1585
1682
  );
1586
1683
  return { markdown: null };
1587
1684
  }
1588
- const md = await readFile(join4(scratch, "output.md"), "utf8").catch(() => null);
1685
+ const md = await readFile(join5(scratch, "output.md"), "utf8").catch(() => null);
1589
1686
  return { markdown: md };
1590
1687
  } finally {
1591
1688
  await rm3(scratch, { recursive: true, force: true });
@@ -1603,16 +1700,19 @@ async function convertDocToMarkdown(absPath) {
1603
1700
  if (!isConvertibleDoc(ext)) return { markdown: null };
1604
1701
  return convertInSandbox(absPath, ext);
1605
1702
  }
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);
1703
+ function shadowDocFilesPaths(artifactsDir, relPath) {
1704
+ const base = basename3(relPath);
1705
+ const nameNoExt = base.replace(/\.[^.]+$/, "") || base;
1706
+ const parent = dirname5(relPath);
1707
+ const relMd = join6(parent === "." ? "" : parent, `${base}_files`, `${nameNoExt}.md`);
1708
+ const shadowRoot = join6(artifactsDir, PROJECT_SHADOW_DIR_NAME);
1709
+ const mdPath = safeJoin(shadowRoot, relMd);
1710
+ if (!mdPath) return null;
1711
+ return {
1712
+ dir: dirname5(mdPath),
1713
+ mdPath,
1714
+ mdRel: relative2(artifactsDir, mdPath).replaceAll("\\", "/")
1715
+ };
1616
1716
  }
1617
1717
  async function writeConvertedMarkdownAt(paths, markdown) {
1618
1718
  await mkdir(paths.dir, { recursive: true });
@@ -1637,6 +1737,15 @@ async function ensureConvertedMarkdownSidecar(sourcePath, paths) {
1637
1737
  }
1638
1738
  return { ...converted, paths };
1639
1739
  }
1740
+ async function ensureShadowDocSidecar(sourceAbs, artifactsDir, relPath) {
1741
+ const paths = shadowDocFilesPaths(artifactsDir, relPath);
1742
+ if (!paths) return null;
1743
+ const shadowRoot = join6(artifactsDir, PROJECT_SHADOW_DIR_NAME);
1744
+ await mkdir(shadowRoot, { recursive: true }).catch(() => {
1745
+ });
1746
+ if (!await realpathContained(shadowRoot, paths.mdPath)) return null;
1747
+ return ensureConvertedMarkdownSidecar(sourceAbs, paths);
1748
+ }
1640
1749
  function chunkMarkdown(md) {
1641
1750
  const lines = md.split(/\r?\n/);
1642
1751
  const headIdx = [];
@@ -1684,8 +1793,8 @@ function parseFrontmatter(content) {
1684
1793
 
1685
1794
  // src/index-store/gitignore.ts
1686
1795
  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";
1796
+ import { join as join7 } from "path";
1797
+ import { projectLocalIndexDir } from "@bendyline/gezel/paths";
1689
1798
 
1690
1799
  // src/index-store/hash.ts
1691
1800
  import { createHash } from "crypto";
@@ -1994,7 +2103,7 @@ import { createLogger as createLogger3 } from "@bendyline/gezel";
1994
2103
 
1995
2104
  // src/transformers-cache.ts
1996
2105
  import { mkdir as mkdir3 } from "fs/promises";
1997
- import { join as join7 } from "path";
2106
+ import { join as join8 } from "path";
1998
2107
  import { createLogger as createLogger2 } from "@bendyline/gezel";
1999
2108
  var log2 = createLogger2("service");
2000
2109
 
@@ -2103,6 +2212,10 @@ CREATE TABLE IF NOT EXISTS summaries (
2103
2212
  summary_md TEXT,
2104
2213
  tags TEXT,
2105
2214
  model TEXT,
2215
+ provider TEXT,
2216
+ gezel_id TEXT,
2217
+ gezel_name TEXT,
2218
+ app_version TEXT,
2106
2219
  created_at TEXT
2107
2220
  );
2108
2221
 
@@ -2219,6 +2332,10 @@ CREATE TABLE IF NOT EXISTS symbol_summaries (
2219
2332
  symbol_name TEXT NOT NULL,
2220
2333
  summary TEXT NOT NULL,
2221
2334
  model TEXT,
2335
+ provider TEXT,
2336
+ gezel_id TEXT,
2337
+ gezel_name TEXT,
2338
+ app_version TEXT,
2222
2339
  created_at TEXT,
2223
2340
  PRIMARY KEY (collection_id, file_path, file_hash, symbol_name)
2224
2341
  );
@@ -2235,6 +2352,10 @@ CREATE TABLE IF NOT EXISTS area_summaries (
2235
2352
  input_hash TEXT NOT NULL,
2236
2353
  summary_md TEXT NOT NULL,
2237
2354
  model TEXT,
2355
+ provider TEXT,
2356
+ gezel_id TEXT,
2357
+ gezel_name TEXT,
2358
+ app_version TEXT,
2238
2359
  created_at TEXT,
2239
2360
  PRIMARY KEY (collection_id, area_path)
2240
2361
  );
@@ -2258,12 +2379,30 @@ CREATE TABLE IF NOT EXISTS file_reviews (
2258
2379
  health INTEGER,
2259
2380
  health_reason TEXT,
2260
2381
  model TEXT,
2382
+ provider TEXT,
2383
+ gezel_id TEXT,
2384
+ gezel_name TEXT,
2385
+ app_version TEXT,
2261
2386
  reviewed_at TEXT,
2262
2387
  attempt_rubric_hash TEXT,
2263
2388
  attempts INTEGER DEFAULT 0,
2264
2389
  last_attempt_at TEXT
2265
2390
  );
2266
2391
 
2392
+ -- AI-shadow gate (images described by the vision model, audio transcribed by
2393
+ -- STT into artifacts/shadow markdown). Same content-addressed discipline as
2394
+ -- enrichments: 'ok' rows survive renames/reverts; failures retry up to a cap
2395
+ -- per hash. The sidecar file is the artifact; this row is only the work gate.
2396
+ CREATE TABLE IF NOT EXISTS shadow_state (
2397
+ content_hash TEXT PRIMARY KEY,
2398
+ collection_id TEXT,
2399
+ file_path TEXT,
2400
+ state TEXT,
2401
+ attempts INTEGER DEFAULT 0,
2402
+ model TEXT,
2403
+ updated_at TEXT
2404
+ );
2405
+
2267
2406
  -- Mirror of the append-only history JSONL (the audit log). kind/project/gezel/at
2268
2407
  -- are real columns so HistoryFilter fields stay SQL-filterable; only the q text
2269
2408
  -- goes through fts_history. INSERT OR IGNORE on the event id makes JSONL
@@ -2292,7 +2431,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS fts_metadata
2292
2431
  CREATE VIRTUAL TABLE IF NOT EXISTS fts_history
2293
2432
  USING fts5(body, event_id UNINDEXED, collection_id UNINDEXED);
2294
2433
  `;
2295
- var SCHEMA_VERSION = 9;
2434
+ var SCHEMA_VERSION = 10;
2296
2435
  function addColumnIfMissing(db, table, column, decl) {
2297
2436
  try {
2298
2437
  const cols = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -2316,6 +2455,12 @@ function applySchema(db) {
2316
2455
  WHERE fingerprint IS NULL OR fingerprint = ''`);
2317
2456
  } catch {
2318
2457
  }
2458
+ for (const table of ["summaries", "symbol_summaries", "area_summaries", "file_reviews"]) {
2459
+ addColumnIfMissing(db, table, "provider", "TEXT");
2460
+ addColumnIfMissing(db, table, "gezel_id", "TEXT");
2461
+ addColumnIfMissing(db, table, "gezel_name", "TEXT");
2462
+ addColumnIfMissing(db, table, "app_version", "TEXT");
2463
+ }
2319
2464
  let fts = true;
2320
2465
  try {
2321
2466
  db.exec(FTS_DDL);
@@ -2343,7 +2488,7 @@ function applySchema(db) {
2343
2488
  // src/index-store/sqlite-driver.ts
2344
2489
  import { mkdir as mkdir4, rename as rename2 } from "fs/promises";
2345
2490
  import { createRequire as createRequire2 } from "module";
2346
- import { dirname as dirname5 } from "path";
2491
+ import { dirname as dirname6 } from "path";
2347
2492
  import { createLogger as createLogger4 } from "@bendyline/gezel";
2348
2493
  var nodeRequire = createRequire2(import.meta.url);
2349
2494
  var log4 = createLogger4("sqlite");
@@ -2366,7 +2511,7 @@ async function openIndexDatabase(dbPath) {
2366
2511
  }
2367
2512
  if (dbPath !== ":memory:") {
2368
2513
  try {
2369
- await mkdir4(dirname5(dbPath), { recursive: true });
2514
+ await mkdir4(dirname6(dbPath), { recursive: true });
2370
2515
  } catch (error) {
2371
2516
  if (isUnavailableIndexError(error)) return null;
2372
2517
  throw error;
@@ -2375,6 +2520,7 @@ async function openIndexDatabase(dbPath) {
2375
2520
  let db;
2376
2521
  try {
2377
2522
  db = new DatabaseSync(dbPath, { allowExtension: true });
2523
+ setBusyTimeout(db);
2378
2524
  assertReadableHeader(db);
2379
2525
  } catch (error) {
2380
2526
  if (dbPath === ":memory:" || !isCorruptDatabaseError(error)) {
@@ -2391,6 +2537,7 @@ async function openIndexDatabase(dbPath) {
2391
2537
  await quarantineCorruptDatabase(dbPath);
2392
2538
  log4.warn(`[sqlite] quarantined corrupt rebuildable index ${dbPath}`);
2393
2539
  db = new DatabaseSync(dbPath, { allowExtension: true });
2540
+ setBusyTimeout(db);
2394
2541
  }
2395
2542
  if (!db) throw new Error("failed to open sqlite index");
2396
2543
  let vecAvailable = false;
@@ -2439,6 +2586,12 @@ async function openIndexDatabase(dbPath) {
2439
2586
  function assertReadableHeader(db) {
2440
2587
  db.prepare("PRAGMA schema_version").get();
2441
2588
  }
2589
+ function setBusyTimeout(db) {
2590
+ try {
2591
+ db.exec("PRAGMA busy_timeout=5000;");
2592
+ } catch {
2593
+ }
2594
+ }
2442
2595
  function isCorruptDatabaseError(error) {
2443
2596
  const message = error instanceof Error ? error.message : String(error);
2444
2597
  return /SQLITE_CORRUPT|SQLITE_NOTADB|database disk image is malformed|file is not a database/i.test(
@@ -2447,9 +2600,7 @@ function isCorruptDatabaseError(error) {
2447
2600
  }
2448
2601
  function isUnavailableIndexError(error) {
2449
2602
  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
- );
2603
+ return /SQLITE_READONLY|SQLITE_CANTOPEN|EACCES|EPERM|read-only/i.test(message);
2453
2604
  }
2454
2605
  async function quarantineCorruptDatabase(dbPath) {
2455
2606
  const suffix = `.corrupt-${Date.now()}`;
@@ -2728,7 +2879,9 @@ var IndexStore = class _IndexStore {
2728
2879
  `SELECT f.* FROM files f
2729
2880
  LEFT JOIN enrichments e ON e.content_hash = f.hash
2730
2881
  WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2731
- AND f.modality IN ('code','text','doc')
2882
+ AND (f.modality IN ('code','text','doc')
2883
+ OR EXISTS (SELECT 1 FROM shadow_state s
2884
+ WHERE s.content_hash = f.hash AND s.state = 'ok'))
2732
2885
  AND (e.content_hash IS NULL
2733
2886
  OR (e.embedded_at IS NULL AND COALESCE(e.attempts, 0) < ${MAX_ENRICH_ATTEMPTS}))
2734
2887
  ORDER BY f.path LIMIT ?`
@@ -2742,12 +2895,45 @@ var IndexStore = class _IndexStore {
2742
2895
  `SELECT COUNT(*) AS n FROM files f
2743
2896
  LEFT JOIN enrichments e ON e.content_hash = f.hash
2744
2897
  WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2745
- AND f.modality IN ('code','text','doc')
2898
+ AND (f.modality IN ('code','text','doc')
2899
+ OR EXISTS (SELECT 1 FROM shadow_state s
2900
+ WHERE s.content_hash = f.hash AND s.state = 'ok'))
2746
2901
  AND (e.content_hash IS NULL
2747
2902
  OR (e.embedded_at IS NULL AND COALESCE(e.attempts, 0) < ${MAX_ENRICH_ATTEMPTS}))`
2748
2903
  ).get(this.collectionId);
2749
2904
  return row?.n ?? 0;
2750
2905
  }
2906
+ /**
2907
+ * Image/audio files whose AI shadow (description / transcript) is missing
2908
+ * or stale for the current content hash. Same capped-retry discipline as
2909
+ * {@link filesNeedingEnrichment}; an 'ok' row is the terminal success.
2910
+ */
2911
+ filesNeedingAiShadow(limit = 5, maxAttempts = MAX_ENRICH_ATTEMPTS) {
2912
+ return this.db.prepare(
2913
+ `SELECT f.* FROM files f
2914
+ LEFT JOIN shadow_state s ON s.content_hash = f.hash
2915
+ WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2916
+ AND f.modality IN ('image','audio')
2917
+ AND (s.content_hash IS NULL
2918
+ OR (s.state <> 'ok' AND COALESCE(s.attempts, 0) < ?))
2919
+ ORDER BY f.path LIMIT ?`
2920
+ ).all(this.collectionId, maxAttempts, limit).map(rowToFile);
2921
+ }
2922
+ markAiShadowOk(contentHash, filePath, model) {
2923
+ this.db.prepare(
2924
+ `INSERT OR REPLACE INTO shadow_state (content_hash, collection_id, file_path, state, attempts, model, updated_at)
2925
+ VALUES (?, ?, ?, 'ok', 0, ?, ?)`
2926
+ ).run(contentHash, this.collectionId, filePath, model ?? null, nowIso());
2927
+ }
2928
+ markAiShadowAttempt(contentHash, filePath) {
2929
+ this.db.prepare(
2930
+ `INSERT INTO shadow_state (content_hash, collection_id, file_path, state, attempts, updated_at)
2931
+ VALUES (?, ?, ?, 'failed', 1, ?)
2932
+ ON CONFLICT(content_hash) DO UPDATE SET
2933
+ state='failed', attempts=COALESCE(shadow_state.attempts, 0) + 1,
2934
+ updated_at=excluded.updated_at`
2935
+ ).run(contentHash, this.collectionId, filePath, nowIso());
2936
+ }
2751
2937
  /**
2752
2938
  * Enrichment coverage counts. `summarized` counts real summaries (join on
2753
2939
  * the summaries table), deliberately NOT the enrichments gate — the gate also
@@ -2766,15 +2952,39 @@ var IndexStore = class _IndexStore {
2766
2952
  `SELECT COUNT(*) AS n FROM files f JOIN enrichments e ON e.content_hash = f.hash
2767
2953
  WHERE e.embedded_at IS NOT NULL AND ${ELIGIBLE}`
2768
2954
  );
2955
+ const skipped = count(
2956
+ `SELECT COUNT(*) AS n FROM files f JOIN enrichments e ON e.content_hash = f.hash
2957
+ WHERE e.embedded_at IS NULL AND COALESCE(e.attempts, 0) >= ${MAX_ENRICH_ATTEMPTS}
2958
+ AND ${ELIGIBLE}`
2959
+ );
2769
2960
  const embedModel = this.getMeta("embed_model");
2770
2961
  return {
2771
2962
  eligible,
2772
2963
  summarized,
2773
2964
  embedded,
2774
2965
  pending: this.countNeedingEnrichment(),
2966
+ skipped,
2967
+ shadowsPending: this.countNeedingAiShadow(),
2775
2968
  ...embedModel ? { embedModel } : {}
2776
2969
  };
2777
2970
  }
2971
+ /**
2972
+ * COUNT twin of `filesNeedingAiShadow` — media files still awaiting an AI
2973
+ * description, attempt-capped ones excluded. The drive works this tier
2974
+ * before summaries, so status must surface it or a fresh scan looks stuck.
2975
+ */
2976
+ countNeedingAiShadow(maxAttempts = MAX_ENRICH_ATTEMPTS) {
2977
+ return Number(
2978
+ this.db.prepare(
2979
+ `SELECT COUNT(*) AS n FROM files f
2980
+ LEFT JOIN shadow_state s ON s.content_hash = f.hash
2981
+ WHERE f.collection_id = ? AND f.trivial = 0 AND f.hash IS NOT NULL
2982
+ AND f.modality IN ('image','audio')
2983
+ AND (s.content_hash IS NULL
2984
+ OR (s.state <> 'ok' AND COALESCE(s.attempts, 0) < ?))`
2985
+ ).get(this.collectionId, maxAttempts)?.n ?? 0
2986
+ );
2987
+ }
2778
2988
  /** Mark a content hash as enriched (vectors built) so it isn't reprocessed. */
2779
2989
  markEnriched(contentHash) {
2780
2990
  this.db.prepare("INSERT OR REPLACE INTO enrichments (content_hash, embedded_at) VALUES (?, ?)").run(contentHash, nowIso());
@@ -2790,6 +3000,23 @@ var IndexStore = class _IndexStore {
2790
3000
  `INSERT INTO enrichments (content_hash, embedded_at, attempts) VALUES (?, NULL, 1)
2791
3001
  ON CONFLICT(content_hash) DO UPDATE SET attempts = COALESCE(enrichments.attempts, 0) + 1`
2792
3002
  ).run(contentHash);
3003
+ return Number(
3004
+ this.db.prepare("SELECT attempts FROM enrichments WHERE content_hash = ?").get(contentHash)?.attempts ?? 1
3005
+ );
3006
+ }
3007
+ /**
3008
+ * Jump a hash straight to the attempt cap — for deterministic failures
3009
+ * (a provider policy-blocking the file's content) where every retry would
3010
+ * fail identically. The file drops off the work list and counts as
3011
+ * `skipped` in {@link enrichmentCounts} until its content changes.
3012
+ */
3013
+ markEnrichSkipped(contentHash) {
3014
+ this.db.prepare(
3015
+ `INSERT INTO enrichments (content_hash, embedded_at, attempts)
3016
+ VALUES (?, NULL, ${MAX_ENRICH_ATTEMPTS})
3017
+ ON CONFLICT(content_hash) DO UPDATE
3018
+ SET attempts = MAX(COALESCE(enrichments.attempts, 0), ${MAX_ENRICH_ATTEMPTS})`
3019
+ ).run(contentHash);
2793
3020
  }
2794
3021
  /**
2795
3022
  * Keyword search over enrichment summaries. fts_summaries rows are
@@ -2942,11 +3169,15 @@ var IndexStore = class _IndexStore {
2942
3169
  upsertSummary(rec) {
2943
3170
  this.db.transaction(() => {
2944
3171
  this.db.prepare(
2945
- `INSERT INTO summaries (content_hash, collection_id, file_path, summary_md, tags, model, created_at)
2946
- VALUES (?, ?, ?, ?, ?, ?, ?)
3172
+ `INSERT INTO summaries (content_hash, collection_id, file_path, summary_md, tags, model,
3173
+ provider, gezel_id, gezel_name, app_version, created_at)
3174
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2947
3175
  ON CONFLICT(content_hash) DO UPDATE SET
2948
3176
  file_path=excluded.file_path, summary_md=excluded.summary_md,
2949
- tags=excluded.tags, model=excluded.model, created_at=excluded.created_at`
3177
+ tags=excluded.tags, model=excluded.model,
3178
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3179
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3180
+ created_at=excluded.created_at`
2950
3181
  ).run(
2951
3182
  rec.contentHash,
2952
3183
  this.collectionId,
@@ -2954,6 +3185,10 @@ var IndexStore = class _IndexStore {
2954
3185
  rec.summaryMd,
2955
3186
  rec.tags ?? null,
2956
3187
  rec.model,
3188
+ rec.provenance?.provider ?? null,
3189
+ rec.provenance?.gezelId ?? null,
3190
+ rec.provenance?.gezelName ?? null,
3191
+ rec.provenance?.appVersion ?? null,
2957
3192
  nowIso()
2958
3193
  );
2959
3194
  if (this.caps.fts) {
@@ -2982,16 +3217,29 @@ var IndexStore = class _IndexStore {
2982
3217
  * for any other (stale) hash of the same file. Keyed by symbol name only —
2983
3218
  * overloads/same-name symbols share a line (accepted v1 simplification).
2984
3219
  */
2985
- putSymbolSummaries(filePath, fileHash, entries, model) {
3220
+ putSymbolSummaries(filePath, fileHash, entries, model, provenance) {
2986
3221
  this.db.transaction(() => {
2987
3222
  this.db.prepare("DELETE FROM symbol_summaries WHERE collection_id = ? AND file_path = ?").run(this.collectionId, filePath);
2988
3223
  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 (?, ?, ?, ?, ?, ?, ?)`
3224
+ `INSERT OR REPLACE INTO symbol_summaries (collection_id, file_path, file_hash, symbol_name, summary, model,
3225
+ provider, gezel_id, gezel_name, app_version, created_at)
3226
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2991
3227
  );
2992
3228
  const at = nowIso();
2993
3229
  for (const e of entries) {
2994
- ins.run(this.collectionId, filePath, fileHash, e.name, e.summary, model ?? null, at);
3230
+ ins.run(
3231
+ this.collectionId,
3232
+ filePath,
3233
+ fileHash,
3234
+ e.name,
3235
+ e.summary,
3236
+ model ?? null,
3237
+ provenance?.provider ?? null,
3238
+ provenance?.gezelId ?? null,
3239
+ provenance?.gezelName ?? null,
3240
+ provenance?.appVersion ?? null,
3241
+ at
3242
+ );
2995
3243
  }
2996
3244
  });
2997
3245
  }
@@ -3009,14 +3257,17 @@ var IndexStore = class _IndexStore {
3009
3257
  upsertFileReview(rec) {
3010
3258
  this.db.prepare(
3011
3259
  `INSERT INTO file_reviews (content_hash, collection_id, file_path, rubric_hash, notes_md,
3012
- issues, health, health_reason, model, reviewed_at,
3260
+ issues, health, health_reason, model,
3261
+ provider, gezel_id, gezel_name, app_version, reviewed_at,
3013
3262
  attempt_rubric_hash, attempts, last_attempt_at)
3014
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, NULL)
3263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, NULL)
3015
3264
  ON CONFLICT(content_hash) DO UPDATE SET
3016
3265
  collection_id=excluded.collection_id, file_path=excluded.file_path,
3017
3266
  rubric_hash=excluded.rubric_hash, notes_md=excluded.notes_md,
3018
3267
  issues=excluded.issues, health=excluded.health,
3019
3268
  health_reason=excluded.health_reason, model=excluded.model,
3269
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3270
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3020
3271
  reviewed_at=excluded.reviewed_at,
3021
3272
  attempt_rubric_hash=NULL, attempts=0, last_attempt_at=NULL`
3022
3273
  ).run(
@@ -3029,9 +3280,32 @@ var IndexStore = class _IndexStore {
3029
3280
  rec.health,
3030
3281
  rec.healthReason,
3031
3282
  rec.model,
3283
+ rec.provenance?.provider ?? null,
3284
+ rec.provenance?.gezelId ?? null,
3285
+ rec.provenance?.gezelName ?? null,
3286
+ rec.provenance?.appVersion ?? null,
3032
3287
  nowIso()
3033
3288
  );
3034
3289
  }
3290
+ /**
3291
+ * Terminal skip for content that CANNOT be reviewed under its current hash
3292
+ * — a doc whose conversion was blocked/failed, an empty file, a source
3293
+ * deleted between scan and review. Burns the whole attempt budget in one
3294
+ * write (success fields untouched) so `filesNeedingReview` stops selecting
3295
+ * the file; without this, one blocked doc permanently steals a batch slot
3296
+ * from its kind on every sweep. A content change (new hash) or a rubric
3297
+ * edit re-admits it exactly like any capped-out attempt.
3298
+ */
3299
+ markReviewUnavailable(contentHash, filePath, rubricHash, maxAttempts) {
3300
+ this.db.prepare(
3301
+ `INSERT INTO file_reviews (content_hash, collection_id, file_path, attempt_rubric_hash, attempts, last_attempt_at)
3302
+ VALUES (?, ?, ?, ?, ?, ?)
3303
+ ON CONFLICT(content_hash) DO UPDATE SET
3304
+ attempts = excluded.attempts,
3305
+ attempt_rubric_hash = excluded.attempt_rubric_hash,
3306
+ last_attempt_at = excluded.last_attempt_at`
3307
+ ).run(contentHash, this.collectionId, filePath, rubricHash, maxAttempts, nowIso());
3308
+ }
3035
3309
  /**
3036
3310
  * Record a non-empty-but-unparseable review reply WITHOUT touching any
3037
3311
  * success field — a stale-rubric review keeps serving until a new one
@@ -3052,7 +3326,8 @@ var IndexStore = class _IndexStore {
3052
3326
  /** The successful review for a content hash (attempt-only rows excluded). */
3053
3327
  getFileReview(contentHash) {
3054
3328
  const row = this.db.prepare(
3055
- `SELECT notes_md, issues, health, health_reason, rubric_hash, model, reviewed_at
3329
+ `SELECT notes_md, issues, health, health_reason, rubric_hash, model,
3330
+ provider, gezel_id, gezel_name, app_version, reviewed_at
3056
3331
  FROM file_reviews
3057
3332
  WHERE content_hash = ? AND notes_md IS NOT NULL AND health IS NOT NULL`
3058
3333
  ).get(contentHash);
@@ -3064,6 +3339,10 @@ var IndexStore = class _IndexStore {
3064
3339
  healthReason: row.health_reason ?? "",
3065
3340
  rubricHash: row.rubric_hash ?? "",
3066
3341
  model: row.model,
3342
+ provider: row.provider,
3343
+ gezelId: row.gezel_id,
3344
+ gezelName: row.gezel_name,
3345
+ appVersion: row.app_version,
3067
3346
  reviewedAt: row.reviewed_at
3068
3347
  };
3069
3348
  }
@@ -3236,6 +3515,17 @@ var IndexStore = class _IndexStore {
3236
3515
  return out;
3237
3516
  };
3238
3517
  const bySeverity = tally(`COALESCE(json_extract(j.value, '$.severity'), 'info')`);
3518
+ const reviewers = this.db.prepare(
3519
+ `SELECT r.model, r.provider, r.gezel_name, COUNT(DISTINCT r.content_hash) AS files
3520
+ ${FROM}
3521
+ GROUP BY r.model, r.provider, r.gezel_name
3522
+ ORDER BY files DESC LIMIT 5`
3523
+ ).all(...params).map((r) => ({
3524
+ model: r.model,
3525
+ provider: r.provider,
3526
+ gezelName: r.gezel_name,
3527
+ files: Number(r.files)
3528
+ }));
3239
3529
  return {
3240
3530
  issues: rows.filter((r) => r.message).map((r) => ({
3241
3531
  path: r.path,
@@ -3248,18 +3538,34 @@ var IndexStore = class _IndexStore {
3248
3538
  total: Object.values(bySeverity).reduce((a, b) => a + b, 0),
3249
3539
  bySeverity,
3250
3540
  byCategory: tally(`COALESCE(json_extract(j.value, '$.category'), 'general')`)
3251
- }
3541
+ },
3542
+ reviewers
3252
3543
  };
3253
3544
  }
3254
3545
  // ── area summaries (deep pass) ─────────────────────────────────────────
3255
3546
  upsertAreaSummary(rec) {
3256
3547
  this.db.prepare(
3257
- `INSERT INTO area_summaries (collection_id, area_path, input_hash, summary_md, model, created_at)
3258
- VALUES (?, ?, ?, ?, ?, ?)
3548
+ `INSERT INTO area_summaries (collection_id, area_path, input_hash, summary_md, model,
3549
+ provider, gezel_id, gezel_name, app_version, created_at)
3550
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3259
3551
  ON CONFLICT(collection_id, area_path) DO UPDATE SET
3260
3552
  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());
3553
+ model=excluded.model,
3554
+ provider=excluded.provider, gezel_id=excluded.gezel_id,
3555
+ gezel_name=excluded.gezel_name, app_version=excluded.app_version,
3556
+ created_at=excluded.created_at`
3557
+ ).run(
3558
+ this.collectionId,
3559
+ rec.areaPath,
3560
+ rec.inputHash,
3561
+ rec.summaryMd,
3562
+ rec.model,
3563
+ rec.provenance?.provider ?? null,
3564
+ rec.provenance?.gezelId ?? null,
3565
+ rec.provenance?.gezelName ?? null,
3566
+ rec.provenance?.appVersion ?? null,
3567
+ nowIso()
3568
+ );
3263
3569
  }
3264
3570
  getAreaSummary(areaPath) {
3265
3571
  const row = this.db.prepare(
@@ -4025,7 +4331,7 @@ function parseBindings(raw) {
4025
4331
 
4026
4332
  // src/index-store/symbols.ts
4027
4333
  import { createRequire as createRequire3 } from "module";
4028
- import { dirname as dirname6, join as join8 } from "path";
4334
+ import { dirname as dirname7, join as join9 } from "path";
4029
4335
  var nodeRequire2 = createRequire3(import.meta.url);
4030
4336
  var SIG_CAP = 160;
4031
4337
  var GRAMMAR_FILE = {
@@ -4098,8 +4404,8 @@ async function ensureInit() {
4098
4404
  initPromise = (async () => {
4099
4405
  try {
4100
4406
  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) });
4407
+ const wtsDir = dirname7(nodeRequire2.resolve("web-tree-sitter"));
4408
+ await mod.Parser.init({ locateFile: (f) => join9(wtsDir, f) });
4103
4409
  ParserCtor = mod.Parser;
4104
4410
  LanguageMod = mod.Language;
4105
4411
  return true;
@@ -4110,7 +4416,7 @@ async function ensureInit() {
4110
4416
  return initPromise;
4111
4417
  }
4112
4418
  function grammarDir() {
4113
- return join8(dirname6(nodeRequire2.resolve("tree-sitter-wasms/package.json")), "out");
4419
+ return join9(dirname7(nodeRequire2.resolve("tree-sitter-wasms/package.json")), "out");
4114
4420
  }
4115
4421
  async function loadLanguage(langId) {
4116
4422
  const file = GRAMMAR_FILE[langId];
@@ -4118,7 +4424,7 @@ async function loadLanguage(langId) {
4118
4424
  if (langCache.has(langId)) return langCache.get(langId) ?? null;
4119
4425
  let lang = null;
4120
4426
  try {
4121
- lang = await LanguageMod.load(join8(grammarDir(), file));
4427
+ lang = await LanguageMod.load(join9(grammarDir(), file));
4122
4428
  } catch {
4123
4429
  lang = null;
4124
4430
  }
@@ -4387,7 +4693,7 @@ function firstLine(text) {
4387
4693
 
4388
4694
  // src/index-store/content-indexer.ts
4389
4695
  var MAX_FILES = 5e4;
4390
- var EXTRACTOR_VERSION = 2;
4696
+ var EXTRACTOR_VERSION = 3;
4391
4697
  var INDEXED_PNG_TEXT_KEYS = ["parameters", "prompt", "Description", "Comment", "Title"];
4392
4698
  var MAX_INDEXED_PNG_TEXT = 2e3;
4393
4699
  function embeddablePngText(text) {
@@ -4402,7 +4708,7 @@ function embeddablePngText(text) {
4402
4708
  }
4403
4709
  return parts.length > 0 ? ` ${parts.join(" ")}` : "";
4404
4710
  }
4405
- async function indexWorkspaceContent(store, workspaceDir) {
4711
+ async function indexWorkspaceContent(store, workspaceDir, artifactsDir) {
4406
4712
  const stats = {
4407
4713
  scanned: 0,
4408
4714
  changed: 0,
@@ -4490,6 +4796,39 @@ async function indexWorkspaceContent(store, workspaceDir) {
4490
4796
  ]);
4491
4797
  continue;
4492
4798
  }
4799
+ if (cls.modality === "audio") {
4800
+ let bytes;
4801
+ try {
4802
+ bytes = await readFile3(file.abs);
4803
+ } catch {
4804
+ stats.skipped++;
4805
+ continue;
4806
+ }
4807
+ const hash2 = sha256(bytes);
4808
+ const record = {
4809
+ path: file.path,
4810
+ hash: hash2,
4811
+ size: file.size,
4812
+ mtimeMs: file.mtimeMs,
4813
+ lang: cls.lang,
4814
+ kind: cls.kind,
4815
+ modality: cls.modality,
4816
+ trivial: cls.trivial,
4817
+ indexedAt,
4818
+ loc: null
4819
+ };
4820
+ if (existing && existing.hash === hash2) {
4821
+ store.upsertFile(record);
4822
+ continue;
4823
+ }
4824
+ stats.changed++;
4825
+ store.upsertFile(record);
4826
+ const nameWords = file.path.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).join(" ");
4827
+ store.putChunks(file.path, hash2, [
4828
+ { kind: "audio", lineStart: 1, lineEnd: 1, text: `${nameWords} audio recording` }
4829
+ ]);
4830
+ continue;
4831
+ }
4493
4832
  if (cls.modality === "doc") {
4494
4833
  let bytes;
4495
4834
  try {
@@ -4511,21 +4850,32 @@ async function indexWorkspaceContent(store, workspaceDir) {
4511
4850
  indexedAt,
4512
4851
  loc: null
4513
4852
  };
4853
+ const convertible = isConvertibleDoc(extname3(file.path));
4514
4854
  if (existing && existing.hash === hash2) {
4515
4855
  store.upsertFile(record);
4856
+ if (!convertible) continue;
4857
+ const paths = shadowDocFilesPaths(artifactsDir, file.path);
4858
+ const sidecarPresent = paths ? await stat3(paths.mdPath).then(
4859
+ (s) => s.isFile(),
4860
+ () => false
4861
+ ) : true;
4862
+ if (sidecarPresent) continue;
4863
+ const state = store.getMetadata(file.path)["doc:convert"];
4864
+ if (state && state !== "ok") continue;
4865
+ const conv = await ensureShadowDocSidecar(file.abs, artifactsDir, file.path);
4866
+ store.setMetadata(file.path, [{ key: "doc:convert", value: convState(conv) }]);
4867
+ if (conv?.markdown != null) stats.docsConverted++;
4516
4868
  continue;
4517
4869
  }
4518
4870
  stats.changed++;
4519
4871
  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) {
4872
+ if (convertible) {
4873
+ const conv = await ensureShadowDocSidecar(file.abs, artifactsDir, file.path);
4874
+ store.setMetadata(file.path, [{ key: "doc:convert", value: convState(conv) }]);
4875
+ if (conv?.markdown != null) {
4526
4876
  store.putChunks(file.path, hash2, chunkMarkdown(conv.markdown));
4527
4877
  stats.docsConverted++;
4528
- } else if (conv.blocked) {
4878
+ } else if (conv?.blocked) {
4529
4879
  store.putChunks(file.path, hash2, [
4530
4880
  {
4531
4881
  kind: "doc",
@@ -4604,11 +4954,51 @@ async function indexWorkspaceContent(store, workspaceDir) {
4604
4954
  if (!seen.has(f.path)) {
4605
4955
  store.deleteFile(f.path);
4606
4956
  stats.removed++;
4957
+ if (isConvertibleDoc(extname3(f.path))) {
4958
+ const paths = shadowDocFilesPaths(artifactsDir, f.path);
4959
+ if (paths) await rm4(paths.dir, { recursive: true, force: true }).catch(() => {
4960
+ });
4961
+ }
4607
4962
  }
4608
4963
  }
4964
+ await sweepShadowOrphans(artifactsDir, seen);
4609
4965
  if (forceCode) store.setMeta("extractor_version", String(EXTRACTOR_VERSION));
4610
4966
  return stats;
4611
4967
  }
4968
+ var SHADOW_SWEEP_MAX_DIRS = 5e4;
4969
+ async function sweepShadowOrphans(artifactsDir, seen) {
4970
+ const shadowRoot = join10(artifactsDir, PROJECT_SHADOW_DIR_NAME2);
4971
+ const queue = [""];
4972
+ let visited = 0;
4973
+ while (queue.length > 0) {
4974
+ const rel = queue.shift();
4975
+ let entries;
4976
+ try {
4977
+ entries = await readdir2(join10(shadowRoot, rel), { withFileTypes: true });
4978
+ } catch {
4979
+ continue;
4980
+ }
4981
+ for (const e of entries) {
4982
+ if (!e.isDirectory()) continue;
4983
+ if (++visited > SHADOW_SWEEP_MAX_DIRS) return;
4984
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
4985
+ if (e.name.endsWith("_files")) {
4986
+ const sourceRel = childRel.slice(0, -"_files".length);
4987
+ if (!seen.has(sourceRel)) {
4988
+ await rm4(join10(shadowRoot, childRel), { recursive: true, force: true }).catch(() => {
4989
+ });
4990
+ }
4991
+ } else {
4992
+ queue.push(childRel);
4993
+ }
4994
+ }
4995
+ }
4996
+ }
4997
+ function convState(conv) {
4998
+ if (conv?.markdown != null) return "ok";
4999
+ if (conv?.blocked) return "blocked";
5000
+ return "failed";
5001
+ }
4612
5002
 
4613
5003
  // src/index-store/static-index-worker.ts
4614
5004
  if (!parentPort) throw new Error("static-index-worker must be run as a worker thread");
@@ -4624,7 +5014,11 @@ port.on("message", (request) => {
4624
5014
  });
4625
5015
  if (!index) throw new Error(`content index unavailable at ${request.dbPath}`);
4626
5016
  try {
4627
- const stats = await indexWorkspaceContent(index, request.workspaceDir);
5017
+ const stats = await indexWorkspaceContent(
5018
+ index,
5019
+ request.workspaceDir,
5020
+ request.artifactsDir
5021
+ );
4628
5022
  port.postMessage({ id: request.id, stats });
4629
5023
  } finally {
4630
5024
  index.close();