squadrant 0.27.0 → 0.27.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.
@@ -9408,16 +9408,275 @@ var init_paths = __esm({
9408
9408
  }
9409
9409
  });
9410
9410
 
9411
- // packages/core/dist/knowledge/store.js
9411
+ // packages/core/dist/knowledge/ingest.js
9412
+ import crypto3 from "crypto";
9412
9413
  import fs15 from "fs";
9413
9414
  import os7 from "os";
9414
9415
  import path16 from "path";
9416
+ import { execFile as execFile2 } from "child_process";
9417
+ import { promisify as promisify2 } from "util";
9418
+ function refFor(kbRoot, file) {
9419
+ const rel = path16.relative(kbRoot, file);
9420
+ if (rel && !rel.startsWith("..") && !path16.isAbsolute(rel))
9421
+ return rel.split(path16.sep).join("/");
9422
+ const home = os7.homedir();
9423
+ return file.startsWith(home + path16.sep) ? `~/${path16.relative(home, file).split(path16.sep).join("/")}` : file;
9424
+ }
9425
+ function readState2(kbRoot, ref) {
9426
+ try {
9427
+ return JSON.parse(fs15.readFileSync(stampFile(kbRoot, ref), "utf8"));
9428
+ } catch {
9429
+ return null;
9430
+ }
9431
+ }
9432
+ function writeState(kbRoot, st) {
9433
+ fs15.mkdirSync(path16.join(kbRoot, CONVERTED_DIR), { recursive: true });
9434
+ fs15.writeFileSync(stampFile(kbRoot, st.ref), JSON.stringify(st) + "\n");
9435
+ }
9436
+ function globToRegExp(glob) {
9437
+ let re = "";
9438
+ for (let i = 0; i < glob.length; i++) {
9439
+ const c = glob[i];
9440
+ if (c === "*") {
9441
+ if (glob[i + 1] === "*") {
9442
+ re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
9443
+ i += glob[i + 2] === "/" ? 2 : 1;
9444
+ } else
9445
+ re += "[^/]*";
9446
+ } else if (c === "?")
9447
+ re += "[^/]";
9448
+ else
9449
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
9450
+ }
9451
+ return new RegExp(`^${re}$`);
9452
+ }
9453
+ function walkFiles(dir, out = []) {
9454
+ if (!fs15.existsSync(dir))
9455
+ return out;
9456
+ for (const ent of fs15.readdirSync(dir, { withFileTypes: true })) {
9457
+ if (ent.name === ".git" || ent.name === CONVERTED_DIR || ent.name === "node_modules")
9458
+ continue;
9459
+ const full = path16.join(dir, ent.name);
9460
+ if (ent.isDirectory())
9461
+ walkFiles(full, out);
9462
+ else if (ent.isFile())
9463
+ out.push(full);
9464
+ }
9465
+ return out;
9466
+ }
9467
+ function expandSource(kbRoot, entry) {
9468
+ const abs = path16.resolve(kbRoot, expandHome(entry.path));
9469
+ if (!/[*?]/.test(entry.path))
9470
+ return fs15.existsSync(abs) && fs15.statSync(abs).isFile() ? [abs] : [];
9471
+ const base = abs.split(/[*?]/)[0];
9472
+ const root = base.endsWith(path16.sep) ? base : path16.dirname(base);
9473
+ const re = globToRegExp(abs.split(path16.sep).join("/"));
9474
+ return walkFiles(root).filter((f) => re.test(f.split(path16.sep).join("/"))).sort();
9475
+ }
9476
+ function detectChange(kbRoot, file) {
9477
+ const ref = refFor(kbRoot, file);
9478
+ const st = fs15.statSync(file);
9479
+ const prior = readState2(kbRoot, ref);
9480
+ if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
9481
+ return { ref, changed: false, stamp: { sha: prior.sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
9482
+ }
9483
+ const sha = sha256(file);
9484
+ return { ref, changed: !prior || prior.sha !== sha, stamp: { sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
9485
+ }
9486
+ function splitSections(text) {
9487
+ const cuts = [];
9488
+ const re = /^#{1,6}[ \t]+(.+)$/gm;
9489
+ for (let m = re.exec(text); m; m = re.exec(text))
9490
+ cuts.push({ at: m.index, heading: m[1].trim() });
9491
+ if (!cuts.length || cuts[0].at > 0)
9492
+ cuts.unshift({ at: 0 });
9493
+ const spans = [];
9494
+ cuts.forEach((c, i) => {
9495
+ const end = i + 1 < cuts.length ? cuts[i + 1].at : text.length;
9496
+ let start = c.at;
9497
+ while (end - start > SECTION_TARGET_CHARS * 1.5) {
9498
+ const brk = text.indexOf("\n\n", start + SECTION_TARGET_CHARS);
9499
+ if (brk < 0 || brk >= end)
9500
+ break;
9501
+ spans.push({ index: 0, start, end: brk, heading: c.heading });
9502
+ start = brk + 2;
9503
+ }
9504
+ if (text.slice(start, end).trim())
9505
+ spans.push({ index: 0, start, end, heading: c.heading });
9506
+ });
9507
+ return spans.map((s, i) => ({ ...s, index: i }));
9508
+ }
9509
+ async function planIngest(kbRoot, opts) {
9510
+ const converter = opts.converter ?? markitdownConverter;
9511
+ const cap = opts.maxSectionsPerPass ?? DEFAULT_MAX_SECTIONS_PER_PASS;
9512
+ const plan = { sources: [], skipped: [], failed: [], carriedSections: 0, sectionCount: 0, estimatedTokens: 0, existing: opts.existing ?? [] };
9513
+ let budget = cap;
9514
+ const seen = /* @__PURE__ */ new Set();
9515
+ for (const entry of opts.sources) {
9516
+ for (const file of expandSource(kbRoot, entry)) {
9517
+ const { ref, changed, stamp, prior } = detectChange(kbRoot, file);
9518
+ if (seen.has(ref))
9519
+ continue;
9520
+ seen.add(ref);
9521
+ const pending = !!prior && prior.cursor < prior.sections;
9522
+ if (!changed && !pending)
9523
+ continue;
9524
+ if (entry.sensitivity === "local-only" && !opts.localModel) {
9525
+ plan.skipped.push({ ref, reason: "sensitivity: local-only and no local model available" });
9526
+ continue;
9527
+ }
9528
+ if (budget <= 0) {
9529
+ plan.carriedSections += changed ? 1 : prior.sections - prior.cursor;
9530
+ continue;
9531
+ }
9532
+ let text;
9533
+ const conv = convertedFile(kbRoot, ref);
9534
+ try {
9535
+ if (!changed && fs15.existsSync(conv))
9536
+ text = fs15.readFileSync(conv, "utf8");
9537
+ else {
9538
+ text = PASS_THROUGH.has(path16.extname(file).toLowerCase()) ? fs15.readFileSync(file, "utf8") : await converter(file);
9539
+ if (!opts.dryRun) {
9540
+ fs15.mkdirSync(path16.dirname(conv), { recursive: true });
9541
+ fs15.writeFileSync(conv, text);
9542
+ }
9543
+ }
9544
+ } catch (e) {
9545
+ plan.failed.push({ ref, error: e.message.split("\n")[0] });
9546
+ continue;
9547
+ }
9548
+ const all = splitSections(text);
9549
+ const from = changed ? 0 : prior.cursor;
9550
+ const take = all.slice(from, from + budget);
9551
+ budget -= take.length;
9552
+ plan.carriedSections += all.length - from - take.length;
9553
+ plan.sectionCount += take.length;
9554
+ plan.estimatedTokens += Math.ceil(take.reduce((n, s) => n + (s.end - s.start), 0) / CHARS_PER_TOKEN);
9555
+ plan.sources.push({
9556
+ ref,
9557
+ file,
9558
+ converted: conv,
9559
+ sha: stamp.sha,
9560
+ priority: entry.priority,
9561
+ domain: entry.domain,
9562
+ sensitivity: entry.sensitivity,
9563
+ sections: take,
9564
+ totalSections: all.length
9565
+ });
9566
+ if (!opts.dryRun)
9567
+ writeState(kbRoot, { ref, ...stamp, sections: all.length, cursor: from + take.length, priority: entry.priority });
9568
+ }
9569
+ }
9570
+ return plan;
9571
+ }
9572
+ var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome, fileForRef, cacheKey, convertedFile, stampFile, sha256;
9573
+ var init_ingest = __esm({
9574
+ "packages/core/dist/knowledge/ingest.js"() {
9575
+ execFileAsync = promisify2(execFile2);
9576
+ CONVERTED_DIR = ".converted";
9577
+ DEFAULT_MAX_SECTIONS_PER_PASS = 40;
9578
+ CHARS_PER_TOKEN = 4;
9579
+ PASS_THROUGH = /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]);
9580
+ SECTION_TARGET_CHARS = 4e3;
9581
+ markitdownConverter = async (file) => {
9582
+ const { stdout } = await execFileAsync("markitdown", [file], { maxBuffer: 64 * 1024 * 1024 });
9583
+ return stdout;
9584
+ };
9585
+ expandHome = (p) => p === "~" || p.startsWith("~/") ? path16.join(os7.homedir(), p.slice(1)) : p;
9586
+ fileForRef = (kbRoot, ref) => path16.resolve(kbRoot, expandHome(ref));
9587
+ cacheKey = (ref) => `${ref.replace(/[^A-Za-z0-9._-]+/g, "_")}.${crypto3.createHash("sha1").update(ref).digest("hex").slice(0, 6)}`;
9588
+ convertedFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.md`);
9589
+ stampFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.sha`);
9590
+ sha256 = (file) => crypto3.createHash("sha256").update(fs15.readFileSync(file)).digest("hex");
9591
+ }
9592
+ });
9593
+
9594
+ // packages/core/dist/knowledge/verify.js
9595
+ function normalizeWithMap(src, keepBreakHyphen = false) {
9596
+ const out = [];
9597
+ const map2 = [];
9598
+ const push = (s, at) => {
9599
+ for (const ch of s) {
9600
+ out.push(ch);
9601
+ map2.push(at);
9602
+ }
9603
+ };
9604
+ let i = 0;
9605
+ while (i < src.length) {
9606
+ const ch = src[i];
9607
+ if (ch === "-" && /^-[ \t]*\r?\n[ \t]*[a-z]/.test(src.slice(i, i + 40)) && i > 0 && /[A-Za-z]/.test(src[i - 1])) {
9608
+ if (keepBreakHyphen)
9609
+ push("-", i);
9610
+ i++;
9611
+ while (/\s/.test(src[i]))
9612
+ i++;
9613
+ continue;
9614
+ }
9615
+ if (/\s/.test(ch)) {
9616
+ const start = i;
9617
+ while (i < src.length && /\s/.test(src[i]))
9618
+ i++;
9619
+ if (out.length && out[out.length - 1] !== " ")
9620
+ push(" ", start);
9621
+ continue;
9622
+ }
9623
+ if (MARKUP.has(ch)) {
9624
+ i++;
9625
+ continue;
9626
+ }
9627
+ if (LIGATURES[ch])
9628
+ push(LIGATURES[ch], i);
9629
+ else if (QUOTES[ch])
9630
+ push(QUOTES[ch], i);
9631
+ else if (DASHES.has(ch))
9632
+ push("-", i);
9633
+ else
9634
+ push(ch, i);
9635
+ i++;
9636
+ }
9637
+ while (out.length && out[out.length - 1] === " ") {
9638
+ out.pop();
9639
+ map2.pop();
9640
+ }
9641
+ return { text: out.join(""), map: map2 };
9642
+ }
9643
+ function findQuote(source, quote) {
9644
+ const needle = normalizeText(quote);
9645
+ if (!needle)
9646
+ return null;
9647
+ const views = typeof source === "string" ? normalizedViews(source) : source;
9648
+ for (const hay of views) {
9649
+ const at = hay.text.indexOf(needle);
9650
+ if (at >= 0)
9651
+ return [hay.map[at], hay.map[at + needle.length - 1] + 1];
9652
+ }
9653
+ return null;
9654
+ }
9655
+ var LIGATURES, QUOTES, DASHES, MARKUP, normalizeText, normalizedViews, DOCLING_DROP_THRESHOLD, needsDocling, spansOverlap;
9656
+ var init_verify = __esm({
9657
+ "packages/core/dist/knowledge/verify.js"() {
9658
+ LIGATURES = { "\uFB01": "fi", "\uFB02": "fl", "\uFB00": "ff", "\uFB03": "ffi", "\uFB04": "ffl", "\uFB05": "st", "\uFB06": "st" };
9659
+ QUOTES = { "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"' };
9660
+ DASHES = /* @__PURE__ */ new Set(["\u2010", "\u2011", "\u2012", "\u2013", "\u2014", "\u2015", "\u2212"]);
9661
+ MARKUP = /* @__PURE__ */ new Set(["*", "_", "`", "\\"]);
9662
+ normalizeText = (s) => normalizeWithMap(s).text;
9663
+ normalizedViews = (src) => [normalizeWithMap(src), normalizeWithMap(src, true)];
9664
+ DOCLING_DROP_THRESHOLD = 0.2;
9665
+ needsDocling = (dropped, total) => total > 0 && dropped / total > DOCLING_DROP_THRESHOLD;
9666
+ spansOverlap = (a, b) => a[0] < b[1] && b[0] < a[1];
9667
+ }
9668
+ });
9669
+
9670
+ // packages/core/dist/knowledge/store.js
9671
+ import fs16 from "fs";
9672
+ import os8 from "os";
9673
+ import path17 from "path";
9415
9674
  import matter2 from "gray-matter";
9416
9675
  function duplicateIdProblem(id, ignored, used) {
9417
9676
  return `duplicate id ${id}: ${ignored} is ignored, ${used} is used (live files win over _proposed, then path order). Fix: rename the id in one file, or delete or move one of them.`;
9418
9677
  }
9419
9678
  function isPendingProposalDup(e, rulesDir) {
9420
- return e.problems.every((p) => p.startsWith("duplicate id")) && path16.relative(rulesDir, e.file).split(path16.sep).includes(PROPOSED_DIR);
9679
+ return e.problems.every((p) => p.startsWith("duplicate id")) && path17.relative(rulesDir, e.file).split(path17.sep).includes(PROPOSED_DIR);
9421
9680
  }
9422
9681
  function splitStatement(body) {
9423
9682
  const first = body.trim().split(/\n\s*\n/)[0] ?? "";
@@ -9426,8 +9685,8 @@ function splitStatement(body) {
9426
9685
  function listMarkdown(dir, includeProposed) {
9427
9686
  const out = [];
9428
9687
  const walk = (d) => {
9429
- for (const ent of fs15.readdirSync(d, { withFileTypes: true })) {
9430
- const full = path16.join(d, ent.name);
9688
+ for (const ent of fs16.readdirSync(d, { withFileTypes: true })) {
9689
+ const full = path17.join(d, ent.name);
9431
9690
  if (ent.isDirectory()) {
9432
9691
  if (ent.name === PROPOSED_DIR && !includeProposed)
9433
9692
  continue;
@@ -9438,11 +9697,11 @@ function listMarkdown(dir, includeProposed) {
9438
9697
  }
9439
9698
  };
9440
9699
  walk(dir);
9441
- const isProposed = (f) => path16.relative(dir, f).split(path16.sep).includes(PROPOSED_DIR);
9700
+ const isProposed = (f) => path17.relative(dir, f).split(path17.sep).includes(PROPOSED_DIR);
9442
9701
  return out.sort((a, b) => Number(isProposed(a)) - Number(isProposed(b)) || (a < b ? -1 : a > b ? 1 : 0));
9443
9702
  }
9444
9703
  function loadRulesDir(dir, layer, opts = {}) {
9445
- if (!fs15.existsSync(dir))
9704
+ if (!fs16.existsSync(dir))
9446
9705
  return { rules: [], errors: [] };
9447
9706
  const rules = [];
9448
9707
  const errors = [];
@@ -9450,7 +9709,7 @@ function loadRulesDir(dir, layer, opts = {}) {
9450
9709
  for (const file of listMarkdown(dir, opts.includeProposed ?? false)) {
9451
9710
  let parsed;
9452
9711
  try {
9453
- parsed = matter2(fs15.readFileSync(file, "utf8"), NO_JS_FRONTMATTER);
9712
+ parsed = matter2(fs16.readFileSync(file, "utf8"), NO_JS_FRONTMATTER);
9454
9713
  } catch (e) {
9455
9714
  errors.push({ file, problems: [`frontmatter parse error: ${e.message}`] });
9456
9715
  continue;
@@ -9474,14 +9733,14 @@ function loadRulesDir(dir, layer, opts = {}) {
9474
9733
  function resolveSourceRef(ref, kbRoot, sources) {
9475
9734
  const bases = /* @__PURE__ */ new Set([kbRoot]);
9476
9735
  for (const s of sources) {
9477
- let d = path16.resolve(kbRoot, expandHome(s.path.split(/[*?[{]/)[0]));
9478
- for (let i = 0; i < 6 && d !== path16.dirname(d); i++) {
9479
- d = path16.dirname(d);
9736
+ let d = path17.resolve(kbRoot, expandHome2(s.path.split(/[*?[{]/)[0]));
9737
+ for (let i = 0; i < 6 && d !== path17.dirname(d); i++) {
9738
+ d = path17.dirname(d);
9480
9739
  bases.add(d);
9481
9740
  }
9482
9741
  }
9483
- const cands = [path16.resolve(kbRoot, expandHome(ref)), ...[...bases].map((b) => path16.join(b, ref))];
9484
- return cands.find((c) => fs15.existsSync(c) && fs15.statSync(c).isFile());
9742
+ const cands = [path17.resolve(kbRoot, expandHome2(ref)), ...[...bases].map((b) => path17.join(b, ref))];
9743
+ return cands.find((c) => fs16.existsSync(c) && fs16.statSync(c).isFile());
9485
9744
  }
9486
9745
  function validateKb(opts) {
9487
9746
  const { rules, errors } = loadRulesDir(opts.rulesDir, "kb", { includeProposed: true });
@@ -9489,7 +9748,7 @@ function validateKb(opts) {
9489
9748
  const dups = errors.filter((e) => e.problems.some((p) => p.startsWith("duplicate id")) && !isPendingProposalDup(e, opts.rulesDir));
9490
9749
  const pendingDups = errors.filter((e) => isPendingProposalDup(e, opts.rulesDir)).length;
9491
9750
  const invalid = errors.filter((e) => !dups.includes(e) && !isPendingProposalDup(e, opts.rulesDir));
9492
- const proposed = rules.filter((r) => path16.relative(opts.rulesDir, r.file).split(path16.sep).includes(PROPOSED_DIR)).length;
9751
+ const proposed = rules.filter((r) => path17.relative(opts.rulesDir, r.file).split(path17.sep).includes(PROPOSED_DIR)).length;
9493
9752
  checks.push({ level: "ok", message: `${rules.length + errors.length} rule files read (${rules.length - proposed} live, ${proposed + pendingDups} proposed parsed OK)` });
9494
9753
  if (invalid.length)
9495
9754
  for (const e of invalid)
@@ -9502,7 +9761,7 @@ function validateKb(opts) {
9502
9761
  else
9503
9762
  checks.push({ level: "ok", message: "no duplicate ids" });
9504
9763
  const cache = /* @__PURE__ */ new Map();
9505
- let verified = 0, unresolved = 0;
9764
+ let verified = 0, unresolved = 0, notIngested = 0;
9506
9765
  for (const r of rules) {
9507
9766
  r.sources.forEach((src, i) => {
9508
9767
  const file = resolveSourceRef(src.ref, opts.kbRoot, opts.sources);
@@ -9510,16 +9769,28 @@ function validateKb(opts) {
9510
9769
  unresolved++;
9511
9770
  return;
9512
9771
  }
9513
- if (!cache.has(file))
9514
- cache.set(file, fs15.readFileSync(file, "utf8"));
9515
- if (cache.get(file).includes(src.quote))
9772
+ const key = `${src.ref}\0${file}`;
9773
+ if (!cache.has(key)) {
9774
+ const conv = readState2(opts.kbRoot, src.ref) ? convertedFile(opts.kbRoot, src.ref) : null;
9775
+ const cached = !!conv && fs16.existsSync(conv);
9776
+ cache.set(key, {
9777
+ hay: normalizedViews(fs16.readFileSync(cached ? conv : file, "utf8")),
9778
+ convertible: !cached && !PASS_THROUGH.has(path17.extname(file).toLowerCase())
9779
+ });
9780
+ }
9781
+ const { hay, convertible } = cache.get(key);
9782
+ if (findQuote(hay, src.quote))
9516
9783
  verified++;
9784
+ else if (convertible)
9785
+ notIngested++;
9517
9786
  else
9518
9787
  checks.push({ level: "fail", message: `quote not found in ${file}: ${r.id} sources[${i}] (${r.file})` });
9519
9788
  });
9520
9789
  }
9521
9790
  if (!checks.some((c) => c.message.startsWith("quote not found")))
9522
9791
  checks.push({ level: "ok", message: `${verified} source quotes found in their source files` });
9792
+ if (notIngested)
9793
+ checks.push({ level: "skip", message: `${notIngested} source refs need conversion but have no .converted cache, quote not checked (run \`squadrant knowledge ingest <kb>\`)` });
9523
9794
  if (unresolved)
9524
9795
  checks.push({ level: "skip", message: `${unresolved} source refs not resolvable on this machine, quote not checked` });
9525
9796
  if (opts.domains) {
@@ -9530,22 +9801,24 @@ function validateKb(opts) {
9530
9801
  }
9531
9802
  return { checks, ok: !checks.some((c) => c.level === "fail") };
9532
9803
  }
9533
- var NO_JS_FRONTMATTER, expandHome;
9804
+ var NO_JS_FRONTMATTER, expandHome2;
9534
9805
  var init_store2 = __esm({
9535
9806
  "packages/core/dist/knowledge/store.js"() {
9536
9807
  init_dist();
9808
+ init_ingest();
9537
9809
  init_paths();
9810
+ init_verify();
9538
9811
  NO_JS_FRONTMATTER = {
9539
9812
  engines: { javascript: { parse: () => {
9540
9813
  throw new Error("only YAML front matter is allowed");
9541
9814
  } } }
9542
9815
  };
9543
- expandHome = (p) => p === "~" || p.startsWith("~/") ? path16.join(os7.homedir(), p.slice(1)) : p;
9816
+ expandHome2 = (p) => p === "~" || p.startsWith("~/") ? path17.join(os8.homedir(), p.slice(1)) : p;
9544
9817
  }
9545
9818
  });
9546
9819
 
9547
9820
  // packages/core/dist/knowledge/layers.js
9548
- import fs16 from "fs";
9821
+ import fs17 from "fs";
9549
9822
  function loadKbRules(cfg, kb, opts = {}) {
9550
9823
  return loadRulesDir(kbRulesDir(cfg, kb), `kb:${kb}`, opts);
9551
9824
  }
@@ -9557,7 +9830,7 @@ function resolveProjectRules(cfg, project, opts = {}) {
9557
9830
  const errors = [];
9558
9831
  const warnings = [];
9559
9832
  for (const kb of subscribedKbs(cfg, project)) {
9560
- if (!fs16.existsSync(kbDir(cfg, kb))) {
9833
+ if (!fs17.existsSync(kbDir(cfg, kb))) {
9561
9834
  warnings.push(`knowledge base '${kb}' not found (run: squadrant knowledge init ${kb})`);
9562
9835
  continue;
9563
9836
  }
@@ -9595,8 +9868,8 @@ var init_layers = __esm({
9595
9868
  });
9596
9869
 
9597
9870
  // packages/core/dist/knowledge/index-file.js
9598
- import fs17 from "fs";
9599
- import path17 from "path";
9871
+ import fs18 from "fs";
9872
+ import path18 from "path";
9600
9873
  function compileIndex(kb, rules, now = /* @__PURE__ */ new Date()) {
9601
9874
  return {
9602
9875
  kb,
@@ -9605,18 +9878,18 @@ function compileIndex(kb, rules, now = /* @__PURE__ */ new Date()) {
9605
9878
  };
9606
9879
  }
9607
9880
  function writeIndex(kbRoot, index) {
9608
- const file = path17.join(kbRoot, "index.json");
9881
+ const file = path18.join(kbRoot, "index.json");
9609
9882
  const tmp = `${file}.tmp-${process.pid}`;
9610
- fs17.writeFileSync(tmp, JSON.stringify(index, null, 2) + "\n");
9611
- fs17.renameSync(tmp, file);
9883
+ fs18.writeFileSync(tmp, JSON.stringify(index, null, 2) + "\n");
9884
+ fs18.renameSync(tmp, file);
9612
9885
  return file;
9613
9886
  }
9614
9887
  function readIndex(kbRoot, kb) {
9615
- const file = path17.join(kbRoot, "index.json");
9616
- if (!fs17.existsSync(file))
9888
+ const file = path18.join(kbRoot, "index.json");
9889
+ if (!fs18.existsSync(file))
9617
9890
  return { index: null, problem: `index.json missing for kb '${kb}'` };
9618
9891
  try {
9619
- const index = JSON.parse(fs17.readFileSync(file, "utf8"));
9892
+ const index = JSON.parse(fs18.readFileSync(file, "utf8"));
9620
9893
  if (!Array.isArray(index.rules))
9621
9894
  return { index: null, problem: `index.json corrupt for kb '${kb}' (no rules array)` };
9622
9895
  return { index };
@@ -12711,15 +12984,15 @@ var init_js_yaml = __esm({
12711
12984
  });
12712
12985
 
12713
12986
  // packages/core/dist/knowledge/sources.js
12714
- import fs18 from "fs";
12715
- import path18 from "path";
12987
+ import fs19 from "fs";
12988
+ import path19 from "path";
12716
12989
  function loadSources(kbRoot) {
12717
- const file = path18.join(kbRoot, "sources.yaml");
12718
- if (!fs18.existsSync(file))
12990
+ const file = path19.join(kbRoot, "sources.yaml");
12991
+ if (!fs19.existsSync(file))
12719
12992
  return { sources: [], errors: [] };
12720
12993
  let data;
12721
12994
  try {
12722
- data = yaml.load(fs18.readFileSync(file, "utf8"));
12995
+ data = yaml.load(fs19.readFileSync(file, "utf8"));
12723
12996
  } catch (e) {
12724
12997
  return { sources: [], errors: [`YAML parse error: ${e.message.split("\n")[0]}`] };
12725
12998
  }
@@ -12757,189 +13030,6 @@ var init_sources = __esm({
12757
13030
  }
12758
13031
  });
12759
13032
 
12760
- // packages/core/dist/knowledge/ingest.js
12761
- import crypto3 from "crypto";
12762
- import fs19 from "fs";
12763
- import os8 from "os";
12764
- import path19 from "path";
12765
- import { execFile as execFile2 } from "child_process";
12766
- import { promisify as promisify2 } from "util";
12767
- function refFor(kbRoot, file) {
12768
- const rel = path19.relative(kbRoot, file);
12769
- if (rel && !rel.startsWith("..") && !path19.isAbsolute(rel))
12770
- return rel.split(path19.sep).join("/");
12771
- const home = os8.homedir();
12772
- return file.startsWith(home + path19.sep) ? `~/${path19.relative(home, file).split(path19.sep).join("/")}` : file;
12773
- }
12774
- function readState2(kbRoot, ref) {
12775
- try {
12776
- return JSON.parse(fs19.readFileSync(stampFile(kbRoot, ref), "utf8"));
12777
- } catch {
12778
- return null;
12779
- }
12780
- }
12781
- function writeState(kbRoot, st) {
12782
- fs19.mkdirSync(path19.join(kbRoot, CONVERTED_DIR), { recursive: true });
12783
- fs19.writeFileSync(stampFile(kbRoot, st.ref), JSON.stringify(st) + "\n");
12784
- }
12785
- function globToRegExp(glob) {
12786
- let re = "";
12787
- for (let i = 0; i < glob.length; i++) {
12788
- const c = glob[i];
12789
- if (c === "*") {
12790
- if (glob[i + 1] === "*") {
12791
- re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
12792
- i += glob[i + 2] === "/" ? 2 : 1;
12793
- } else
12794
- re += "[^/]*";
12795
- } else if (c === "?")
12796
- re += "[^/]";
12797
- else
12798
- re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12799
- }
12800
- return new RegExp(`^${re}$`);
12801
- }
12802
- function walkFiles(dir, out = []) {
12803
- if (!fs19.existsSync(dir))
12804
- return out;
12805
- for (const ent of fs19.readdirSync(dir, { withFileTypes: true })) {
12806
- if (ent.name === ".git" || ent.name === CONVERTED_DIR || ent.name === "node_modules")
12807
- continue;
12808
- const full = path19.join(dir, ent.name);
12809
- if (ent.isDirectory())
12810
- walkFiles(full, out);
12811
- else if (ent.isFile())
12812
- out.push(full);
12813
- }
12814
- return out;
12815
- }
12816
- function expandSource(kbRoot, entry) {
12817
- const abs = path19.resolve(kbRoot, expandHome2(entry.path));
12818
- if (!/[*?]/.test(entry.path))
12819
- return fs19.existsSync(abs) && fs19.statSync(abs).isFile() ? [abs] : [];
12820
- const base = abs.split(/[*?]/)[0];
12821
- const root = base.endsWith(path19.sep) ? base : path19.dirname(base);
12822
- const re = globToRegExp(abs.split(path19.sep).join("/"));
12823
- return walkFiles(root).filter((f) => re.test(f.split(path19.sep).join("/"))).sort();
12824
- }
12825
- function detectChange(kbRoot, file) {
12826
- const ref = refFor(kbRoot, file);
12827
- const st = fs19.statSync(file);
12828
- const prior = readState2(kbRoot, ref);
12829
- if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
12830
- return { ref, changed: false, stamp: { sha: prior.sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
12831
- }
12832
- const sha = sha256(file);
12833
- return { ref, changed: !prior || prior.sha !== sha, stamp: { sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
12834
- }
12835
- function splitSections(text) {
12836
- const cuts = [];
12837
- const re = /^#{1,6}[ \t]+(.+)$/gm;
12838
- for (let m = re.exec(text); m; m = re.exec(text))
12839
- cuts.push({ at: m.index, heading: m[1].trim() });
12840
- if (!cuts.length || cuts[0].at > 0)
12841
- cuts.unshift({ at: 0 });
12842
- const spans = [];
12843
- cuts.forEach((c, i) => {
12844
- const end = i + 1 < cuts.length ? cuts[i + 1].at : text.length;
12845
- let start = c.at;
12846
- while (end - start > SECTION_TARGET_CHARS * 1.5) {
12847
- const brk = text.indexOf("\n\n", start + SECTION_TARGET_CHARS);
12848
- if (brk < 0 || brk >= end)
12849
- break;
12850
- spans.push({ index: 0, start, end: brk, heading: c.heading });
12851
- start = brk + 2;
12852
- }
12853
- if (text.slice(start, end).trim())
12854
- spans.push({ index: 0, start, end, heading: c.heading });
12855
- });
12856
- return spans.map((s, i) => ({ ...s, index: i }));
12857
- }
12858
- async function planIngest(kbRoot, opts) {
12859
- const converter = opts.converter ?? markitdownConverter;
12860
- const cap = opts.maxSectionsPerPass ?? DEFAULT_MAX_SECTIONS_PER_PASS;
12861
- const plan = { sources: [], skipped: [], failed: [], carriedSections: 0, sectionCount: 0, estimatedTokens: 0, existing: opts.existing ?? [] };
12862
- let budget = cap;
12863
- const seen = /* @__PURE__ */ new Set();
12864
- for (const entry of opts.sources) {
12865
- for (const file of expandSource(kbRoot, entry)) {
12866
- const { ref, changed, stamp, prior } = detectChange(kbRoot, file);
12867
- if (seen.has(ref))
12868
- continue;
12869
- seen.add(ref);
12870
- const pending = !!prior && prior.cursor < prior.sections;
12871
- if (!changed && !pending)
12872
- continue;
12873
- if (entry.sensitivity === "local-only" && !opts.localModel) {
12874
- plan.skipped.push({ ref, reason: "sensitivity: local-only and no local model available" });
12875
- continue;
12876
- }
12877
- if (budget <= 0) {
12878
- plan.carriedSections += changed ? 1 : prior.sections - prior.cursor;
12879
- continue;
12880
- }
12881
- let text;
12882
- const conv = convertedFile(kbRoot, ref);
12883
- try {
12884
- if (!changed && fs19.existsSync(conv))
12885
- text = fs19.readFileSync(conv, "utf8");
12886
- else {
12887
- text = PASS_THROUGH.has(path19.extname(file).toLowerCase()) ? fs19.readFileSync(file, "utf8") : await converter(file);
12888
- if (!opts.dryRun) {
12889
- fs19.mkdirSync(path19.dirname(conv), { recursive: true });
12890
- fs19.writeFileSync(conv, text);
12891
- }
12892
- }
12893
- } catch (e) {
12894
- plan.failed.push({ ref, error: e.message.split("\n")[0] });
12895
- continue;
12896
- }
12897
- const all = splitSections(text);
12898
- const from = changed ? 0 : prior.cursor;
12899
- const take = all.slice(from, from + budget);
12900
- budget -= take.length;
12901
- plan.carriedSections += all.length - from - take.length;
12902
- plan.sectionCount += take.length;
12903
- plan.estimatedTokens += Math.ceil(take.reduce((n, s) => n + (s.end - s.start), 0) / CHARS_PER_TOKEN);
12904
- plan.sources.push({
12905
- ref,
12906
- file,
12907
- converted: conv,
12908
- sha: stamp.sha,
12909
- priority: entry.priority,
12910
- domain: entry.domain,
12911
- sensitivity: entry.sensitivity,
12912
- sections: take,
12913
- totalSections: all.length
12914
- });
12915
- if (!opts.dryRun)
12916
- writeState(kbRoot, { ref, ...stamp, sections: all.length, cursor: from + take.length, priority: entry.priority });
12917
- }
12918
- }
12919
- return plan;
12920
- }
12921
- var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome2, fileForRef, cacheKey, convertedFile, stampFile, sha256;
12922
- var init_ingest = __esm({
12923
- "packages/core/dist/knowledge/ingest.js"() {
12924
- execFileAsync = promisify2(execFile2);
12925
- CONVERTED_DIR = ".converted";
12926
- DEFAULT_MAX_SECTIONS_PER_PASS = 40;
12927
- CHARS_PER_TOKEN = 4;
12928
- PASS_THROUGH = /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]);
12929
- SECTION_TARGET_CHARS = 4e3;
12930
- markitdownConverter = async (file) => {
12931
- const { stdout } = await execFileAsync("markitdown", [file], { maxBuffer: 64 * 1024 * 1024 });
12932
- return stdout;
12933
- };
12934
- expandHome2 = (p) => p === "~" || p.startsWith("~/") ? path19.join(os8.homedir(), p.slice(1)) : p;
12935
- fileForRef = (kbRoot, ref) => path19.resolve(kbRoot, expandHome2(ref));
12936
- cacheKey = (ref) => `${ref.replace(/[^A-Za-z0-9._-]+/g, "_")}.${crypto3.createHash("sha1").update(ref).digest("hex").slice(0, 6)}`;
12937
- convertedFile = (kbRoot, ref) => path19.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.md`);
12938
- stampFile = (kbRoot, ref) => path19.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.sha`);
12939
- sha256 = (file) => crypto3.createHash("sha256").update(fs19.readFileSync(file)).digest("hex");
12940
- }
12941
- });
12942
-
12943
13033
  // packages/core/dist/knowledge/match.js
12944
13034
  import path20 from "path";
12945
13035
  function globMatches(glob, relPath) {
@@ -13357,6 +13447,13 @@ function ensureGitRepo(dir) {
13357
13447
  return false;
13358
13448
  }
13359
13449
  }
13450
+ function ensureKbGitignore(dir) {
13451
+ const file = path23.join(dir, ".gitignore");
13452
+ if (fs22.existsSync(file))
13453
+ return false;
13454
+ fs22.writeFileSync(file, KB_GITIGNORE);
13455
+ return true;
13456
+ }
13360
13457
  function findLegacyOverlays(cfg) {
13361
13458
  return Object.keys(cfg.projects).filter((p) => fs22.existsSync(legacyOverlay(cfg, p))).sort();
13362
13459
  }
@@ -13411,6 +13508,8 @@ function moveLegacyKbs(cfg, opts = {}) {
13411
13508
  }
13412
13509
  if (ensureGitRepo(to))
13413
13510
  notes.push("git init");
13511
+ if (ensureKbGitignore(to))
13512
+ notes.push("wrote .gitignore (raw/, .converted/)");
13414
13513
  if (fs22.existsSync(path23.join(to, "index.json")))
13415
13514
  notes.push(`index.json holds old paths: run squadrant knowledge reindex ${kb}`);
13416
13515
  if (action === "copied")
@@ -13418,87 +13517,17 @@ function moveLegacyKbs(cfg, opts = {}) {
13418
13517
  return { kb, from, to, action, ...notes.length ? { note: notes.join("; ") } : {} };
13419
13518
  });
13420
13519
  }
13421
- var legacyRoot, legacyOverlay;
13520
+ var legacyRoot, KB_GITIGNORE, legacyOverlay;
13422
13521
  var init_migrate = __esm({
13423
13522
  "packages/core/dist/knowledge/migrate.js"() {
13424
13523
  init_dist();
13425
13524
  init_paths();
13426
13525
  legacyRoot = (cfg) => path23.join(resolveHome(cfg.hubVault), "knowledge");
13526
+ KB_GITIGNORE = "# squadrant KB defaults: source documents stay local\nraw/\n.converted/\n";
13427
13527
  legacyOverlay = (cfg, project) => path23.join(resolveHome(cfg.projects[project].spokeVault), "knowledge", "rules");
13428
13528
  }
13429
13529
  });
13430
13530
 
13431
- // packages/core/dist/knowledge/verify.js
13432
- function normalizeWithMap(src, keepBreakHyphen = false) {
13433
- const out = [];
13434
- const map2 = [];
13435
- const push = (s, at) => {
13436
- for (const ch of s) {
13437
- out.push(ch);
13438
- map2.push(at);
13439
- }
13440
- };
13441
- let i = 0;
13442
- while (i < src.length) {
13443
- const ch = src[i];
13444
- if (ch === "-" && /^-[ \t]*\r?\n[ \t]*[a-z]/.test(src.slice(i, i + 40)) && i > 0 && /[A-Za-z]/.test(src[i - 1])) {
13445
- if (keepBreakHyphen)
13446
- push("-", i);
13447
- i++;
13448
- while (/\s/.test(src[i]))
13449
- i++;
13450
- continue;
13451
- }
13452
- if (/\s/.test(ch)) {
13453
- const start = i;
13454
- while (i < src.length && /\s/.test(src[i]))
13455
- i++;
13456
- if (out.length && out[out.length - 1] !== " ")
13457
- push(" ", start);
13458
- continue;
13459
- }
13460
- if (LIGATURES[ch])
13461
- push(LIGATURES[ch], i);
13462
- else if (QUOTES[ch])
13463
- push(QUOTES[ch], i);
13464
- else if (DASHES.has(ch))
13465
- push("-", i);
13466
- else
13467
- push(ch, i);
13468
- i++;
13469
- }
13470
- while (out.length && out[out.length - 1] === " ") {
13471
- out.pop();
13472
- map2.pop();
13473
- }
13474
- return { text: out.join(""), map: map2 };
13475
- }
13476
- function findQuote(source, quote) {
13477
- const needle = normalizeText(quote);
13478
- if (!needle)
13479
- return null;
13480
- const views = typeof source === "string" ? normalizedViews(source) : source;
13481
- for (const hay of views) {
13482
- const at = hay.text.indexOf(needle);
13483
- if (at >= 0)
13484
- return [hay.map[at], hay.map[at + needle.length - 1] + 1];
13485
- }
13486
- return null;
13487
- }
13488
- var LIGATURES, QUOTES, DASHES, normalizeText, normalizedViews, DOCLING_DROP_THRESHOLD, needsDocling, spansOverlap;
13489
- var init_verify = __esm({
13490
- "packages/core/dist/knowledge/verify.js"() {
13491
- LIGATURES = { "\uFB01": "fi", "\uFB02": "fl", "\uFB00": "ff", "\uFB03": "ffi", "\uFB04": "ffl", "\uFB05": "st", "\uFB06": "st" };
13492
- QUOTES = { "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"' };
13493
- DASHES = /* @__PURE__ */ new Set(["\u2010", "\u2011", "\u2012", "\u2013", "\u2014", "\u2015", "\u2212"]);
13494
- normalizeText = (s) => normalizeWithMap(s).text;
13495
- normalizedViews = (src) => [normalizeWithMap(src), normalizeWithMap(src, true)];
13496
- DOCLING_DROP_THRESHOLD = 0.2;
13497
- needsDocling = (dropped, total) => total > 0 && dropped / total > DOCLING_DROP_THRESHOLD;
13498
- spansOverlap = (a, b) => a[0] < b[1] && b[0] < a[1];
13499
- }
13500
- });
13501
-
13502
13531
  // packages/core/dist/knowledge/anchor.js
13503
13532
  import fs23 from "fs";
13504
13533
  import path24 from "path";
@@ -15024,6 +15053,7 @@ __export(dist_exports, {
15024
15053
  NOISY_RATE: () => NOISY_RATE,
15025
15054
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
15026
15055
  PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
15056
+ PASS_THROUGH: () => PASS_THROUGH,
15027
15057
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
15028
15058
  PROMPT_MIN_SCORE: () => PROMPT_MIN_SCORE,
15029
15059
  PROMPT_RULE_CAP: () => PROMPT_RULE_CAP,
@@ -15163,6 +15193,7 @@ __export(dist_exports, {
15163
15193
  encodeMsg: () => encodeMsg,
15164
15194
  ensureDaemon: () => ensureDaemon,
15165
15195
  ensureGitRepo: () => ensureGitRepo,
15196
+ ensureKbGitignore: () => ensureKbGitignore,
15166
15197
  ensureSocksDir: () => ensureSocksDir,
15167
15198
  escalationsFile: () => escalationsFile,
15168
15199
  evaluateCodeConflict: () => evaluateCodeConflict,