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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dist/cli.js +211 -207
- package/dist/engine.d.ts +9 -1
- package/dist/engine.js +56 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +355 -300
- package/dist/index.js.map +1 -1
- package/dist/{init-PvpXanVd.d.ts → init-CrZhoNTj.d.ts} +74 -74
- package/dist/lessons.d.ts +8 -4
- package/dist/lessons.js +687 -588
- package/dist/lessons.js.map +1 -1
- package/package.json +2 -2
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
|
|
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 =
|
|
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
|
|
27752
|
+
var TRIGGER_PREFIX2 = {
|
|
27602
27753
|
file_glob: "glob",
|
|
27603
27754
|
command_pattern: "cmd",
|
|
27604
27755
|
keyword: "kw"
|
|
27605
27756
|
};
|
|
27606
|
-
function
|
|
27757
|
+
function makeTriggerId2(spec) {
|
|
27607
27758
|
const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
|
|
27608
|
-
return `t-${
|
|
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
|
-
|
|
27955
|
-
|
|
27956
|
-
|
|
27957
|
-
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
|
|
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
|
-
|
|
27963
|
-
|
|
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
|
-
|
|
27970
|
-
|
|
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
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
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/
|
|
28297
|
-
function
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
|
|
28301
|
-
|
|
28302
|
-
|
|
28303
|
-
|
|
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
|
-
|
|
28306
|
-
|
|
28307
|
-
|
|
28308
|
-
|
|
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
|
-
|
|
28311
|
-
|
|
28312
|
-
|
|
28313
|
-
|
|
28314
|
-
|
|
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
|
-
|
|
28317
|
-
|
|
28318
|
-
|
|
28319
|
-
|
|
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/
|
|
28323
|
-
function
|
|
28324
|
-
|
|
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 = {}) {
|