agentsmesh 0.31.0 → 0.32.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.
package/dist/index.js CHANGED
@@ -27351,14 +27351,56 @@ async function diffOutputChecksums(rootBase, lockOutputs) {
27351
27351
  return { outputsModified, outputsRemoved };
27352
27352
  }
27353
27353
 
27354
+ // src/core/generate/stale-cleanup.ts
27355
+ init_fs();
27356
+ init_builtin_targets();
27357
+ async function listFiles2(root, base = root) {
27358
+ const entries = await readdir(root, { withFileTypes: true });
27359
+ const files = [];
27360
+ for (const entry of entries) {
27361
+ const abs = join(root, entry.name);
27362
+ if (entry.isDirectory()) {
27363
+ files.push(...await listFiles2(abs, base));
27364
+ continue;
27365
+ }
27366
+ files.push(relative(base, abs).replace(/\\/g, "/"));
27367
+ }
27368
+ return files;
27369
+ }
27370
+ async function findStaleGeneratedOutputs(args) {
27371
+ const expected = new Set(args.expectedPaths);
27372
+ const stale = /* @__PURE__ */ new Set();
27373
+ const scope = args.scope ?? "project";
27374
+ for (const target31 of args.targets) {
27375
+ const managed = getTargetManagedOutputs(target31, scope);
27376
+ if (!managed) continue;
27377
+ for (const file of managed.files) stale.add(file);
27378
+ for (const dir of managed.dirs) {
27379
+ const absDir = join(args.projectRoot, dir);
27380
+ if (!await exists(absDir)) continue;
27381
+ for (const file of await listFiles2(absDir)) {
27382
+ stale.add(`${dir}/${file}`.replace(/\/+/g, "/"));
27383
+ }
27384
+ }
27385
+ }
27386
+ const found = [];
27387
+ for (const relPath of stale) {
27388
+ if (expected.has(relPath)) continue;
27389
+ if (await exists(join(args.projectRoot, relPath))) found.push(relPath);
27390
+ }
27391
+ return found.sort();
27392
+ }
27393
+
27354
27394
  // src/core/check/lock-sync.ts
