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.
package/dist/index.js CHANGED
@@ -10341,16 +10341,275 @@ var init_paths = __esm({
10341
10341
  }
10342
10342
  });
10343
10343
 
10344
- // packages/core/dist/knowledge/store.js
10344
+ // packages/core/dist/knowledge/ingest.js
10345
+ import crypto3 from "crypto";
10345
10346
  import fs15 from "fs";
10346
10347
  import os7 from "os";
10347
10348
  import path16 from "path";
10349
+ import { execFile as execFile2 } from "child_process";
10350
+ import { promisify as promisify2 } from "util";
10351
+ function refFor(kbRoot, file) {
10352
+ const rel = path16.relative(kbRoot, file);
10353
+ if (rel && !rel.startsWith("..") && !path16.isAbsolute(rel))
10354
+ return rel.split(path16.sep).join("/");
10355
+ const home = os7.homedir();
10356
+ return file.startsWith(home + path16.sep) ? `~/${path16.relative(home, file).split(path16.sep).join("/")}` : file;
10357
+ }
10358
+ function readState2(kbRoot, ref) {
10359
+ try {
10360
+ return JSON.parse(fs15.readFileSync(stampFile(kbRoot, ref), "utf8"));
10361
+ } catch {
10362
+ return null;
10363
+ }
10364
+ }
10365
+ function writeState(kbRoot, st) {
10366
+ fs15.mkdirSync(path16.join(kbRoot, CONVERTED_DIR), { recursive: true });
10367
+ fs15.writeFileSync(stampFile(kbRoot, st.ref), JSON.stringify(st) + "\n");
10368
+ }
10369
+ function globToRegExp(glob) {
10370
+ let re = "";
10371
+ for (let i = 0; i < glob.length; i++) {
10372
+ const c = glob[i];
10373
+ if (c === "*") {
10374
+ if (glob[i + 1] === "*") {
10375
+ re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
10376
+ i += glob[i + 2] === "/" ? 2 : 1;
10377
+ } else
10378
+ re += "[^/]*";
10379
+ } else if (c === "?")
10380
+ re += "[^/]";
10381
+ else
10382
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
10383
+ }
10384
+ return new RegExp(`^${re}$`);
10385
+ }
10386
+ function walkFiles(dir, out = []) {
10387
+ if (!fs15.existsSync(dir))
10388
+ return out;
10389
+ for (const ent of fs15.readdirSync(dir, { withFileTypes: true })) {
10390
+ if (ent.name === ".git" || ent.name === CONVERTED_DIR || ent.name === "node_modules")
10391
+ continue;
10392
+ const full = path16.join(dir, ent.name);
10393
+ if (ent.isDirectory())
10394
+ walkFiles(full, out);
10395
+ else if (ent.isFile())
10396
+ out.push(full);
10397
+ }
10398
+ return out;
10399
+ }
10400
+ function expandSource(kbRoot, entry) {
10401
+ const abs = path16.resolve(kbRoot, expandHome(entry.path));
10402
+ if (!/[*?]/.test(entry.path))
10403
+ return fs15.existsSync(abs) && fs15.statSync(abs).isFile() ? [abs] : [];
10404
+ const base = abs.split(/[*?]/)[0];
10405
+ const root = base.endsWith(path16.sep) ? base : path16.dirname(base);
10406
+ const re = globToRegExp(abs.split(path16.sep).join("/"));
10407
+ return walkFiles(root).filter((f) => re.test(f.split(path16.sep).join("/"))).sort();
10408
+ }
10409
+ function detectChange(kbRoot, file) {
10410
+ const ref = refFor(kbRoot, file);
10411
+ const st = fs15.statSync(file);
10412
+ const prior = readState2(kbRoot, ref);
10413
+ if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
10414
+ return { ref, changed: false, stamp: { sha: prior.sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
10415
+ }
10416
+ const sha = sha256(file);
10417
+ return { ref, changed: !prior || prior.sha !== sha, stamp: { sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
10418
+ }
10419
+ function splitSections(text) {
10420
+ const cuts = [];
10421
+ const re = /^#{1,6}[ \t]+(.+)$/gm;
10422
+ for (let m = re.exec(text); m; m = re.exec(text))
10423
+ cuts.push({ at: m.index, heading: m[1].trim() });
10424
+ if (!cuts.length || cuts[0].at > 0)
10425
+ cuts.unshift({ at: 0 });
10426
+ const spans = [];
10427
+ cuts.forEach((c, i) => {
10428
+ const end = i + 1 < cuts.length ? cuts[i + 1].at : text.length;
10429
+ let start = c.at;
10430
+ while (end - start > SECTION_TARGET_CHARS * 1.5) {
10431
+ const brk = text.indexOf("\n\n", start + SECTION_TARGET_CHARS);
10432
+ if (brk < 0 || brk >= end)
10433
+ break;
10434
+ spans.push({ index: 0, start, end: brk, heading: c.heading });
10435
+ start = brk + 2;
10436
+ }
10437
+ if (text.slice(start, end).trim())
10438
+ spans.push({ index: 0, start, end, heading: c.heading });
10439
+ });
10440
+ return spans.map((s, i) => ({ ...s, index: i }));
10441
+ }
10442
+ async function planIngest(kbRoot, opts) {
10443
+ const converter = opts.converter ?? markitdownConverter;
10444
+ const cap = opts.maxSectionsPerPass ?? DEFAULT_MAX_SECTIONS_PER_PASS;
10445
+ const plan = { sources: [], skipped: [], failed: [], carriedSections: 0, sectionCount: 0, estimatedTokens: 0, existing: opts.existing ?? [] };
10446
+ let budget = cap;
10447
+ const seen = /* @__PURE__ */ new Set();
10448
+ for (const entry of opts.sources) {
10449
+ for (const file of expandSource(kbRoot, entry)) {
10450
+ const { ref, changed, stamp, prior } = detectChange(kbRoot, file);
10451
+ if (seen.has(ref))
10452
+ continue;
10453
+ seen.add(ref);
10454
+ const pending = !!prior && prior.cursor < prior.sections;
10455
+ if (!changed && !pending)
10456
+ continue;
10457
+ if (entry.sensitivity === "local-only" && !opts.localModel) {
10458
+ plan.skipped.push({ ref, reason: "sensitivity: local-only and no local model available" });
10459
+ continue;
10460
+ }
10461
+ if (budget <= 0) {
10462
+ plan.carriedSections += changed ? 1 : prior.sections - prior.cursor;
10463
+ continue;
10464
+ }
10465
+ let text;
10466
+ const conv = convertedFile(kbRoot, ref);
10467
+ try {
10468
+ if (!changed && fs15.existsSync(conv))
10469
+ text = fs15.readFileSync(conv, "utf8");
10470
+ else {
10471
+ text = PASS_THROUGH.has(path16.extname(file).toLowerCase()) ? fs15.readFileSync(file, "utf8") : await converter(file);
10472
+ if (!opts.dryRun) {
10473
+ fs15.mkdirSync(path16.dirname(conv), { recursive: true });
10474
+ fs15.writeFileSync(conv, text);
10475
+ }
10476
+ }
10477
+ } catch (e) {
10478
+ plan.failed.push({ ref, error: e.message.split("\n")[0] });
10479
+ continue;
10480
+ }
10481
+ const all = splitSections(text);
10482
+ const from = changed ? 0 : prior.cursor;
10483
+ const take = all.slice(from, from + budget);
10484
+ budget -= take.length;
10485
+ plan.carriedSections += all.length - from - take.length;
10486
+ plan.sectionCount += take.length;
10487
+ plan.estimatedTokens += Math.ceil(take.reduce((n, s) => n + (s.end - s.start), 0) / CHARS_PER_TOKEN);
10488
+ plan.sources.push({
10489
+ ref,
10490
+ file,
10491
+ converted: conv,
10492
+ sha: stamp.sha,
10493
+ priority: entry.priority,
10494
+ domain: entry.domain,
10495
+ sensitivity: entry.sensitivity,
10496
+ sections: take,
10497
+ totalSections: all.length
10498
+ });
10499
+ if (!opts.dryRun)
10500
+ writeState(kbRoot, { ref, ...stamp, sections: all.length, cursor: from + take.length, priority: entry.priority });
10501
+ }
10502
+ }
10503
+ return plan;
10504
+ }
10505
+ var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome, fileForRef, cacheKey, convertedFile, stampFile, sha256;
10506
+ var init_ingest = __esm({
10507
+ "packages/core/dist/knowledge/ingest.js"() {
10508
+ execFileAsync = promisify2(execFile2);
10509
+ CONVERTED_DIR = ".converted";
10510
+ DEFAULT_MAX_SECTIONS_PER_PASS = 40;
10511
+ CHARS_PER_TOKEN = 4;
10512
+ PASS_THROUGH = /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]);
10513
+ SECTION_TARGET_CHARS = 4e3;
10514
+ markitdownConverter = async (file) => {
10515
+ const { stdout } = await execFileAsync("markitdown", [file], { maxBuffer: 64 * 1024 * 1024 });
10516
+ return stdout;
10517
+ };
10518
+ expandHome = (p) => p === "~" || p.startsWith("~/") ? path16.join(os7.homedir(), p.slice(1)) : p;
10519
+ fileForRef = (kbRoot, ref) => path16.resolve(kbRoot, expandHome(ref));
10520
+ cacheKey = (ref) => `${ref.replace(/[^A-Za-z0-9._-]+/g, "_")}.${crypto3.createHash("sha1").update(ref).digest("hex").slice(0, 6)}`;
10521
+ convertedFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.md`);
10522
+ stampFile = (kbRoot, ref) => path16.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.sha`);
10523
+ sha256 = (file) => crypto3.createHash("sha256").update(fs15.readFileSync(file)).digest("hex");
10524
+ }
10525
+ });
10526
+
10527
+ // packages/core/dist/knowledge/verify.js
10528
+ function normalizeWithMap(src, keepBreakHyphen = false) {
10529
+ const out = [];
10530
+ const map2 = [];
10531
+ const push = (s, at) => {
10532
+ for (const ch of s) {
10533
+ out.push(ch);
10534
+ map2.push(at);
10535
+ }
10536
+ };
10537
+ let i = 0;
10538
+ while (i < src.length) {
10539
+ const ch = src[i];
10540
+ if (ch === "-" && /^-[ \t]*\r?\n[ \t]*[a-z]/.test(src.slice(i, i + 40)) && i > 0 && /[A-Za-z]/.test(src[i - 1])) {
10541
+ if (keepBreakHyphen)
10542
+ push("-", i);
10543
+ i++;
10544
+ while (/\s/.test(src[i]))
10545
+ i++;
10546
+ continue;
10547
+ }
10548
+ if (/\s/.test(ch)) {
10549
+ const start = i;
10550
+ while (i < src.length && /\s/.test(src[i]))
10551
+ i++;
10552
+ if (out.length && out[out.length - 1] !== " ")
10553
+ push(" ", start);
10554
+ continue;
10555
+ }
10556
+ if (MARKUP.has(ch)) {
10557
+ i++;
10558
+ continue;
10559
+ }
10560
+ if (LIGATURES[ch])
10561
+ push(LIGATURES[ch], i);
10562
+ else if (QUOTES[ch])
10563
+ push(QUOTES[ch], i);
10564
+ else if (DASHES.has(ch))
10565
+ push("-", i);
10566
+ else
10567
+ push(ch, i);
10568
+ i++;
10569
+ }
10570
+ while (out.length && out[out.length - 1] === " ") {
10571
+ out.pop();
10572
+ map2.pop();
10573
+ }
10574
+ return { text: out.join(""), map: map2 };
10575
+ }
10576
+ function findQuote(source, quote) {
10577
+ const needle = normalizeText(quote);
10578
+ if (!needle)
10579
+ return null;
10580
+ const views = typeof source === "string" ? normalizedViews(source) : source;
10581
+ for (const hay of views) {
10582
+ const at = hay.text.indexOf(needle);
10583
+ if (at >= 0)
10584
+ return [hay.map[at], hay.map[at + needle.length - 1] + 1];
10585
+ }
10586
+ return null;
10587
+ }
10588
+ var LIGATURES, QUOTES, DASHES, MARKUP, normalizeText, normalizedViews, DOCLING_DROP_THRESHOLD, needsDocling, spansOverlap;
10589
+ var init_verify = __esm({
10590
+ "packages/core/dist/knowledge/verify.js"() {
10591
+ LIGATURES = { "\uFB01": "fi", "\uFB02": "fl", "\uFB00": "ff", "\uFB03": "ffi", "\uFB04": "ffl", "\uFB05": "st", "\uFB06": "st" };
10592
+ QUOTES = { "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"' };
10593
+ DASHES = /* @__PURE__ */ new Set(["\u2010", "\u2011", "\u2012", "\u2013", "\u2014", "\u2015", "\u2212"]);
10594
+ MARKUP = /* @__PURE__ */ new Set(["*", "_", "`", "\\"]);
10595
+ normalizeText = (s) => normalizeWithMap(s).text;
10596
+ normalizedViews = (src) => [normalizeWithMap(src), normalizeWithMap(src, true)];
10597
+ DOCLING_DROP_THRESHOLD = 0.2;
10598
+ needsDocling = (dropped, total) => total > 0 && dropped / total > DOCLING_DROP_THRESHOLD;
10599
+ spansOverlap = (a, b) => a[0] < b[1] && b[0] < a[1];
10600
+ }
10601
+ });
10602
+
10603
+ // packages/core/dist/knowledge/store.js
10604
+ import fs16 from "fs";
10605
+ import os8 from "os";
10606
+ import path17 from "path";
10348
10607
  import matter2 from "gray-matter";
10349
10608
  function duplicateIdProblem(id, ignored, used) {
10350
10609
  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.`;
10351
10610
  }
10352
10611
  function isPendingProposalDup(e, rulesDir) {
10353
- return e.problems.every((p) => p.startsWith("duplicate id")) && path16.relative(rulesDir, e.file).split(path16.sep).includes(PROPOSED_DIR);
10612
+ return e.problems.every((p) => p.startsWith("duplicate id")) && path17.relative(rulesDir, e.file).split(path17.sep).includes(PROPOSED_DIR);
10354
10613
  }
10355
10614
  function splitStatement(body) {
10356
10615
  const first = body.trim().split(/\n\s*\n/)[0] ?? "";
@@ -10359,8 +10618,8 @@ function splitStatement(body) {
10359
10618
  function listMarkdown(dir, includeProposed) {
10360
10619
  const out = [];
10361
10620
  const walk = (d) => {
10362
- for (const ent of fs15.readdirSync(d, { withFileTypes: true })) {
10363
- const full = path16.join(d, ent.name);
10621
+ for (const ent of fs16.readdirSync(d, { withFileTypes: true })) {
10622
+ const full = path17.join(d, ent.name);
10364
10623
  if (ent.isDirectory()) {
10365
10624
  if (ent.name === PROPOSED_DIR && !includeProposed)
10366
10625
  continue;
@@ -10371,11 +10630,11 @@ function listMarkdown(dir, includeProposed) {
10371
10630
  }
10372
10631
  };
10373
10632
  walk(dir);
10374
- const isProposed = (f) => path16.relative(dir, f).split(path16.sep).includes(PROPOSED_DIR);
10633
+ const isProposed = (f) => path17.relative(dir, f).split(path17.sep).includes(PROPOSED_DIR);
10375
10634
  return out.sort((a, b) => Number(isProposed(a)) - Number(isProposed(b)) || (a < b ? -1 : a > b ? 1 : 0));
10376
10635
  }
10377
10636
  function loadRulesDir(dir, layer, opts = {}) {
10378
- if (!fs15.existsSync(dir))
10637
+ if (!fs16.existsSync(dir))
10379
10638
  return { rules: [], errors: [] };
10380
10639
  const rules = [];
10381
10640
  const errors = [];
@@ -10383,7 +10642,7 @@ function loadRulesDir(dir, layer, opts = {}) {
10383
10642
  for (const file of listMarkdown(dir, opts.includeProposed ?? false)) {
10384
10643
  let parsed;
10385
10644
  try {
10386
- parsed = matter2(fs15.readFileSync(file, "utf8"), NO_JS_FRONTMATTER);
10645
+ parsed = matter2(fs16.readFileSync(file, "utf8"), NO_JS_FRONTMATTER);
10387
10646
  } catch (e) {
10388
10647
  errors.push({ file, problems: [`frontmatter parse error: ${e.message}`] });
10389
10648
  continue;
@@ -10407,14 +10666,14 @@ function loadRulesDir(dir, layer, opts = {}) {
10407
10666
  function resolveSourceRef(ref, kbRoot, sources) {
10408
10667
  const bases = /* @__PURE__ */ new Set([kbRoot]);
10409
10668
  for (const s of sources) {
10410
- let d = path16.resolve(kbRoot, expandHome(s.path.split(/[*?[{]/)[0]));
10411
- for (let i = 0; i < 6 && d !== path16.dirname(d); i++) {
10412
- d = path16.dirname(d);
10669
+ let d = path17.resolve(kbRoot, expandHome2(s.path.split(/[*?[{]/)[0]));
10670
+ for (let i = 0; i < 6 && d !== path17.dirname(d); i++) {
10671
+ d = path17.dirname(d);
10413
10672
  bases.add(d);
10414
10673
  }
10415
10674
  }
10416
- const cands = [path16.resolve(kbRoot, expandHome(ref)), ...[...bases].map((b) => path16.join(b, ref))];
10417
- return cands.find((c) => fs15.existsSync(c) && fs15.statSync(c).isFile());
10675
+ const cands = [path17.resolve(kbRoot, expandHome2(ref)), ...[...bases].map((b) => path17.join(b, ref))];
10676
+ return cands.find((c) => fs16.existsSync(c) && fs16.statSync(c).isFile());
10418
10677
  }
10419
10678
  function validateKb(opts) {
10420
10679
  const { rules, errors } = loadRulesDir(opts.rulesDir, "kb", { includeProposed: true });
@@ -10422,7 +10681,7 @@ function validateKb(opts) {
10422
10681
  const dups = errors.filter((e) => e.problems.some((p) => p.startsWith("duplicate id")) && !isPendingProposalDup(e, opts.rulesDir));
10423
10682
  const pendingDups = errors.filter((e) => isPendingProposalDup(e, opts.rulesDir)).length;
10424
10683
  const invalid = errors.filter((e) => !dups.includes(e) && !isPendingProposalDup(e, opts.rulesDir));
10425
- const proposed = rules.filter((r) => path16.relative(opts.rulesDir, r.file).split(path16.sep).includes(PROPOSED_DIR)).length;
10684
+ const proposed = rules.filter((r) => path17.relative(opts.rulesDir, r.file).split(path17.sep).includes(PROPOSED_DIR)).length;
10426
10685
  checks.push({ level: "ok", message: `${rules.length + errors.length} rule files read (${rules.length - proposed} live, ${proposed + pendingDups} proposed parsed OK)` });
10427
10686
  if (invalid.length)
10428
10687
  for (const e of invalid)
@@ -10435,7 +10694,7 @@ function validateKb(opts) {
10435
10694
  else
10436
10695
  checks.push({ level: "ok", message: "no duplicate ids" });
10437
10696
  const cache = /* @__PURE__ */ new Map();
10438
- let verified = 0, unresolved = 0;
10697
+ let verified = 0, unresolved = 0, notIngested = 0;
10439
10698
  for (const r of rules) {
10440
10699
  r.sources.forEach((src, i) => {
10441
10700
  const file = resolveSourceRef(src.ref, opts.kbRoot, opts.sources);
@@ -10443,16 +10702,28 @@ function validateKb(opts) {
10443
10702
  unresolved++;
10444
10703
  return;
10445
10704
  }
10446
- if (!cache.has(file))
10447
- cache.set(file, fs15.readFileSync(file, "utf8"));
10448
- if (cache.get(file).includes(src.quote))
10705
+ const key = `${src.ref}\0${file}`;
10706
+ if (!cache.has(key)) {
10707
+ const conv = readState2(opts.kbRoot, src.ref) ? convertedFile(opts.kbRoot, src.ref) : null;
10708
+ const cached = !!conv && fs16.existsSync(conv);
10709
+ cache.set(key, {
10710
+ hay: normalizedViews(fs16.readFileSync(cached ? conv : file, "utf8")),
10711
+ convertible: !cached && !PASS_THROUGH.has(path17.extname(file).toLowerCase())
10712
+ });
10713
+ }
10714
+ const { hay, convertible } = cache.get(key);
10715
+ if (findQuote(hay, src.quote))
10449
10716
  verified++;
10717
+ else if (convertible)
10718
+ notIngested++;
10450
10719
  else
10451
10720
  checks.push({ level: "fail", message: `quote not found in ${file}: ${r.id} sources[${i}] (${r.file})` });
10452
10721
  });
10453
10722
  }
10454
10723
  if (!checks.some((c) => c.message.startsWith("quote not found")))
10455
10724
  checks.push({ level: "ok", message: `${verified} source quotes found in their source files` });
10725
+ if (notIngested)
10726
+ checks.push({ level: "skip", message: `${notIngested} source refs need conversion but have no .converted cache, quote not checked (run \`squadrant knowledge ingest <kb>\`)` });
10456
10727
  if (unresolved)
10457
10728
  checks.push({ level: "skip", message: `${unresolved} source refs not resolvable on this machine, quote not checked` });
10458
10729
  if (opts.domains) {
@@ -10463,22 +10734,24 @@ function validateKb(opts) {
10463
10734
  }
10464
10735
  return { checks, ok: !checks.some((c) => c.level === "fail") };
10465
10736
  }
10466
- var NO_JS_FRONTMATTER, expandHome;
10737
+ var NO_JS_FRONTMATTER, expandHome2;
10467
10738
  var init_store2 = __esm({
10468
10739
  "packages/core/dist/knowledge/store.js"() {
10469
10740
  init_dist();
10741
+ init_ingest();
10470
10742
  init_paths();
10743
+ init_verify();
10471
10744
  NO_JS_FRONTMATTER = {
10472
10745
  engines: { javascript: { parse: () => {
10473
10746
  throw new Error("only YAML front matter is allowed");
10474
10747
  } } }
10475
10748
  };
10476
- expandHome = (p) => p === "~" || p.startsWith("~/") ? path16.join(os7.homedir(), p.slice(1)) : p;
10749
+ expandHome2 = (p) => p === "~" || p.startsWith("~/") ? path17.join(os8.homedir(), p.slice(1)) : p;
10477
10750
  }
10478
10751
  });
10479
10752
 
10480
10753
  // packages/core/dist/knowledge/layers.js
10481
- import fs16 from "fs";
10754
+ import fs17 from "fs";
10482
10755
  function loadKbRules(cfg, kb, opts = {}) {
10483
10756
  return loadRulesDir(kbRulesDir(cfg, kb), `kb:${kb}`, opts);
10484
10757
  }
@@ -10490,7 +10763,7 @@ function resolveProjectRules(cfg, project, opts = {}) {
10490
10763
  const errors = [];
10491
10764
  const warnings = [];
10492
10765
  for (const kb of subscribedKbs(cfg, project)) {
10493
- if (!fs16.existsSync(kbDir(cfg, kb))) {
10766
+ if (!fs17.existsSync(kbDir(cfg, kb))) {
10494
10767
  warnings.push(`knowledge base '${kb}' not found (run: squadrant knowledge init ${kb})`);
10495
10768
  continue;
10496
10769
  }
@@ -10528,8 +10801,8 @@ var init_layers = __esm({
10528
10801
  });
10529
10802
 
10530
10803
  // packages/core/dist/knowledge/index-file.js
10531
- import fs17 from "fs";
10532
- import path17 from "path";
10804
+ import fs18 from "fs";
10805
+ import path18 from "path";
10533
10806
  function compileIndex(kb, rules, now = /* @__PURE__ */ new Date()) {
10534
10807
  return {
10535
10808
  kb,
@@ -10538,18 +10811,18 @@ function compileIndex(kb, rules, now = /* @__PURE__ */ new Date()) {
10538
10811
  };
10539
10812
  }
10540
10813
  function writeIndex(kbRoot, index) {
10541
- const file = path17.join(kbRoot, "index.json");
10814
+ const file = path18.join(kbRoot, "index.json");
10542
10815
  const tmp = `${file}.tmp-${process.pid}`;
10543
- fs17.writeFileSync(tmp, JSON.stringify(index, null, 2) + "\n");
10544
- fs17.renameSync(tmp, file);
10816
+ fs18.writeFileSync(tmp, JSON.stringify(index, null, 2) + "\n");
10817
+ fs18.renameSync(tmp, file);
10545
10818
  return file;
10546
10819
  }
10547
10820
  function readIndex(kbRoot, kb) {
10548
- const file = path17.join(kbRoot, "index.json");
10549
- if (!fs17.existsSync(file))
10821
+ const file = path18.join(kbRoot, "index.json");
10822
+ if (!fs18.existsSync(file))
10550
10823
  return { index: null, problem: `index.json missing for kb '${kb}'` };
10551
10824
  try {
10552
- const index = JSON.parse(fs17.readFileSync(file, "utf8"));
10825
+ const index = JSON.parse(fs18.readFileSync(file, "utf8"));
10553
10826
  if (!Array.isArray(index.rules))
10554
10827
  return { index: null, problem: `index.json corrupt for kb '${kb}' (no rules array)` };
10555
10828
  return { index };
@@ -13644,15 +13917,15 @@ var init_js_yaml = __esm({
13644
13917
  });
13645
13918
 
13646
13919
  // packages/core/dist/knowledge/sources.js
13647
- import fs18 from "fs";
13648
- import path18 from "path";
13920
+ import fs19 from "fs";
13921
+ import path19 from "path";
13649
13922
  function loadSources(kbRoot) {
13650
- const file = path18.join(kbRoot, "sources.yaml");
13651
- if (!fs18.existsSync(file))
13923
+ const file = path19.join(kbRoot, "sources.yaml");
13924
+ if (!fs19.existsSync(file))
13652
13925
  return { sources: [], errors: [] };
13653
13926
  let data;
13654
13927
  try {
13655
- data = yaml.load(fs18.readFileSync(file, "utf8"));
13928
+ data = yaml.load(fs19.readFileSync(file, "utf8"));
13656
13929
  } catch (e) {
13657
13930
  return { sources: [], errors: [`YAML parse error: ${e.message.split("\n")[0]}`] };
13658
13931
  }
@@ -13690,189 +13963,6 @@ var init_sources = __esm({
13690
13963
  }
13691
13964
  });
13692
13965
 
13693
- // packages/core/dist/knowledge/ingest.js
13694
- import crypto3 from "crypto";
13695
- import fs19 from "fs";
13696
- import os8 from "os";
13697
- import path19 from "path";
13698
- import { execFile as execFile2 } from "child_process";
13699
- import { promisify as promisify2 } from "util";
13700
- function refFor(kbRoot, file) {
13701
- const rel = path19.relative(kbRoot, file);
13702
- if (rel && !rel.startsWith("..") && !path19.isAbsolute(rel))
13703
- return rel.split(path19.sep).join("/");
13704
- const home = os8.homedir();
13705
- return file.startsWith(home + path19.sep) ? `~/${path19.relative(home, file).split(path19.sep).join("/")}` : file;
13706
- }
13707
- function readState2(kbRoot, ref) {
13708
- try {
13709
- return JSON.parse(fs19.readFileSync(stampFile(kbRoot, ref), "utf8"));
13710
- } catch {
13711
- return null;
13712
- }
13713
- }
13714
- function writeState(kbRoot, st) {
13715
- fs19.mkdirSync(path19.join(kbRoot, CONVERTED_DIR), { recursive: true });
13716
- fs19.writeFileSync(stampFile(kbRoot, st.ref), JSON.stringify(st) + "\n");
13717
- }
13718
- function globToRegExp(glob) {
13719
- let re = "";
13720
- for (let i = 0; i < glob.length; i++) {
13721
- const c = glob[i];
13722
- if (c === "*") {
13723
- if (glob[i + 1] === "*") {
13724
- re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
13725
- i += glob[i + 2] === "/" ? 2 : 1;
13726
- } else
13727
- re += "[^/]*";
13728
- } else if (c === "?")
13729
- re += "[^/]";
13730
- else
13731
- re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13732
- }
13733
- return new RegExp(`^${re}$`);
13734
- }
13735
- function walkFiles(dir, out = []) {
13736
- if (!fs19.existsSync(dir))
13737
- return out;
13738
- for (const ent of fs19.readdirSync(dir, { withFileTypes: true })) {
13739
- if (ent.name === ".git" || ent.name === CONVERTED_DIR || ent.name === "node_modules")
13740
- continue;
13741
- const full = path19.join(dir, ent.name);
13742
- if (ent.isDirectory())
13743
- walkFiles(full, out);
13744
- else if (ent.isFile())
13745
- out.push(full);
13746
- }
13747
- return out;
13748
- }
13749
- function expandSource(kbRoot, entry) {
13750
- const abs = path19.resolve(kbRoot, expandHome2(entry.path));
13751
- if (!/[*?]/.test(entry.path))
13752
- return fs19.existsSync(abs) && fs19.statSync(abs).isFile() ? [abs] : [];
13753
- const base = abs.split(/[*?]/)[0];
13754
- const root = base.endsWith(path19.sep) ? base : path19.dirname(base);
13755
- const re = globToRegExp(abs.split(path19.sep).join("/"));
13756
- return walkFiles(root).filter((f) => re.test(f.split(path19.sep).join("/"))).sort();
13757
- }
13758
- function detectChange(kbRoot, file) {
13759
- const ref = refFor(kbRoot, file);
13760
- const st = fs19.statSync(file);
13761
- const prior = readState2(kbRoot, ref);
13762
- if (prior && prior.size === st.size && prior.mtimeMs === st.mtimeMs) {
13763
- return { ref, changed: false, stamp: { sha: prior.sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
13764
- }
13765
- const sha = sha256(file);
13766
- return { ref, changed: !prior || prior.sha !== sha, stamp: { sha, size: st.size, mtimeMs: st.mtimeMs }, prior };
13767
- }
13768
- function splitSections(text) {
13769
- const cuts = [];
13770
- const re = /^#{1,6}[ \t]+(.+)$/gm;
13771
- for (let m = re.exec(text); m; m = re.exec(text))
13772
- cuts.push({ at: m.index, heading: m[1].trim() });
13773
- if (!cuts.length || cuts[0].at > 0)
13774
- cuts.unshift({ at: 0 });
13775
- const spans = [];
13776
- cuts.forEach((c, i) => {
13777
- const end = i + 1 < cuts.length ? cuts[i + 1].at : text.length;
13778
- let start = c.at;
13779
- while (end - start > SECTION_TARGET_CHARS * 1.5) {
13780
- const brk = text.indexOf("\n\n", start + SECTION_TARGET_CHARS);
13781
- if (brk < 0 || brk >= end)
13782
- break;
13783
- spans.push({ index: 0, start, end: brk, heading: c.heading });
13784
- start = brk + 2;
13785
- }
13786
- if (text.slice(start, end).trim())
13787
- spans.push({ index: 0, start, end, heading: c.heading });
13788
- });
13789
- return spans.map((s, i) => ({ ...s, index: i }));
13790
- }
13791
- async function planIngest(kbRoot, opts) {
13792
- const converter = opts.converter ?? markitdownConverter;
13793
- const cap = opts.maxSectionsPerPass ?? DEFAULT_MAX_SECTIONS_PER_PASS;
13794
- const plan = { sources: [], skipped: [], failed: [], carriedSections: 0, sectionCount: 0, estimatedTokens: 0, existing: opts.existing ?? [] };
13795
- let budget = cap;
13796
- const seen = /* @__PURE__ */ new Set();
13797
- for (const entry of opts.sources) {
13798
- for (const file of expandSource(kbRoot, entry)) {
13799
- const { ref, changed, stamp, prior } = detectChange(kbRoot, file);
13800
- if (seen.has(ref))
13801
- continue;
13802
- seen.add(ref);
13803
- const pending = !!prior && prior.cursor < prior.sections;
13804
- if (!changed && !pending)
13805
- continue;
13806
- if (entry.sensitivity === "local-only" && !opts.localModel) {
13807
- plan.skipped.push({ ref, reason: "sensitivity: local-only and no local model available" });
13808
- continue;
13809
- }
13810
- if (budget <= 0) {
13811
- plan.carriedSections += changed ? 1 : prior.sections - prior.cursor;
13812
- continue;
13813
- }
13814
- let text;
13815
- const conv = convertedFile(kbRoot, ref);
13816
- try {
13817
- if (!changed && fs19.existsSync(conv))
13818
- text = fs19.readFileSync(conv, "utf8");
13819
- else {
13820
- text = PASS_THROUGH.has(path19.extname(file).toLowerCase()) ? fs19.readFileSync(file, "utf8") : await converter(file);
13821
- if (!opts.dryRun) {
13822
- fs19.mkdirSync(path19.dirname(conv), { recursive: true });
13823
- fs19.writeFileSync(conv, text);
13824
- }
13825
- }
13826
- } catch (e) {
13827
- plan.failed.push({ ref, error: e.message.split("\n")[0] });
13828
- continue;
13829
- }
13830
- const all = splitSections(text);
13831
- const from = changed ? 0 : prior.cursor;
13832
- const take = all.slice(from, from + budget);
13833
- budget -= take.length;
13834
- plan.carriedSections += all.length - from - take.length;
13835
- plan.sectionCount += take.length;
13836
- plan.estimatedTokens += Math.ceil(take.reduce((n, s) => n + (s.end - s.start), 0) / CHARS_PER_TOKEN);
13837
- plan.sources.push({
13838
- ref,
13839
- file,
13840
- converted: conv,
13841
- sha: stamp.sha,
13842
- priority: entry.priority,
13843
- domain: entry.domain,
13844
- sensitivity: entry.sensitivity,
13845
- sections: take,
13846
- totalSections: all.length
13847
- });
13848
- if (!opts.dryRun)
13849
- writeState(kbRoot, { ref, ...stamp, sections: all.length, cursor: from + take.length, priority: entry.priority });
13850
- }
13851
- }
13852
- return plan;
13853
- }
13854
- var execFileAsync, CONVERTED_DIR, DEFAULT_MAX_SECTIONS_PER_PASS, CHARS_PER_TOKEN, PASS_THROUGH, SECTION_TARGET_CHARS, markitdownConverter, expandHome2, fileForRef, cacheKey, convertedFile, stampFile, sha256;
13855
- var init_ingest = __esm({
13856
- "packages/core/dist/knowledge/ingest.js"() {
13857
- execFileAsync = promisify2(execFile2);
13858
- CONVERTED_DIR = ".converted";
13859
- DEFAULT_MAX_SECTIONS_PER_PASS = 40;
13860
- CHARS_PER_TOKEN = 4;
13861
- PASS_THROUGH = /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]);
13862
- SECTION_TARGET_CHARS = 4e3;
13863
- markitdownConverter = async (file) => {
13864
- const { stdout } = await execFileAsync("markitdown", [file], { maxBuffer: 64 * 1024 * 1024 });
13865
- return stdout;
13866
- };
13867
- expandHome2 = (p) => p === "~" || p.startsWith("~/") ? path19.join(os8.homedir(), p.slice(1)) : p;
13868
- fileForRef = (kbRoot, ref) => path19.resolve(kbRoot, expandHome2(ref));
13869
- cacheKey = (ref) => `${ref.replace(/[^A-Za-z0-9._-]+/g, "_")}.${crypto3.createHash("sha1").update(ref).digest("hex").slice(0, 6)}`;
13870
- convertedFile = (kbRoot, ref) => path19.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.md`);
13871
- stampFile = (kbRoot, ref) => path19.join(kbRoot, CONVERTED_DIR, `${cacheKey(ref)}.sha`);
13872
- sha256 = (file) => crypto3.createHash("sha256").update(fs19.readFileSync(file)).digest("hex");
13873
- }
13874
- });
13875
-
13876
13966
  // packages/core/dist/knowledge/match.js
13877
13967
  import path20 from "path";
13878
13968
  function globMatches(glob, relPath) {
@@ -14290,6 +14380,13 @@ function ensureGitRepo(dir) {
14290
14380
  return false;
14291
14381
  }
14292
14382
  }
14383
+ function ensureKbGitignore(dir) {
14384
+ const file = path23.join(dir, ".gitignore");
14385
+ if (fs22.existsSync(file))
14386
+ return false;
14387
+ fs22.writeFileSync(file, KB_GITIGNORE);
14388
+ return true;
14389
+ }
14293
14390
  function findLegacyOverlays(cfg) {
14294
14391
  return Object.keys(cfg.projects).filter((p) => fs22.existsSync(legacyOverlay(cfg, p))).sort();
14295
14392
  }
@@ -14344,6 +14441,8 @@ function moveLegacyKbs(cfg, opts = {}) {
14344
14441
  }
14345
14442
  if (ensureGitRepo(to))
14346
14443
  notes.push("git init");
14444
+ if (ensureKbGitignore(to))
14445
+ notes.push("wrote .gitignore (raw/, .converted/)");
14347
14446
  if (fs22.existsSync(path23.join(to, "index.json")))
14348
14447
  notes.push(`index.json holds old paths: run squadrant knowledge reindex ${kb}`);
14349
14448
  if (action === "copied")
@@ -14351,87 +14450,17 @@ function moveLegacyKbs(cfg, opts = {}) {
14351
14450
  return { kb, from, to, action, ...notes.length ? { note: notes.join("; ") } : {} };
14352
14451
  });
14353
14452
  }
14354
- var legacyRoot, legacyOverlay;
14453
+ var legacyRoot, KB_GITIGNORE, legacyOverlay;
14355
14454
  var init_migrate = __esm({
14356
14455
  "packages/core/dist/knowledge/migrate.js"() {
14357
14456
  init_dist();
14358
14457
  init_paths();
14359
14458
  legacyRoot = (cfg) => path23.join(resolveHome(cfg.hubVault), "knowledge");
14459
+ KB_GITIGNORE = "# squadrant KB defaults: source documents stay local\nraw/\n.converted/\n";
14360
14460
  legacyOverlay = (cfg, project) => path23.join(resolveHome(cfg.projects[project].spokeVault), "knowledge", "rules");
14361
14461
  }
14362
14462
  });
14363
14463
 
14364
- // packages/core/dist/knowledge/verify.js
14365
- function normalizeWithMap(src, keepBreakHyphen = false) {
14366
- const out = [];
14367
- const map2 = [];
14368
- const push = (s, at) => {
14369
- for (const ch of s) {
14370
- out.push(ch);
14371
- map2.push(at);
14372
- }
14373
- };
14374
- let i = 0;
14375
- while (i < src.length) {
14376
- const ch = src[i];
14377
- if (ch === "-" && /^-[ \t]*\r?\n[ \t]*[a-z]/.test(src.slice(i, i + 40)) && i > 0 && /[A-Za-z]/.test(src[i - 1])) {
14378
- if (keepBreakHyphen)
14379
- push("-", i);
14380
- i++;
14381
- while (/\s/.test(src[i]))
14382
- i++;
14383
- continue;
14384
- }
14385
- if (/\s/.test(ch)) {
14386
- const start = i;
14387
- while (i < src.length && /\s/.test(src[i]))
14388
- i++;
14389
- if (out.length && out[out.length - 1] !== " ")
14390
- push(" ", start);
14391
- continue;
14392
- }
14393
- if (LIGATURES[ch])
14394
- push(LIGATURES[ch], i);
14395
- else if (QUOTES[ch])
14396
- push(QUOTES[ch], i);
14397
- else if (DASHES.has(ch))
14398
- push("-", i);
14399
- else
14400
- push(ch, i);
14401
- i++;
14402
- }
14403
- while (out.length && out[out.length - 1] === " ") {
14404
- out.pop();
14405
- map2.pop();
14406
- }
14407
- return { text: out.join(""), map: map2 };
14408
- }
14409
- function findQuote(source, quote) {
14410
- const needle = normalizeText(quote);
14411
- if (!needle)
14412
- return null;
14413
- const views = typeof source === "string" ? normalizedViews(source) : source;
14414
- for (const hay of views) {
14415
- const at = hay.text.indexOf(needle);
14416
- if (at >= 0)
14417
- return [hay.map[at], hay.map[at + needle.length - 1] + 1];
14418
- }
14419
- return null;
14420
- }
14421
- var LIGATURES, QUOTES, DASHES, normalizeText, normalizedViews, DOCLING_DROP_THRESHOLD, needsDocling, spansOverlap;
14422
- var init_verify = __esm({
14423
- "packages/core/dist/knowledge/verify.js"() {
14424
- LIGATURES = { "\uFB01": "fi", "\uFB02": "fl", "\uFB00": "ff", "\uFB03": "ffi", "\uFB04": "ffl", "\uFB05": "st", "\uFB06": "st" };
14425
- QUOTES = { "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"' };
14426
- DASHES = /* @__PURE__ */ new Set(["\u2010", "\u2011", "\u2012", "\u2013", "\u2014", "\u2015", "\u2212"]);
14427
- normalizeText = (s) => normalizeWithMap(s).text;
14428
- normalizedViews = (src) => [normalizeWithMap(src), normalizeWithMap(src, true)];
14429
- DOCLING_DROP_THRESHOLD = 0.2;
14430
- needsDocling = (dropped, total) => total > 0 && dropped / total > DOCLING_DROP_THRESHOLD;
14431
- spansOverlap = (a, b) => a[0] < b[1] && b[0] < a[1];
14432
- }
14433
- });
14434
-
14435
14464
  // packages/core/dist/knowledge/anchor.js
14436
14465
  import fs23 from "fs";
14437
14466
  import path24 from "path";
@@ -15957,6 +15986,7 @@ __export(dist_exports2, {
15957
15986
  NOISY_RATE: () => NOISY_RATE,
15958
15987
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
15959
15988
  PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
15989
+ PASS_THROUGH: () => PASS_THROUGH,
15960
15990
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
15961
15991
  PROMPT_MIN_SCORE: () => PROMPT_MIN_SCORE,
15962
15992
  PROMPT_RULE_CAP: () => PROMPT_RULE_CAP,
@@ -16096,6 +16126,7 @@ __export(dist_exports2, {
16096
16126
  encodeMsg: () => encodeMsg,
16097
16127
  ensureDaemon: () => ensureDaemon,
16098
16128
  ensureGitRepo: () => ensureGitRepo,
16129
+ ensureKbGitignore: () => ensureKbGitignore,
16099
16130
  ensureSocksDir: () => ensureSocksDir,
16100
16131
  escalationsFile: () => escalationsFile,
16101
16132
  evaluateCodeConflict: () => evaluateCodeConflict,
@@ -30670,6 +30701,7 @@ function runKnowledgeInit(kb, configPath = DEFAULT_CONFIG_PATH) {
30670
30701
  created.push("index.json");
30671
30702
  }
30672
30703
  if (ensureGitRepo(dir)) created.push(".git");
30704
+ if (ensureKbGitignore(dir)) created.push(".gitignore");
30673
30705
  if (!kbConfigs(cfg)[kb]) {
30674
30706
  cfg.knowledgeBases = { ...cfg.knowledgeBases ?? {}, [kb]: {} };
30675
30707
  saveConfig(cfg, configPath);
@@ -30700,6 +30732,7 @@ var knowledgeCommand = new Command39("knowledge").description("Manage rules know
30700
30732
  knowledgeCommand.command("init <kb>").description("Create a knowledge base (its own git repo) under ~/squadrant/kb/<kb>/").action((kb) => {
30701
30733
  const { dir, created } = runKnowledgeInit(kb);
30702
30734
  console.log(created.length ? chalk38.green(`Initialised ${dir} (${created.join(", ")})`) : `Already initialised: ${dir}`);
30735
+ console.log(chalk38.dim("raw/ and .converted/ are git-ignored by default (edit .gitignore to opt in)."));
30703
30736
  console.log(chalk38.dim(KNOWLEDGE_PRIVACY_NOTICE));
30704
30737
  });
30705
30738
  knowledgeCommand.command("subscribe <kb>").description("Subscribe a project to a knowledge base").requiredOption("--project <name>", "project to subscribe").action((kb, opts) => {