27355
27395
  async function checkLockSync(opts) {
27356
- const { config, configDir, canonicalDir, rootBase } = opts;
27396
+ const { config, configDir, canonicalDir, rootBase, scope = "project" } = opts;
27357
27397
  const lock = await readLock(canonicalDir);
27358
27398
  if (lock === null) {
27359
27399
  return {
27360
27400
  inSync: false,
27361
27401
  hasLock: false,
27402
+ canonicalDrift: false,
27403
+ outputDrift: false,
27362
27404
  modified: [],
27363
27405
  added: [],
27364
27406
  removed: [],
@@ -27366,6 +27408,7 @@ async function checkLockSync(opts) {
27366
27408
  lockedViolations: [],
27367
27409
  outputsModified: [],
27368
27410
  outputsRemoved: [],
27411
+ outputsStale: [],
27369
27412
  outputsChecked: false
27370
27413
  };
27371
27414
  }
@@ -27404,10 +27447,20 @@ async function checkLockSync(opts) {
27404
27447
  );
27405
27448
  const outputsChecked = rootBase !== void 0 && lock.outputs !== void 0;
27406
27449
  const { outputsModified, outputsRemoved } = outputsChecked ? await diffOutputChecksums(rootBase, lock.outputs ?? {}) : { outputsModified: [], outputsRemoved: [] };
27407
- const inSync = modified.length === 0 && added.length === 0 && removed.length === 0 && extendsModified.length === 0 && outputsModified.length === 0 && outputsRemoved.length === 0;
27450
+ const outputsStale = rootBase !== void 0 && lock.outputs !== void 0 ? await findStaleGeneratedOutputs({
27451
+ projectRoot: rootBase,
27452
+ targets: [...config.targets, ...config.pluginTargets ?? []],
27453
+ expectedPaths: Object.keys(lock.outputs),
27454
+ scope
27455
+ }) : [];
27456
+ const canonicalDrift = modified.length > 0 || added.length > 0 || removed.length > 0 || extendsModified.length > 0;
27457
+ const outputDrift = outputsModified.length > 0 || outputsRemoved.length > 0 || outputsStale.length > 0;
27458
+ const inSync = !canonicalDrift && !outputDrift;
27408
27459
  return {
27409
27460
  inSync,
27410
27461
  hasLock: true,
27462
+ canonicalDrift,
27463
+ outputDrift,
27411
27464
  modified,
27412
27465
  added,
27413
27466
  removed,
@@ -27415,6 +27468,7 @@ async function checkLockSync(opts) {
27415
27468
  lockedViolations,
27416
27469
  outputsModified,
27417
27470
  outputsRemoved,
27471
+ outputsStale,
27418
27472
  outputsChecked
27419
27473
  };
27420
27474
  }
@@ -27554,6 +27608,103 @@ function copyTargetDescriptor(descriptor31) {
27554
27608
  function getTargetCatalog() {
27555
27609
  return Object.freeze(BUILTIN_TARGETS.map(copyTargetDescriptor));
27556
27610
  }
27611
+ var LegacyTriggersSchema = z.object({
27612
+ file_globs: z.array(z.string()),
27613
+ command_patterns: z.array(z.string()),
27614
+ keywords: z.array(z.string())
27615
+ }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
27616
+ message: "cluster must declare at least one trigger of any type"
27617
+ });
27618
+ var LegacyClusterSchema = z.object({
27619
+ topic: z.string().regex(/^[a-z0-9-]+$/),
27620
+ file: z.string().regex(/\.md$/),
27621
+ summary: z.string().min(1),
27622
+ triggers: LegacyTriggersSchema
27623
+ });
27624
+ var LegacyIndexSchema = z.object({
27625
+ version: z.literal(1),
27626
+ clusters: z.array(LegacyClusterSchema)
27627
+ });
27628
+ function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
27629
+ const specs = [
27630
+ ...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
27631
+ ...cluster.triggers.command_patterns.map(
27632
+ (p) => ({ kind: "command_pattern", pattern: p })
27633
+ ),
27634
+ ...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
27635
+ ];
27636
+ const ids = [];
27637
+ for (const spec of specs) {
27638
+ const key = `${spec.kind}|${spec.pattern}`;
27639
+ let id = triggerIdByKey.get(key);
27640
+ if (id === void 0) {
27641
+ id = makeTriggerId(spec);
27642
+ triggerIdByKey.set(key, id);
27643
+ triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
27644
+ }
27645
+ if (!ids.includes(id)) ids.push(id);
27646
+ }
27647
+ return ids;
27648
+ }
27649
+ var TRIGGER_PREFIX = {
27650
+ file_glob: "glob",
27651
+ command_pattern: "cmd",
27652
+ keyword: "kw"
27653
+ };
27654
+ function makeTriggerId(spec) {
27655
+ const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
27656
+ return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
27657
+ }
27658
+ var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
27659
+ var NEXT_HEADING_RE = /^##\s+/;
27660
+ var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
27661
+ var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
27662
+ var EVIDENCE_REF_RE = /L\d+/g;
27663
+ function parseRulesSection(markdown) {
27664
+ const lines = markdown.split(/\r?\n/);
27665
+ let inRules = false;
27666
+ const rules = [];
27667
+ for (const line of lines) {
27668
+ if (!inRules) {
27669
+ if (RULE_HEADING_RE.test(line)) inRules = true;
27670
+ continue;
27671
+ }
27672
+ if (NEXT_HEADING_RE.test(line)) break;
27673
+ const m = RULE_LINE_RE.exec(line);
27674
+ if (m === null) continue;
27675
+ const ruleIndex = Number(m[1]);
27676
+ let body = m[2];
27677
+ const evidence = [];
27678
+ let tail = EVIDENCE_TAIL_RE.exec(body);
27679
+ while (tail !== null) {
27680
+ const refs = tail[1];
27681
+ const matches = refs.match(EVIDENCE_REF_RE);
27682
+ if (matches !== null) evidence.unshift(...matches);
27683
+ body = body.slice(0, tail.index).trimEnd();
27684
+ tail = EVIDENCE_TAIL_RE.exec(body);
27685
+ }
27686
+ rules.push({ index: ruleIndex, body, evidence });
27687
+ }
27688
+ return rules;
27689
+ }
27690
+ var LEGACY_ARTIFACT_REL = [
27691
+ "index.yaml",
27692
+ "journal.md",
27693
+ "journal.legacy.md",
27694
+ "topics",
27695
+ "distill-ledger.yaml",
27696
+ "distill-proposal.md"
27697
+ ];
27698
+ function deleteLegacyArtifacts(baseDir) {
27699
+ const deleted = [];
27700
+ for (const rel2 of LEGACY_ARTIFACT_REL) {
27701
+ const abs = join(baseDir, rel2);
27702
+ if (!existsSync(abs)) continue;
27703
+ rmSync(abs, { recursive: true, force: true });
27704
+ deleted.push(abs);
27705
+ }
27706
+ return deleted;
27707
+ }
27557
27708
  function normalizeRule2(rule) {
27558
27709
  return rule.trim().replace(/\s+/g, " ").toLowerCase();
27559
27710
  }
@@ -27587,7 +27738,7 @@ function mergeTriggers(graph, spec) {
27587
27738
  if (!triggerIds.includes(existing)) triggerIds.push(existing);
27588
27739
  continue;
27589
27740
  }
27590
- const id = makeTriggerId(spec2);
27741
+ const id = makeTriggerId2(spec2);
27591
27742
  graph.triggers[id] = { kind: spec2.kind, pattern: spec2.pattern };
27592
27743
  reverseLookup.set(key, id);
27593
27744
  triggerIds.push(id);
@@ -27598,14 +27749,14 @@ function mergeTriggers(graph, spec) {
27598
27749
  function triggerKey(t) {
27599
27750
  return `${t.kind}|${t.pattern}`;
27600
27751
  }
27601
- var TRIGGER_PREFIX = {
27752
+ var TRIGGER_PREFIX2 = {
27602
27753
  file_glob: "glob",
27603
27754
  command_pattern: "cmd",
27604
27755
  keyword: "kw"
27605
27756
  };
27606
- function makeTriggerId(spec) {
27757
+ function makeTriggerId2(spec) {
27607
27758
  const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
27608
- return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
27759
+ return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
27609
27760
  }
27610
27761
  function makeLessonId(graph, topic, ruleKey) {
27611
27762
  const slug = ruleToSlug(ruleKey);
@@ -27717,282 +27868,54 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
27717
27868
  code: "STOPWORD_KEYWORD",
27718
27869
  message: `Lesson "${lessonId}" has keyword trigger(s) containing stopwords/short words (${stopworded.join(", ")}); recall filters them from the pattern but NOT from the file/command text, so the phrase can never match contiguously on the --file/--cmd path \u2014 drop the stopwords (e.g. "state art" instead of "state of the art").`
27719
27870
  });
27720
- }
27721
- if (knownPaths !== void 0) {
27722
- const dead = deadFileGlobIds(graph, knownPaths);
27723
- const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
27724
- if (deadHere.length > 0) {
27725
- warnings.push({
27726
- code: "DEAD_GLOB",
27727
- message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
27728
- });
27729
- }
27730
- const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
27731
- if (wide.length > 0) {
27732
- warnings.push({
27733
- code: "WIDE_GLOB_MATCH",
27734
- message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
27735
- });
27736
- }
27737
- }
27738
- return warnings;
27739
- }
27740
-
27741
- // src/lessons/capture-near-duplicate.ts
27742
- var NEAR_DUPLICATE_THRESHOLD = 0.6;
27743
- function nearDuplicateWarning(graph, lessonId) {
27744
- const subject = graph.lessons[lessonId];
27745
- if (subject === void 0) return null;
27746
- const subjectTokens = new Set(tokenize(subject.rule));
27747
- if (subjectTokens.size === 0) return null;
27748
- let best = null;
27749
- for (const [id, other] of Object.entries(graph.lessons)) {
27750
- if (id === lessonId || other.status !== "active") continue;
27751
- const otherTokens = new Set(tokenize(other.rule));
27752
- if (otherTokens.size === 0) continue;
27753
- const score = jaccard(subjectTokens, otherTokens);
27754
- if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
27755
- best = { id, score };
27756
- }
27757
- }
27758
- if (best === null) return null;
27759
- return {
27760
- code: "NEAR_DUPLICATE_LESSON",
27761
- message: `Lesson "${lessonId}" closely resembles active lesson "${best.id}" (~${Math.round(best.score * 100)}% token overlap); consider updating "${best.id}" instead of adding a paraphrase (recall would surface both).`
27762
- };
27763
- }
27764
- function jaccard(a, b) {
27765
- let intersection = 0;
27766
- for (const t of a) if (b.has(t)) intersection += 1;
27767
- return intersection / (a.size + b.size - intersection);
27768
- }
27769
- var LegacyTriggersSchema = z.object({
27770
- file_globs: z.array(z.string()),
27771
- command_patterns: z.array(z.string()),
27772
- keywords: z.array(z.string())
27773
- }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
27774
- message: "cluster must declare at least one trigger of any type"
27775
- });
27776
- var LegacyClusterSchema = z.object({
27777
- topic: z.string().regex(/^[a-z0-9-]+$/),
27778
- file: z.string().regex(/\.md$/),
27779
- summary: z.string().min(1),
27780
- triggers: LegacyTriggersSchema
27781
- });
27782
- var LegacyIndexSchema = z.object({
27783
- version: z.literal(1),
27784
- clusters: z.array(LegacyClusterSchema)
27785
- });
27786
- function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
27787
- const specs = [
27788
- ...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
27789
- ...cluster.triggers.command_patterns.map(
27790
- (p) => ({ kind: "command_pattern", pattern: p })
27791
- ),
27792
- ...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
27793
- ];
27794
- const ids = [];
27795
- for (const spec of specs) {
27796
- const key = `${spec.kind}|${spec.pattern}`;
27797
- let id = triggerIdByKey.get(key);
27798
- if (id === void 0) {
27799
- id = makeTriggerId2(spec);
27800
- triggerIdByKey.set(key, id);
27801
- triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
27802
- }
27803
- if (!ids.includes(id)) ids.push(id);
27804
- }
27805
- return ids;
27806
- }
27807
- var TRIGGER_PREFIX2 = {
27808
- file_glob: "glob",
27809
- command_pattern: "cmd",
27810
- keyword: "kw"
27811
- };
27812
- function makeTriggerId2(spec) {
27813
- const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
27814
- return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
27815
- }
27816
- var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
27817
- var NEXT_HEADING_RE = /^##\s+/;
27818
- var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
27819
- var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
27820
- var EVIDENCE_REF_RE = /L\d+/g;
27821
- function parseRulesSection(markdown) {
27822
- const lines = markdown.split(/\r?\n/);
27823
- let inRules = false;
27824
- const rules = [];
27825
- for (const line of lines) {
27826
- if (!inRules) {
27827
- if (RULE_HEADING_RE.test(line)) inRules = true;
27828
- continue;
27829
- }
27830
- if (NEXT_HEADING_RE.test(line)) break;
27831
- const m = RULE_LINE_RE.exec(line);
27832
- if (m === null) continue;
27833
- const ruleIndex = Number(m[1]);
27834
- let body = m[2];
27835
- const evidence = [];
27836
- let tail = EVIDENCE_TAIL_RE.exec(body);
27837
- while (tail !== null) {
27838
- const refs = tail[1];
27839
- const matches = refs.match(EVIDENCE_REF_RE);
27840
- if (matches !== null) evidence.unshift(...matches);
27841
- body = body.slice(0, tail.index).trimEnd();
27842
- tail = EVIDENCE_TAIL_RE.exec(body);
27843
- }
27844
- rules.push({ index: ruleIndex, body, evidence });
27845
- }
27846
- return rules;
27847
- }
27848
- var LEGACY_ARTIFACT_REL = [
27849
- "index.yaml",
27850
- "journal.md",
27851
- "journal.legacy.md",
27852
- "topics",
27853
- "distill-ledger.yaml",
27854
- "distill-proposal.md"
27855
- ];
27856
- function deleteLegacyArtifacts(baseDir) {
27857
- const deleted = [];
27858
- for (const rel2 of LEGACY_ARTIFACT_REL) {
27859
- const abs = join(baseDir, rel2);
27860
- if (!existsSync(abs)) continue;
27861
- rmSync(abs, { recursive: true, force: true });
27862
- deleted.push(abs);
27863
- }
27864
- return deleted;
27865
- }
27866
-
27867
- // src/lessons/import-legacy-merge.ts
27868
- async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
27869
- let addedLessons = 0;
27870
- const addedTriggers = /* @__PURE__ */ new Set();
27871
- const touchedTopics = /* @__PURE__ */ new Set();
27872
- await mutateLessonsGraphLocked(projectRoot, (g) => {
27873
- addedLessons = 0;
27874
- addedTriggers.clear();
27875
- touchedTopics.clear();
27876
- for (const spec of specs) {
27877
- const result = addLessonInto(g, spec, {
27878
- allowNewTopic: true,
27879
- topicSummary: summaryByTopic.get(spec.topic),
27880
- // Legacy lessons may predate the ≥1-trigger requirement; recover them
27881
- // as-is rather than dropping historical knowledge.
27882
- allowNoTrigger: true
27883
- });
27884
- if (result.isNewLesson) addedLessons += 1;
27885
- for (const t of result.newTriggerIds) addedTriggers.add(t);
27886
- touchedTopics.add(spec.topic);
27887
- }
27888
- });
27889
- const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
27890
- return {
27891
- wroteGraphPath: paths.graph,
27892
- deletedPaths,
27893
- topicCount: touchedTopics.size,
27894
- lessonCount: addedLessons,
27895
- triggerCount: addedTriggers.size
27896
- };
27897
- }
27898
-
27899
- // src/lessons/import-legacy.ts
27900
- var LessonsGraphExistsError = class extends Error {
27901
- code = "LESSONS_GRAPH_EXISTS";
27902
- constructor() {
27903
- super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
27904
- this.name = "LessonsGraphExistsError";
27905
- }
27906
- };
27907
- async function importLegacyLessons(projectRoot, options) {
27908
- const paths = lessonsPaths(projectRoot);
27909
- const indexRaw = readFileSync(paths.index, "utf8");
27910
- const index = LegacyIndexSchema.parse(parse(indexRaw));
27911
- const topics = {};
27912
- const triggersById = /* @__PURE__ */ new Map();
27913
- const triggerIdByKey = /* @__PURE__ */ new Map();
27914
- const lessons = {};
27915
- const specs = [];
27916
- const summaryByTopic = /* @__PURE__ */ new Map();
27917
- for (const cluster of index.clusters) {
27918
- topics[cluster.topic] = { summary: cluster.summary };
27919
- summaryByTopic.set(cluster.topic, cluster.summary);
27920
- const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
27921
- const topicFile = join(projectRoot, cluster.file);
27922
- if (!existsSync(topicFile)) {
27923
- throw new Error(
27924
- `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
27925
- );
27926
- }
27927
- const topicMarkdown = readFileSync(topicFile, "utf8");
27928
- for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
27929
- const lessonEvidence = [
27930
- `legacy:${cluster.file}#rule-${ruleIndex}`,
27931
- ...evidence.map((e) => `legacy:${e}`)
27932
- ];
27933
- lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
27934
- rule: body,
27935
- topics: [cluster.topic],
27936
- triggers: clusterTriggerIds,
27937
- evidence: lessonEvidence,
27938
- status: "active",
27939
- createdAt: options.migratedAt
27940
- };
27941
- specs.push({
27942
- rule: body,
27943
- topic: cluster.topic,
27944
- triggers: {
27945
- files: cluster.triggers.file_globs,
27946
- commands: cluster.triggers.command_patterns,
27947
- keywords: cluster.triggers.keywords
27948
- },
27949
- evidence: lessonEvidence,
27950
- createdAt: options.migratedAt
27871
+ }
27872
+ if (knownPaths !== void 0) {
27873
+ const dead = deadFileGlobIds(graph, knownPaths);
27874
+ const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
27875
+ if (deadHere.length > 0) {
27876
+ warnings.push({
27877
+ code: "DEAD_GLOB",
27878
+ message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
27879
+ });
27880
+ }
27881
+ const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
27882
+ if (wide.length > 0) {
27883
+ warnings.push({
27884
+ code: "WIDE_GLOB_MATCH",
27885
+ message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
27951
27886
  });
27952
27887
  }
27953
27888
  }
27954
- if (options.merge === true)
27955
- return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
27956
- const triggers = Object.fromEntries(triggersById.entries());
27957
- await mutateLessonsGraphLocked(projectRoot, (g) => {
27958
- const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
27959
- if (options.force !== true && populated) {
27960
- throw new LessonsGraphExistsError();
27889
+ return warnings;
27890
+ }
27891
+
27892
+ // src/lessons/capture-near-duplicate.ts
27893
+ var NEAR_DUPLICATE_THRESHOLD = 0.6;
27894
+ function nearDuplicateWarning(graph, lessonId) {
27895
+ const subject = graph.lessons[lessonId];
27896
+ if (subject === void 0) return null;
27897
+ const subjectTokens = new Set(tokenize(subject.rule));
27898
+ if (subjectTokens.size === 0) return null;
27899
+ let best = null;
27900
+ for (const [id, other] of Object.entries(graph.lessons)) {
27901
+ if (id === lessonId || other.status !== "active") continue;
27902
+ const otherTokens = new Set(tokenize(other.rule));
27903
+ if (otherTokens.size === 0) continue;
27904
+ const score = jaccard(subjectTokens, otherTokens);
27905
+ if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
27906
+ best = { id, score };
27961
27907
  }
27962
- g.version = CURRENT_GRAPH_VERSION;
27963
- g.lessons = lessons;
27964
- g.topics = topics;
27965
- g.triggers = triggers;
27966
- });
27967
- const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
27908
+ }
27909
+ if (best === null) return null;
27968
27910
  return {
27969
- wroteGraphPath: paths.graph,
27970
- deletedPaths,
27971
- topicCount: Object.keys(topics).length,
27972
- lessonCount: Object.keys(lessons).length,
27973
- triggerCount: triggersById.size
27911
+ code: "NEAR_DUPLICATE_LESSON",
27912
+ message: `Lesson "${lessonId}" closely resembles active lesson "${best.id}" (~${Math.round(best.score * 100)}% token overlap); consider updating "${best.id}" instead of adding a paraphrase (recall would surface both).`
27974
27913
  };
27975
27914
  }
27976
-
27977
- // src/lessons/auto-migrate.ts
27978
- function todayIso2() {
27979
- const now = /* @__PURE__ */ new Date();
27980
- const y = now.getUTCFullYear();
27981
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
27982
- const d = String(now.getUTCDate()).padStart(2, "0");
27983
- return `${y}-${m}-${d}`;
27984
- }
27985
- async function maybeAutoMigrateLessons(projectRoot) {
27986
- if (existsSync(graphFilePath(projectRoot))) return false;
27987
- const paths = lessonsPaths(projectRoot);
27988
- if (!existsSync(paths.index)) return false;
27989
- try {
27990
- await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
27991
- return true;
27992
- } catch (err) {
27993
- if (err instanceof LessonsGraphExistsError) return false;
27994
- throw err;
27995
- }
27915
+ function jaccard(a, b) {
27916
+ let intersection = 0;
27917
+ for (const t of a) if (b.has(t)) intersection += 1;
27918
+ return intersection / (a.size + b.size - intersection);
27996
27919
  }
27997
27920
 
27998
27921
  // src/utils/filesystem/process-lock.ts
@@ -28293,35 +28216,135 @@ function findExistingLessonByRule(graph, ruleKey) {
28293
28216
  return null;
28294
28217
  }
28295
28218
 
28296
- // src/lessons/ranking-signals.ts
28297
- function buildFanout(graph) {
28298
- const fanout = /* @__PURE__ */ new Map();
28299
- for (const lesson of Object.values(graph.lessons)) {
28300
- if (lesson.status !== "active") continue;
28301
- for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
28302
- }
28303
- return fanout;
28219
+ // src/lessons/import-legacy-merge.ts
28220
+ async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
28221
+ let addedLessons = 0;
28222
+ const addedTriggers = /* @__PURE__ */ new Set();
28223
+ const touchedTopics = /* @__PURE__ */ new Set();
28224
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
28225
+ addedLessons = 0;
28226
+ addedTriggers.clear();
28227
+ touchedTopics.clear();
28228
+ for (const spec of specs) {
28229
+ const result = addLessonInto(g, spec, {
28230
+ allowNewTopic: true,
28231
+ topicSummary: summaryByTopic.get(spec.topic),
28232
+ // Legacy lessons may predate the ≥1-trigger requirement; recover them
28233
+ // as-is rather than dropping historical knowledge.
28234
+ allowNoTrigger: true
28235
+ });
28236
+ if (result.isNewLesson) addedLessons += 1;
28237
+ for (const t of result.newTriggerIds) addedTriggers.add(t);
28238
+ touchedTopics.add(spec.topic);
28239
+ }
28240
+ });
28241
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
28242
+ return {
28243
+ wroteGraphPath: paths.graph,
28244
+ deletedPaths,
28245
+ topicCount: touchedTopics.size,
28246
+ lessonCount: addedLessons,
28247
+ triggerCount: addedTriggers.size
28248
+ };
28304
28249
  }
28305
- function buildTopicCoherence(matches) {
28306
- const topicCount = /* @__PURE__ */ new Map();
28307
- for (const { lesson } of matches) {
28308
- for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
28250
+
28251
+ // src/lessons/import-legacy.ts
28252
+ var LessonsGraphExistsError = class extends Error {
28253
+ code = "LESSONS_GRAPH_EXISTS";
28254
+ constructor() {
28255
+ super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
28256
+ this.name = "LessonsGraphExistsError";
28309
28257
  }
28310
- const coherence = /* @__PURE__ */ new Map();
28311
- for (const { id, lesson } of matches) {
28312
- let best = 0;
28313
- for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
28314
- coherence.set(id, best);
28258
+ };
28259
+ async function importLegacyLessons(projectRoot, options) {
28260
+ const paths = lessonsPaths(projectRoot);
28261
+ const indexRaw = readFileSync(paths.index, "utf8");
28262
+ const index = LegacyIndexSchema.parse(parse(indexRaw));
28263
+ const topics = {};
28264
+ const triggersById = /* @__PURE__ */ new Map();
28265
+ const triggerIdByKey = /* @__PURE__ */ new Map();
28266
+ const lessons = {};
28267
+ const specs = [];
28268
+ const summaryByTopic = /* @__PURE__ */ new Map();
28269
+ for (const cluster of index.clusters) {
28270
+ topics[cluster.topic] = { summary: cluster.summary };
28271
+ summaryByTopic.set(cluster.topic, cluster.summary);
28272
+ const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
28273
+ const topicFile = join(projectRoot, cluster.file);
28274
+ if (!existsSync(topicFile)) {
28275
+ throw new Error(
28276
+ `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
28277
+ );
28278
+ }
28279
+ const topicMarkdown = readFileSync(topicFile, "utf8");
28280
+ for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
28281
+ const lessonEvidence = [
28282
+ `legacy:${cluster.file}#rule-${ruleIndex}`,
28283
+ ...evidence.map((e) => `legacy:${e}`)
28284
+ ];
28285
+ lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
28286
+ rule: body,
28287
+ topics: [cluster.topic],
28288
+ triggers: clusterTriggerIds,
28289
+ evidence: lessonEvidence,
28290
+ status: "active",
28291
+ createdAt: options.migratedAt
28292
+ };
28293
+ specs.push({
28294
+ rule: body,
28295
+ topic: cluster.topic,
28296
+ triggers: {
28297
+ files: cluster.triggers.file_globs,
28298
+ commands: cluster.triggers.command_patterns,
28299
+ keywords: cluster.triggers.keywords
28300
+ },
28301
+ evidence: lessonEvidence,
28302
+ createdAt: options.migratedAt
28303
+ });
28304
+ }
28315
28305
  }
28316
- return coherence;
28317
- }
28318
- function recallLogPath(projectRoot) {
28319
- return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
28306
+ if (options.merge === true)
28307
+ return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
28308
+ const triggers = Object.fromEntries(triggersById.entries());
28309
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
28310
+ const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
28311
+ if (options.force !== true && populated) {
28312
+ throw new LessonsGraphExistsError();
28313
+ }
28314
+ g.version = CURRENT_GRAPH_VERSION;
28315
+ g.lessons = lessons;
28316
+ g.topics = topics;
28317
+ g.triggers = triggers;
28318
+ });
28319
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
28320
+ return {
28321
+ wroteGraphPath: paths.graph,
28322
+ deletedPaths,
28323
+ topicCount: Object.keys(topics).length,
28324
+ lessonCount: Object.keys(lessons).length,
28325
+ triggerCount: triggersById.size
28326
+ };
28320
28327
  }
28321
28328
 
28322
- // src/lessons/capture-telemetry.ts
28323
- function captureLogPath(projectRoot) {
28324
- return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
28329
+ // src/lessons/auto-migrate.ts
28330
+ function todayIso2() {
28331
+ const now = /* @__PURE__ */ new Date();
28332
+ const y = now.getUTCFullYear();
28333
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
28334
+ const d = String(now.getUTCDate()).padStart(2, "0");
28335
+ return `${y}-${m}-${d}`;
28336
+ }
28337
+ async function maybeAutoMigrateLessons(projectRoot) {
28338
+ if (existsSync(graphFilePath(projectRoot))) return false;
28339
+ const paths = lessonsPaths(projectRoot);
28340
+ if (!existsSync(paths.index)) return false;
28341
+ try {
28342
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
28343
+ return true;
28344
+ } catch (err) {
28345
+ if (err instanceof LessonsGraphExistsError) return false;
28346
+ throw err;
28347
+ }
28325
28348
  }
28326
28349
 
28327
28350
  // src/lessons/keyword-match.ts
@@ -28409,6 +28432,29 @@ function triggerMatches(trigger, query, budget) {
28409
28432
  }
28410
28433
  }
28411
28434
 
28435
+ // src/lessons/ranking-signals.ts
28436
+ function buildFanout(graph) {
28437
+ const fanout = /* @__PURE__ */ new Map();
28438
+ for (const lesson of Object.values(graph.lessons)) {
28439
+ if (lesson.status !== "active") continue;
28440
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
28441
+ }
28442
+ return fanout;
28443
+ }
28444
+ function buildTopicCoherence(matches) {
28445
+ const topicCount = /* @__PURE__ */ new Map();
28446
+ for (const { lesson } of matches) {
28447
+ for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
28448
+ }
28449
+ const coherence = /* @__PURE__ */ new Map();
28450
+ for (const { id, lesson } of matches) {
28451
+ let best = 0;
28452
+ for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
28453
+ coherence.set(id, best);
28454
+ }
28455
+ return coherence;
28456
+ }
28457
+
28412
28458
  // src/lessons/ranking.ts
28413
28459
  var DEFAULT_RECALL_LIMIT = 10;
28414
28460
  var DEFAULT_RECALL_MAX_TOKENS = 400;
@@ -28502,12 +28548,21 @@ function defaultLessonsConfig() {
28502
28548
  return {
28503
28549
  recallLimit: DEFAULT_RECALL_LIMIT,
28504
28550
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
28505
- autoPrune: false
28551
+ autoPrune: false,
28552
+ repairTriggers: false
28506
28553
  };
28507
28554
  }
28555
+ function recallLogPath(projectRoot) {
28556
+ return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
28557
+ }
28558
+
28559
+ // src/lessons/outcome-log.ts
28508
28560
  function outcomeLogPath(projectRoot) {
28509
28561
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
28510
28562
  }
28563
+ function captureLogPath(projectRoot) {
28564
+ return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
28565
+ }
28511
28566
 
28512
28567
  // src/lessons/merge.ts
28513
28568
  async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {