agentsmesh 0.33.0 → 0.34.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 +20 -0
- package/README.md +72 -121
- package/dist/canonical.js +491 -274
- package/dist/canonical.js.map +1 -1
- package/dist/cli.js +254 -253
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +569 -242
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +772 -401
- package/dist/index.js.map +1 -1
- package/dist/{init-rvKjGLTB.d.ts → init-ChDbKSJ0.d.ts} +8 -8
- package/dist/lessons.d.ts +2 -2
- package/dist/lessons.js +1121 -973
- package/dist/lessons.js.map +1 -1
- package/dist/{target-descriptor-DeS4XOtV.d.ts → target-descriptor-DtgeHKPG.d.ts} +7 -0
- package/dist/targets.d.ts +2 -2
- package/dist/targets.js +331 -110
- package/dist/targets.js.map +1 -1
- package/package.json +27 -22
package/dist/lessons.js
CHANGED
|
@@ -280,6 +280,23 @@ function todayIso() {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// src/lessons/add-errors.ts
|
|
283
|
+
var EmptyRuleError = class extends Error {
|
|
284
|
+
code = "EMPTY_RULE";
|
|
285
|
+
constructor() {
|
|
286
|
+
super("Lesson rule must not be empty \u2014 pass one imperative sentence.");
|
|
287
|
+
this.name = "EmptyRuleError";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var BroadCommandPatternError = class extends Error {
|
|
291
|
+
constructor(pattern) {
|
|
292
|
+
super(
|
|
293
|
+
`Command pattern ${JSON.stringify(pattern)} matches nearly every command, so it would fire on every recall. Key it on the action instead \u2014 a word-bounded program + subcommand (e.g. "\\bgit commit\\b", "\\brm\\b").`
|
|
294
|
+
);
|
|
295
|
+
this.pattern = pattern;
|
|
296
|
+
this.name = "BroadCommandPatternError";
|
|
297
|
+
}
|
|
298
|
+
code = "BROAD_COMMAND_PATTERN";
|
|
299
|
+
};
|
|
283
300
|
var UnknownTopicError = class extends Error {
|
|
284
301
|
constructor(topic) {
|
|
285
302
|
super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
|
|
@@ -319,992 +336,1144 @@ var UnrecallableLessonError = class extends Error {
|
|
|
319
336
|
code = "UNRECALLABLE_LESSON";
|
|
320
337
|
};
|
|
321
338
|
|
|
322
|
-
// src/lessons/
|
|
323
|
-
var
|
|
324
|
-
var
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"and",
|
|
333
|
-
"or",
|
|
334
|
-
"for",
|
|
335
|
-
"is",
|
|
336
|
-
"on",
|
|
337
|
-
"at",
|
|
338
|
-
"with",
|
|
339
|
-
"be",
|
|
340
|
-
"as",
|
|
341
|
-
"it",
|
|
342
|
-
"that",
|
|
343
|
-
"this",
|
|
344
|
-
"its",
|
|
345
|
-
"must"
|
|
346
|
-
]);
|
|
347
|
-
function tokenize(text) {
|
|
348
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
349
|
-
}
|
|
350
|
-
function queryTerms(query) {
|
|
351
|
-
const parts = [];
|
|
352
|
-
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
353
|
-
if (query.file !== void 0) parts.push(query.file);
|
|
354
|
-
if (query.command !== void 0) parts.push(query.command);
|
|
355
|
-
return tokenize(parts.join(" "));
|
|
356
|
-
}
|
|
357
|
-
function buildCorpus(graph) {
|
|
358
|
-
const docs = [];
|
|
359
|
-
const df = /* @__PURE__ */ new Map();
|
|
360
|
-
let total = 0;
|
|
361
|
-
let n = 0;
|
|
362
|
-
for (const lesson of Object.values(graph.lessons)) {
|
|
363
|
-
if (lesson.status !== "active") continue;
|
|
364
|
-
const toks = tokenize(lesson.rule);
|
|
365
|
-
n += 1;
|
|
366
|
-
total += toks.length;
|
|
367
|
-
docs.push(toks.length);
|
|
368
|
-
for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
339
|
+
// src/lessons/regex-linear/nfa-compile.ts
|
|
340
|
+
var MAX_NFA_STATES = 2e3;
|
|
341
|
+
var Builder = class {
|
|
342
|
+
states = [];
|
|
343
|
+
alloc() {
|
|
344
|
+
if (this.states.length >= MAX_NFA_STATES) {
|
|
345
|
+
throw new Error(`NFA state limit exceeded (${MAX_NFA_STATES}); pattern expands too large`);
|
|
346
|
+
}
|
|
347
|
+
this.states.push({ eps: [], asserts: [], chars: [] });
|
|
348
|
+
return this.states.length - 1;
|
|
369
349
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return { idf, avgdl: total / N || 1 };
|
|
350
|
+
};
|
|
351
|
+
function isNonLineTerminator(c) {
|
|
352
|
+
return c !== "\n" && c !== "\r" && c !== "\u2028" && c !== "\u2029";
|
|
374
353
|
}
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
354
|
+
function compileNode(b, node) {
|
|
355
|
+
switch (node.k) {
|
|
356
|
+
case "empty":
|
|
357
|
+
case "assert": {
|
|
358
|
+
const s = b.alloc();
|
|
359
|
+
const e = b.alloc();
|
|
360
|
+
if (node.k === "assert") b.states[s].asserts.push({ kind: node.kind, target: e });
|
|
361
|
+
else b.states[s].eps.push(e);
|
|
362
|
+
return { start: s, end: e };
|
|
363
|
+
}
|
|
364
|
+
case "char":
|
|
365
|
+
case "any":
|
|
366
|
+
case "class": {
|
|
367
|
+
const s = b.alloc();
|
|
368
|
+
const e = b.alloc();
|
|
369
|
+
const test = node.k === "char" ? (c) => c === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
|
|
370
|
+
b.states[s].chars.push({ test, target: e });
|
|
371
|
+
return { start: s, end: e };
|
|
372
|
+
}
|
|
373
|
+
case "concat": {
|
|
374
|
+
if (node.items.length === 0) return compileNode(b, { k: "empty" });
|
|
375
|
+
let first = null;
|
|
376
|
+
let prevEnd = -1;
|
|
377
|
+
for (const item of node.items) {
|
|
378
|
+
const frag = compileNode(b, item);
|
|
379
|
+
if (first === null) first = frag;
|
|
380
|
+
else b.states[prevEnd].eps.push(frag.start);
|
|
381
|
+
prevEnd = frag.end;
|
|
382
|
+
}
|
|
383
|
+
return { start: first.start, end: prevEnd };
|
|
384
|
+
}
|
|
385
|
+
case "alt": {
|
|
386
|
+
const s = b.alloc();
|
|
387
|
+
const e = b.alloc();
|
|
388
|
+
for (const opt of node.opts) {
|
|
389
|
+
const frag = compileNode(b, opt);
|
|
390
|
+
b.states[s].eps.push(frag.start);
|
|
391
|
+
b.states[frag.end].eps.push(e);
|
|
392
|
+
}
|
|
393
|
+
return { start: s, end: e };
|
|
394
|
+
}
|
|
395
|
+
case "opt": {
|
|
396
|
+
const s = b.alloc();
|
|
397
|
+
const e = b.alloc();
|
|
398
|
+
const frag = compileNode(b, node.node);
|
|
399
|
+
b.states[s].eps.push(frag.start, e);
|
|
400
|
+
b.states[frag.end].eps.push(e);
|
|
401
|
+
return { start: s, end: e };
|
|
402
|
+
}
|
|
403
|
+
case "star": {
|
|
404
|
+
const s = b.alloc();
|
|
405
|
+
const e = b.alloc();
|
|
406
|
+
const frag = compileNode(b, node.node);
|
|
407
|
+
b.states[s].eps.push(frag.start, e);
|
|
408
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
409
|
+
return { start: s, end: e };
|
|
410
|
+
}
|
|
411
|
+
case "plus": {
|
|
412
|
+
const e = b.alloc();
|
|
413
|
+
const frag = compileNode(b, node.node);
|
|
414
|
+
b.states[frag.end].eps.push(frag.start, e);
|
|
415
|
+
return { start: frag.start, end: e };
|
|
416
|
+
}
|
|
386
417
|
}
|
|
387
|
-
|
|
418
|
+
}
|
|
419
|
+
function compileNfa(ast) {
|
|
420
|
+
const b = new Builder();
|
|
421
|
+
const { start, end } = compileNode(b, ast);
|
|
422
|
+
return { states: b.states, start, accept: end };
|
|
388
423
|
}
|
|
389
424
|
|
|
390
|
-
// src/lessons/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
// src/lessons/regex-linear/nfa.ts
|
|
426
|
+
function wordBoundary(input, pos) {
|
|
427
|
+
const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
|
|
428
|
+
const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
|
|
429
|
+
return before !== after;
|
|
394
430
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
431
|
+
function assertHolds(kind, input, pos) {
|
|
432
|
+
switch (kind) {
|
|
433
|
+
case "start":
|
|
434
|
+
return pos === 0;
|
|
435
|
+
case "end":
|
|
436
|
+
return pos === input.length;
|
|
437
|
+
case "wordB":
|
|
438
|
+
return wordBoundary(input, pos);
|
|
439
|
+
case "nonWordB":
|
|
440
|
+
return !wordBoundary(input, pos);
|
|
441
|
+
}
|
|
397
442
|
}
|
|
398
|
-
function
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
443
|
+
function buildMatcher(ast) {
|
|
444
|
+
const { states, start, accept } = compileNfa(ast);
|
|
445
|
+
const closure = (set, idx, input, pos, budget) => {
|
|
446
|
+
const stack = [idx];
|
|
447
|
+
while (stack.length > 0) {
|
|
448
|
+
if (budget.remaining <= 0) return;
|
|
449
|
+
const cur = stack.pop();
|
|
450
|
+
if (set.has(cur)) continue;
|
|
451
|
+
set.add(cur);
|
|
452
|
+
budget.remaining -= 1;
|
|
453
|
+
for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
|
|
454
|
+
for (const a of states[cur].asserts) {
|
|
455
|
+
if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
return {
|
|
460
|
+
// The input is matched in full (no truncation — truncation would miss suffix
|
|
461
|
+
// matches and let `$` falsely match an invented endpoint). Work is bounded by
|
|
462
|
+
// the shared budget instead: when it runs out we report a safe non-match.
|
|
463
|
+
test(input, budget) {
|
|
464
|
+
const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
|
|
465
|
+
if (b.remaining <= 0) return false;
|
|
466
|
+
let current = /* @__PURE__ */ new Set();
|
|
467
|
+
for (let pos = 0; pos <= input.length; pos += 1) {
|
|
468
|
+
closure(current, start, input, pos, b);
|
|
469
|
+
if (current.has(accept)) return true;
|
|
470
|
+
if (b.remaining <= 0) return false;
|
|
471
|
+
if (pos === input.length) break;
|
|
472
|
+
const ch = input[pos];
|
|
473
|
+
const next = /* @__PURE__ */ new Set();
|
|
474
|
+
for (const s of current) {
|
|
475
|
+
b.remaining -= 1;
|
|
476
|
+
for (const t of states[s].chars) {
|
|
477
|
+
if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
current = next;
|
|
481
|
+
}
|
|
482
|
+
return current.has(accept);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
402
485
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
486
|
+
|
|
487
|
+
// src/lessons/regex-linear/ast.ts
|
|
488
|
+
var UnsupportedRegexError = class extends Error {
|
|
489
|
+
constructor(message) {
|
|
490
|
+
super(message);
|
|
491
|
+
this.name = "UnsupportedRegexError";
|
|
408
492
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// src/lessons/regex-linear/parse-helpers.ts
|
|
496
|
+
var MAX_REPEAT = 1e3;
|
|
497
|
+
var isWord = (c) => /[A-Za-z0-9_]/.test(c);
|
|
498
|
+
function expandRepeat(atom, min, max) {
|
|
499
|
+
const items = [];
|
|
500
|
+
for (let k = 0; k < min; k += 1) items.push(atom);
|
|
501
|
+
if (max === Infinity) {
|
|
502
|
+
items.push({ k: "star", node: atom });
|
|
503
|
+
} else {
|
|
504
|
+
for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
|
|
420
505
|
}
|
|
421
|
-
return
|
|
506
|
+
if (items.length === 0) return { k: "empty" };
|
|
507
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
422
508
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
509
|
+
function escapeClass(c) {
|
|
510
|
+
switch (c) {
|
|
511
|
+
case "d":
|
|
512
|
+
return (x) => x >= "0" && x <= "9";
|
|
513
|
+
case "D":
|
|
514
|
+
return (x) => !(x >= "0" && x <= "9");
|
|
515
|
+
case "w":
|
|
516
|
+
return isWord;
|
|
517
|
+
case "W":
|
|
518
|
+
return (x) => !isWord(x);
|
|
519
|
+
case "s":
|
|
520
|
+
return (x) => /\s/.test(x);
|
|
521
|
+
case "S":
|
|
522
|
+
return (x) => !/\s/.test(x);
|
|
523
|
+
default:
|
|
524
|
+
return null;
|
|
431
525
|
}
|
|
432
526
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
527
|
+
var HEX2 = /^[0-9a-fA-F]{2}$/;
|
|
528
|
+
var HEX4 = /^[0-9a-fA-F]{4}$/;
|
|
529
|
+
function readUnicodeEscape(src, i, c) {
|
|
530
|
+
if (c === "x") {
|
|
531
|
+
const hex2 = src.slice(i, i + 2);
|
|
532
|
+
return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
|
|
533
|
+
}
|
|
534
|
+
const hex = src.slice(i, i + 4);
|
|
535
|
+
return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
|
|
438
536
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (trigger.kind !== "command_pattern") continue;
|
|
444
|
-
if (!active.has(triggerId)) continue;
|
|
445
|
-
if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
|
|
446
|
-
findings.push({
|
|
447
|
-
level: "warning",
|
|
448
|
-
code: "RUNNER_ANCHORED_PATTERN",
|
|
449
|
-
message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) is anchored to one runner \u2014 it won't fire for the same task via another runner (e.g. \`npx\` vs \`pnpm\`). Drop the \`^<runner>\` anchor and key on the task (e.g. \`\\bvitest\\b\`).`,
|
|
450
|
-
triggerId
|
|
451
|
-
});
|
|
537
|
+
function readControlEscape(src, i) {
|
|
538
|
+
const x = src[i];
|
|
539
|
+
if (x === void 0 || !/[A-Za-z]/.test(x)) {
|
|
540
|
+
throw new UnsupportedRegexError("\\c must be followed by a letter");
|
|
452
541
|
}
|
|
542
|
+
return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
|
|
453
543
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const p = pattern.trim();
|
|
460
|
-
if (p === "*" || p === "**") return true;
|
|
461
|
-
if (!p.includes("**")) return false;
|
|
462
|
-
const basename5 = p.slice(p.lastIndexOf("/") + 1);
|
|
463
|
-
return basename5.startsWith("*");
|
|
544
|
+
function classEscapeChar(src, i, e) {
|
|
545
|
+
if (e === "b") return { ch: "\b", len: 0 };
|
|
546
|
+
if (e === "c") return readControlEscape(src, i);
|
|
547
|
+
if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
|
|
548
|
+
return { ch: escapeLiteral(e), len: 0 };
|
|
464
549
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
550
|
+
function escapeLiteral(c) {
|
|
551
|
+
switch (c) {
|
|
552
|
+
case "t":
|
|
553
|
+
return " ";
|
|
554
|
+
case "n":
|
|
555
|
+
return "\n";
|
|
556
|
+
case "r":
|
|
557
|
+
return "\r";
|
|
558
|
+
case "f":
|
|
559
|
+
return "\f";
|
|
560
|
+
case "v":
|
|
561
|
+
return "\v";
|
|
562
|
+
case "0":
|
|
563
|
+
return "\0";
|
|
564
|
+
default:
|
|
565
|
+
return c;
|
|
474
566
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/lessons/regex-linear/parse.ts
|
|
570
|
+
function parseRegex(src) {
|
|
571
|
+
let i = 0;
|
|
572
|
+
const peek = () => src[i];
|
|
573
|
+
const eat = () => src[i++];
|
|
574
|
+
function parseAlt() {
|
|
575
|
+
const opts = [parseConcat()];
|
|
576
|
+
while (peek() === "|") {
|
|
577
|
+
i += 1;
|
|
578
|
+
opts.push(parseConcat());
|
|
579
|
+
}
|
|
580
|
+
return opts.length === 1 ? opts[0] : { k: "alt", opts };
|
|
482
581
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
582
|
+
function parseConcat() {
|
|
583
|
+
const items = [];
|
|
584
|
+
while (i < src.length && peek() !== "|" && peek() !== ")") {
|
|
585
|
+
items.push(parseQuantified());
|
|
586
|
+
}
|
|
587
|
+
if (items.length === 0) return { k: "empty" };
|
|
588
|
+
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
488
589
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
590
|
+
function parseQuantified() {
|
|
591
|
+
const atom = parseAtom();
|
|
592
|
+
const q = peek();
|
|
593
|
+
if (q === "*" || q === "+" || q === "?") {
|
|
594
|
+
i += 1;
|
|
595
|
+
if (peek() === "?") i += 1;
|
|
596
|
+
return q === "*" ? { k: "star", node: atom } : q === "+" ? { k: "plus", node: atom } : { k: "opt", node: atom };
|
|
597
|
+
}
|
|
598
|
+
if (q === "{") {
|
|
599
|
+
const repeat = tryParseBrace();
|
|
600
|
+
if (repeat !== null) return expandRepeat(atom, repeat.min, repeat.max);
|
|
601
|
+
}
|
|
602
|
+
return atom;
|
|
495
603
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
604
|
+
function tryParseBrace() {
|
|
605
|
+
const m = /^\{(\d+)(,(\d*)?)?\}/.exec(src.slice(i));
|
|
606
|
+
if (m === null) return null;
|
|
607
|
+
i += m[0].length;
|
|
608
|
+
if (peek() === "?") i += 1;
|
|
609
|
+
const min = Number(m[1]);
|
|
610
|
+
const max = m[2] === void 0 ? min : m[3] === "" || m[3] === void 0 ? Infinity : Number(m[3]);
|
|
611
|
+
if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
|
|
612
|
+
throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
|
|
613
|
+
}
|
|
614
|
+
return { min, max };
|
|
502
615
|
}
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
616
|
+
function parseAtom() {
|
|
617
|
+
const c = peek();
|
|
618
|
+
if (c === "(") return parseGroup();
|
|
619
|
+
if (c === "[") return parseClass();
|
|
620
|
+
if (c === "\\") return parseEscape();
|
|
621
|
+
if (c === ".") {
|
|
622
|
+
i += 1;
|
|
623
|
+
return { k: "any" };
|
|
511
624
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
code: "WIDE_GLOB_MATCH",
|
|
516
|
-
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.`
|
|
517
|
-
});
|
|
625
|
+
if (c === "^") {
|
|
626
|
+
i += 1;
|
|
627
|
+
return { k: "assert", kind: "start" };
|
|
518
628
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
function nearDuplicateWarning(graph, lessonId) {
|
|
526
|
-
const subject = graph.lessons[lessonId];
|
|
527
|
-
if (subject === void 0) return null;
|
|
528
|
-
const subjectTokens = new Set(tokenize(subject.rule));
|
|
529
|
-
if (subjectTokens.size === 0) return null;
|
|
530
|
-
let best = null;
|
|
531
|
-
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
532
|
-
if (id === lessonId || other.status !== "active") continue;
|
|
533
|
-
const otherTokens = new Set(tokenize(other.rule));
|
|
534
|
-
if (otherTokens.size === 0) continue;
|
|
535
|
-
const score = jaccard(subjectTokens, otherTokens);
|
|
536
|
-
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
537
|
-
best = { id, score };
|
|
629
|
+
if (c === "$") {
|
|
630
|
+
i += 1;
|
|
631
|
+
return { k: "assert", kind: "end" };
|
|
632
|
+
}
|
|
633
|
+
if (c === void 0 || c === "*" || c === "+" || c === "?" || c === ")") {
|
|
634
|
+
throw new UnsupportedRegexError(`Unexpected '${c ?? "<end>"}' in pattern`);
|
|
538
635
|
}
|
|
636
|
+
i += 1;
|
|
637
|
+
return { k: "char", ch: c };
|
|
539
638
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
639
|
+
function parseGroup() {
|
|
640
|
+
i += 1;
|
|
641
|
+
if (peek() === "?") {
|
|
642
|
+
const c2 = src[i + 1];
|
|
643
|
+
if (c2 === "=" || c2 === "!" || c2 === "<") {
|
|
644
|
+
if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
|
|
645
|
+
throw new UnsupportedRegexError("Lookaround assertions are not supported");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (c2 === ":") i += 2;
|
|
649
|
+
else if (c2 === "<") {
|
|
650
|
+
i += 2;
|
|
651
|
+
while (i < src.length && src[i] !== ">") i += 1;
|
|
652
|
+
i += 1;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const inner = parseAlt();
|
|
656
|
+
if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
|
|
657
|
+
i += 1;
|
|
658
|
+
return inner;
|
|
559
659
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
this.holder = holder;
|
|
576
|
-
this.label = label;
|
|
577
|
-
}
|
|
578
|
-
};
|
|
579
|
-
var FileSystemError = class extends AgentsMeshError {
|
|
580
|
-
path;
|
|
581
|
-
errnoCode;
|
|
582
|
-
constructor(path, message, options) {
|
|
583
|
-
super("AM_FILESYSTEM", message, options);
|
|
584
|
-
this.name = "FileSystemError";
|
|
585
|
-
this.path = path;
|
|
586
|
-
this.errnoCode = options?.errnoCode;
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
// src/utils/filesystem/process-lock.ts
|
|
591
|
-
var DEFAULT_STALE_MS = 6e4;
|
|
592
|
-
var DEFAULT_RETRIES = 30;
|
|
593
|
-
var DEFAULT_RETRY_DELAY_MS = 200;
|
|
594
|
-
var YOUNG_LOCK_GRACE_MS = 2e3;
|
|
595
|
-
async function acquireProcessLock(lockPath, opts = {}) {
|
|
596
|
-
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
597
|
-
const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
598
|
-
const stale = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
599
|
-
await mkdir(dirname(lockPath), { recursive: true });
|
|
600
|
-
let attempt = 0;
|
|
601
|
-
while (true) {
|
|
602
|
-
const acquired = await tryAcquire(lockPath);
|
|
603
|
-
if (acquired) return acquired;
|
|
604
|
-
const existing = await inspectLock(lockPath);
|
|
605
|
-
if (existing !== "young" && isStale(existing, stale)) {
|
|
606
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
607
|
-
});
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
if (attempt >= retries) {
|
|
611
|
-
const holder = existing === "young" ? null : existing;
|
|
612
|
-
throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
|
|
660
|
+
function parseEscape() {
|
|
661
|
+
i += 1;
|
|
662
|
+
const c = peek();
|
|
663
|
+
if (c === void 0) throw new UnsupportedRegexError("Trailing backslash");
|
|
664
|
+
if (/[1-9]/.test(c) || c === "k")
|
|
665
|
+
throw new UnsupportedRegexError("Backreferences are not supported");
|
|
666
|
+
i += 1;
|
|
667
|
+
if (c === "b") return { k: "assert", kind: "wordB" };
|
|
668
|
+
if (c === "B") return { k: "assert", kind: "nonWordB" };
|
|
669
|
+
const cls = escapeClass(c);
|
|
670
|
+
if (cls !== null) return { k: "class", test: cls };
|
|
671
|
+
if (c === "x" || c === "u" || c === "c") {
|
|
672
|
+
const { ch, len } = c === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c);
|
|
673
|
+
i += len;
|
|
674
|
+
return { k: "char", ch };
|
|
613
675
|
}
|
|
614
|
-
|
|
615
|
-
await sleep(delay);
|
|
676
|
+
return { k: "char", ch: escapeLiteral(c) };
|
|
616
677
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
678
|
+
function parseClass() {
|
|
679
|
+
i += 1;
|
|
680
|
+
const negate = peek() === "^";
|
|
681
|
+
if (negate) i += 1;
|
|
682
|
+
const tests = [];
|
|
683
|
+
while (i < src.length && peek() !== "]") {
|
|
684
|
+
tests.push(parseClassMember());
|
|
685
|
+
}
|
|
686
|
+
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
687
|
+
i += 1;
|
|
688
|
+
const base = (c) => tests.some((t) => t(c));
|
|
689
|
+
return { k: "class", test: negate ? (c) => !base(c) : base };
|
|
624
690
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
638
|
-
} catch {
|
|
691
|
+
function parseClassMember() {
|
|
692
|
+
let lo;
|
|
693
|
+
if (peek() === "\\") {
|
|
694
|
+
i += 1;
|
|
695
|
+
const e = eat();
|
|
696
|
+
const cls = escapeClass(e);
|
|
697
|
+
if (cls !== null) return cls;
|
|
698
|
+
const r = classEscapeChar(src, i, e);
|
|
699
|
+
i += r.len;
|
|
700
|
+
lo = r.ch;
|
|
701
|
+
} else {
|
|
702
|
+
lo = eat();
|
|
639
703
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
async function inspectLock(lockPath) {
|
|
659
|
-
try {
|
|
660
|
-
const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
|
|
661
|
-
const parsed = JSON.parse(raw);
|
|
662
|
-
if (!isLockMetadata(parsed)) return null;
|
|
663
|
-
return parsed;
|
|
664
|
-
} catch {
|
|
665
|
-
try {
|
|
666
|
-
const info = await stat(lockPath);
|
|
667
|
-
const ageMs = Date.now() - info.mtimeMs;
|
|
668
|
-
if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
|
|
669
|
-
} catch {
|
|
704
|
+
if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
|
|
705
|
+
i += 1;
|
|
706
|
+
let hi;
|
|
707
|
+
if (peek() === "\\") {
|
|
708
|
+
i += 1;
|
|
709
|
+
const e2 = eat();
|
|
710
|
+
const r = classEscapeChar(src, i, e2);
|
|
711
|
+
i += r.len;
|
|
712
|
+
hi = r.ch;
|
|
713
|
+
} else {
|
|
714
|
+
hi = eat();
|
|
715
|
+
}
|
|
716
|
+
const a = lo.codePointAt(0);
|
|
717
|
+
const b = hi.codePointAt(0);
|
|
718
|
+
return (c) => {
|
|
719
|
+
const p = c.codePointAt(0);
|
|
720
|
+
return p >= a && p <= b;
|
|
721
|
+
};
|
|
670
722
|
}
|
|
671
|
-
return
|
|
723
|
+
return (c) => c === lo;
|
|
672
724
|
}
|
|
725
|
+
const ast = parseAlt();
|
|
726
|
+
if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
|
|
727
|
+
return ast;
|
|
673
728
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
function isProcessAlive(pid) {
|
|
682
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
729
|
+
|
|
730
|
+
// src/lessons/regex-linear/index.ts
|
|
731
|
+
var cache = /* @__PURE__ */ new Map();
|
|
732
|
+
function compileLinearMatcher(pattern) {
|
|
733
|
+
const hit = cache.get(pattern);
|
|
734
|
+
if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
|
|
735
|
+
let matcher;
|
|
683
736
|
try {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
return err.code === "EPERM";
|
|
737
|
+
matcher = buildMatcher(parseRegex(pattern));
|
|
738
|
+
} catch {
|
|
739
|
+
matcher = null;
|
|
688
740
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (!meta) return "unknown (unreadable lock metadata)";
|
|
692
|
-
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
693
|
-
return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
|
|
694
|
-
}
|
|
695
|
-
function isLockMetadata(value) {
|
|
696
|
-
if (typeof value !== "object" || value === null) return false;
|
|
697
|
-
const v = value;
|
|
698
|
-
return typeof v.pid === "number" && typeof v.started === "number";
|
|
699
|
-
}
|
|
700
|
-
function getHostname() {
|
|
701
|
-
return hostname();
|
|
702
|
-
}
|
|
703
|
-
function sleep(ms) {
|
|
704
|
-
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
741
|
+
cache.set(pattern, matcher);
|
|
742
|
+
return matcher;
|
|
705
743
|
}
|
|
706
744
|
|
|
707
|
-
// src/lessons/
|
|
708
|
-
var
|
|
709
|
-
function
|
|
710
|
-
|
|
745
|
+
// src/lessons/regex-safety.ts
|
|
746
|
+
var MAX_PATTERN_LENGTH = 1e3;
|
|
747
|
+
function isSafeRegexPattern(pattern) {
|
|
748
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
749
|
+
return compileLinearMatcher(pattern) !== null;
|
|
711
750
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
|
|
751
|
+
function getCommandMatcher(pattern) {
|
|
752
|
+
if (pattern.length > MAX_PATTERN_LENGTH) return null;
|
|
753
|
+
return compileLinearMatcher(pattern);
|
|
716
754
|
}
|
|
717
755
|
|
|
718
|
-
// src/lessons/
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
756
|
+
// src/lessons/command-pattern-breadth.ts
|
|
757
|
+
var COMMAND_PROBE_CORPUS = [
|
|
758
|
+
"git status",
|
|
759
|
+
'git commit -m "wip"',
|
|
760
|
+
"pnpm test",
|
|
761
|
+
"npx vitest run src/x.test.ts",
|
|
762
|
+
"ls -la",
|
|
763
|
+
"cat README.md",
|
|
764
|
+
"rm -rf dist",
|
|
765
|
+
"mkdir -p build/out",
|
|
766
|
+
"node scripts/build.js",
|
|
767
|
+
"docker compose up -d",
|
|
768
|
+
"curl -s https://example.com",
|
|
769
|
+
"echo hello > out.txt",
|
|
770
|
+
"sed -i 's/a/b/' file.txt",
|
|
771
|
+
"pnpm lint --fix",
|
|
772
|
+
"python3 -m pytest",
|
|
773
|
+
"cargo build --release",
|
|
774
|
+
"make",
|
|
775
|
+
"npm install --global typescript",
|
|
776
|
+
"cp a.txt b.txt",
|
|
777
|
+
"grep -rn TODO src"
|
|
778
|
+
];
|
|
779
|
+
var BROAD_HIT_RATIO = 0.5;
|
|
780
|
+
var PROBE_BUDGET = 1e5;
|
|
781
|
+
function isBroadCommandPattern(pattern) {
|
|
782
|
+
const matcher = getCommandMatcher(pattern);
|
|
783
|
+
if (matcher === null) return false;
|
|
784
|
+
if (matcher.test("", { remaining: PROBE_BUDGET })) return true;
|
|
785
|
+
let hits = 0;
|
|
786
|
+
for (const command of COMMAND_PROBE_CORPUS) {
|
|
787
|
+
if (matcher.test(command, { remaining: PROBE_BUDGET })) hits += 1;
|
|
751
788
|
}
|
|
789
|
+
return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
|
|
752
790
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
791
|
+
|
|
792
|
+
// src/lessons/ranking-text.ts
|
|
793
|
+
var K1 = 1.5;
|
|
794
|
+
var B = 0.75;
|
|
795
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
796
|
+
"the",
|
|
797
|
+
"a",
|
|
798
|
+
"an",
|
|
799
|
+
"to",
|
|
800
|
+
"of",
|
|
801
|
+
"in",
|
|
802
|
+
"and",
|
|
803
|
+
"or",
|
|
804
|
+
"for",
|
|
805
|
+
"is",
|
|
806
|
+
"on",
|
|
807
|
+
"at",
|
|
808
|
+
"with",
|
|
809
|
+
"be",
|
|
810
|
+
"as",
|
|
811
|
+
"it",
|
|
812
|
+
"that",
|
|
813
|
+
"this",
|
|
814
|
+
"its",
|
|
815
|
+
"must"
|
|
816
|
+
]);
|
|
817
|
+
function tokenize(text) {
|
|
818
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
819
|
+
}
|
|
820
|
+
function queryTerms(query) {
|
|
821
|
+
const parts = [];
|
|
822
|
+
if (query.keyword !== void 0) parts.push(query.keyword);
|
|
823
|
+
if (query.file !== void 0) parts.push(query.file);
|
|
824
|
+
if (query.command !== void 0) parts.push(query.command);
|
|
825
|
+
return tokenize(parts.join(" "));
|
|
826
|
+
}
|
|
827
|
+
function buildCorpus(graph) {
|
|
828
|
+
const docs = [];
|
|
829
|
+
const df = /* @__PURE__ */ new Map();
|
|
830
|
+
let total = 0;
|
|
831
|
+
let n = 0;
|
|
832
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
833
|
+
if (lesson.status !== "active") continue;
|
|
834
|
+
const toks = tokenize(lesson.rule);
|
|
835
|
+
n += 1;
|
|
836
|
+
total += toks.length;
|
|
837
|
+
docs.push(toks.length);
|
|
838
|
+
for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
|
|
773
839
|
}
|
|
840
|
+
const N = Math.max(n, 1);
|
|
841
|
+
const idf = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
|
|
843
|
+
return { idf, avgdl: total / N || 1 };
|
|
774
844
|
}
|
|
775
|
-
function
|
|
776
|
-
const
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
845
|
+
function bm25(terms, ruleText, corpus) {
|
|
846
|
+
const toks = tokenize(ruleText);
|
|
847
|
+
const dl = toks.length || 1;
|
|
848
|
+
const tf = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
850
|
+
let score = 0;
|
|
851
|
+
for (const t of new Set(terms)) {
|
|
852
|
+
const f = tf.get(t) ?? 0;
|
|
853
|
+
if (f === 0) continue;
|
|
854
|
+
const idf = corpus.idf.get(t);
|
|
855
|
+
score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
|
|
781
856
|
}
|
|
782
|
-
return
|
|
857
|
+
return score;
|
|
783
858
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
859
|
+
|
|
860
|
+
// src/lessons/keyword-signal.ts
|
|
861
|
+
var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
|
|
862
|
+
function isLowSignalKeyword(pattern) {
|
|
863
|
+
return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
|
|
864
|
+
}
|
|
865
|
+
function splitRawTokens(pattern) {
|
|
866
|
+
return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
867
|
+
}
|
|
868
|
+
function keywordNeedleLosesTokens(pattern) {
|
|
869
|
+
const raw = splitRawTokens(pattern);
|
|
870
|
+
if (raw.length < 2) return false;
|
|
871
|
+
return tokenize(pattern).length !== raw.length;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/lessons/trigger-effectiveness.ts
|
|
875
|
+
function ineffectiveTriggers(graph, triggerIds) {
|
|
876
|
+
const out = [];
|
|
877
|
+
for (const id of triggerIds) {
|
|
878
|
+
const trigger = graph.triggers[id];
|
|
879
|
+
if (trigger === void 0) continue;
|
|
880
|
+
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
881
|
+
if (reason !== null) out.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
802
882
|
}
|
|
883
|
+
return out;
|
|
803
884
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
findings.push({
|
|
809
|
-
level: "error",
|
|
810
|
-
code: "SELF_SUPERSEDED",
|
|
811
|
-
message: `Lesson "${lessonId}" is superseded by itself.`,
|
|
812
|
-
lessonId
|
|
813
|
-
});
|
|
814
|
-
continue;
|
|
885
|
+
function ineffectiveReason(kind, pattern) {
|
|
886
|
+
if (kind === "keyword") {
|
|
887
|
+
if (tokenize(pattern).length === 0) {
|
|
888
|
+
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
815
889
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
findings.push({
|
|
819
|
-
level: "error",
|
|
820
|
-
code: "INACTIVE_SUPERSEDER",
|
|
821
|
-
message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target.status} \u2014 the chain dead-ends with no live replacement.`,
|
|
822
|
-
lessonId
|
|
823
|
-
});
|
|
890
|
+
if (keywordNeedleLosesTokens(pattern)) {
|
|
891
|
+
return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
|
|
824
892
|
}
|
|
893
|
+
return null;
|
|
825
894
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
let cur = startId;
|
|
833
|
-
while (cur !== void 0) {
|
|
834
|
-
if (seen.has(cur)) {
|
|
835
|
-
if (cur !== startId || reported.has(cur)) break;
|
|
836
|
-
reported.add(cur);
|
|
837
|
-
findings.push({
|
|
838
|
-
level: "error",
|
|
839
|
-
code: "SUPERSEDE_CYCLE",
|
|
840
|
-
message: `Lesson "${startId}" is part of a supersededBy cycle.`,
|
|
841
|
-
lessonId: startId
|
|
842
|
-
});
|
|
843
|
-
break;
|
|
844
|
-
}
|
|
845
|
-
seen.add(cur);
|
|
846
|
-
const next = graph.lessons[cur]?.supersededBy;
|
|
847
|
-
if (next === cur) break;
|
|
848
|
-
cur = next;
|
|
895
|
+
if (kind === "command_pattern") {
|
|
896
|
+
let valid = true;
|
|
897
|
+
try {
|
|
898
|
+
new RegExp(pattern);
|
|
899
|
+
} catch {
|
|
900
|
+
valid = false;
|
|
849
901
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
findings.push({
|
|
856
|
-
level: "warning",
|
|
857
|
-
code: "UNREACHABLE_LESSON",
|
|
858
|
-
message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
|
|
859
|
-
lessonId
|
|
860
|
-
});
|
|
902
|
+
if (!valid) {
|
|
903
|
+
return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
|
|
904
|
+
}
|
|
905
|
+
if (!isSafeRegexPattern(pattern)) {
|
|
906
|
+
return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
|
|
861
907
|
}
|
|
908
|
+
return null;
|
|
862
909
|
}
|
|
910
|
+
return null;
|
|
863
911
|
}
|
|
864
|
-
function
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
912
|
+
function blockingDeadTriggers(graph, triggerIds) {
|
|
913
|
+
return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/lessons/add-gates.ts
|
|
917
|
+
function assertRuleShape(rule) {
|
|
918
|
+
const trimmed = rule.trim();
|
|
919
|
+
if (trimmed.length === 0) throw new EmptyRuleError();
|
|
920
|
+
if (trimmed.length > MAX_RULE_LENGTH) throw new RuleTooLongError(trimmed.length, MAX_RULE_LENGTH);
|
|
921
|
+
return trimmed;
|
|
922
|
+
}
|
|
923
|
+
function skipsTriggerGates(input, options) {
|
|
924
|
+
return options.allowNoTrigger === true || input.scope === "always";
|
|
925
|
+
}
|
|
926
|
+
function countInputTriggers(triggers) {
|
|
927
|
+
return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
|
|
928
|
+
}
|
|
929
|
+
function assertTriggerInputs(input, options, existingTriggerCount) {
|
|
930
|
+
if (!skipsTriggerGates(input, options) && countInputTriggers(input.triggers) === 0 && existingTriggerCount === 0) {
|
|
931
|
+
throw new NoTriggerError();
|
|
870
932
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
level: "warning",
|
|
875
|
-
code: "ORPHAN_TOPIC",
|
|
876
|
-
message: `Topic "${topicId}" is not referenced by any lesson.`,
|
|
877
|
-
topicId
|
|
878
|
-
});
|
|
879
|
-
}
|
|
933
|
+
if (options.allowNoTrigger !== true) {
|
|
934
|
+
const broad = (input.triggers.commands ?? []).find(isBroadCommandPattern);
|
|
935
|
+
if (broad !== void 0) throw new BroadCommandPatternError(broad);
|
|
880
936
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
message: `Trigger "${triggerId}" is not referenced by any lesson.`,
|
|
887
|
-
triggerId
|
|
888
|
-
});
|
|
889
|
-
}
|
|
937
|
+
}
|
|
938
|
+
function assertRecallable(graph, resultingTriggers) {
|
|
939
|
+
const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
|
|
940
|
+
if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
|
|
941
|
+
throw new UnrecallableLessonError(blockingDead);
|
|
890
942
|
}
|
|
891
943
|
}
|
|
892
944
|
|
|
893
|
-
// src/lessons/
|
|
894
|
-
var
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
945
|
+
// src/lessons/glob-breadth.ts
|
|
946
|
+
var WILDCARD = /[*?[\]]/;
|
|
947
|
+
function globNarrowness(pattern) {
|
|
948
|
+
const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
949
|
+
if (segments.length === 0) return 0;
|
|
950
|
+
let literal = 0;
|
|
951
|
+
let globstars = 0;
|
|
952
|
+
for (const segment of segments) {
|
|
953
|
+
if (segment === "**") globstars += 1;
|
|
954
|
+
else if (!WILDCARD.test(segment)) literal += 1;
|
|
903
955
|
}
|
|
904
|
-
|
|
905
|
-
function isNonLineTerminator(c) {
|
|
906
|
-
return c !== "\n" && c !== "\r" && c !== "\u2028" && c !== "\u2029";
|
|
956
|
+
return literal / (segments.length + globstars);
|
|
907
957
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
case "char":
|
|
919
|
-
case "any":
|
|
920
|
-
case "class": {
|
|
921
|
-
const s = b.alloc();
|
|
922
|
-
const e = b.alloc();
|
|
923
|
-
const test = node.k === "char" ? (c) => c === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
|
|
924
|
-
b.states[s].chars.push({ test, target: e });
|
|
925
|
-
return { start: s, end: e };
|
|
926
|
-
}
|
|
927
|
-
case "concat": {
|
|
928
|
-
if (node.items.length === 0) return compileNode(b, { k: "empty" });
|
|
929
|
-
let first = null;
|
|
930
|
-
let prevEnd = -1;
|
|
931
|
-
for (const item of node.items) {
|
|
932
|
-
const frag = compileNode(b, item);
|
|
933
|
-
if (first === null) first = frag;
|
|
934
|
-
else b.states[prevEnd].eps.push(frag.start);
|
|
935
|
-
prevEnd = frag.end;
|
|
936
|
-
}
|
|
937
|
-
return { start: first.start, end: prevEnd };
|
|
938
|
-
}
|
|
939
|
-
case "alt": {
|
|
940
|
-
const s = b.alloc();
|
|
941
|
-
const e = b.alloc();
|
|
942
|
-
for (const opt of node.opts) {
|
|
943
|
-
const frag = compileNode(b, opt);
|
|
944
|
-
b.states[s].eps.push(frag.start);
|
|
945
|
-
b.states[frag.end].eps.push(e);
|
|
946
|
-
}
|
|
947
|
-
return { start: s, end: e };
|
|
948
|
-
}
|
|
949
|
-
case "opt": {
|
|
950
|
-
const s = b.alloc();
|
|
951
|
-
const e = b.alloc();
|
|
952
|
-
const frag = compileNode(b, node.node);
|
|
953
|
-
b.states[s].eps.push(frag.start, e);
|
|
954
|
-
b.states[frag.end].eps.push(e);
|
|
955
|
-
return { start: s, end: e };
|
|
956
|
-
}
|
|
957
|
-
case "star": {
|
|
958
|
-
const s = b.alloc();
|
|
959
|
-
const e = b.alloc();
|
|
960
|
-
const frag = compileNode(b, node.node);
|
|
961
|
-
b.states[s].eps.push(frag.start, e);
|
|
962
|
-
b.states[frag.end].eps.push(frag.start, e);
|
|
963
|
-
return { start: s, end: e };
|
|
964
|
-
}
|
|
965
|
-
case "plus": {
|
|
966
|
-
const e = b.alloc();
|
|
967
|
-
const frag = compileNode(b, node.node);
|
|
968
|
-
b.states[frag.end].eps.push(frag.start, e);
|
|
969
|
-
return { start: frag.start, end: e };
|
|
970
|
-
}
|
|
958
|
+
var BROAD_GLOB_NARROWNESS = 0.34;
|
|
959
|
+
function isBroadFileGlob(pattern) {
|
|
960
|
+
return globNarrowness(pattern) < BROAD_GLOB_NARROWNESS;
|
|
961
|
+
}
|
|
962
|
+
function activeTriggerIds(graph) {
|
|
963
|
+
const ids = /* @__PURE__ */ new Set();
|
|
964
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
965
|
+
if (lesson.status !== "active") continue;
|
|
966
|
+
for (const t of lesson.triggers) ids.add(t);
|
|
971
967
|
}
|
|
968
|
+
return ids;
|
|
972
969
|
}
|
|
973
|
-
function
|
|
974
|
-
const
|
|
975
|
-
const
|
|
976
|
-
|
|
970
|
+
function deadFileGlobIds(graph, knownPaths) {
|
|
971
|
+
const active = activeTriggerIds(graph);
|
|
972
|
+
const paths = [...knownPaths];
|
|
973
|
+
const dead = /* @__PURE__ */ new Set();
|
|
974
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
975
|
+
if (trigger.kind !== "file_glob") continue;
|
|
976
|
+
if (!active.has(triggerId)) continue;
|
|
977
|
+
const isMatch = picomatch(trigger.pattern, { dot: true });
|
|
978
|
+
if (!paths.some((p) => isMatch(p))) dead.add(triggerId);
|
|
979
|
+
}
|
|
980
|
+
return dead;
|
|
981
|
+
}
|
|
982
|
+
function collectDeadFileGlobs(graph, findings, knownPaths) {
|
|
983
|
+
for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
|
|
984
|
+
findings.push({
|
|
985
|
+
level: "warning",
|
|
986
|
+
code: "DEAD_FILE_GLOB",
|
|
987
|
+
message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file in the working tree \u2014 the lesson is unreachable via this trigger (a rename likely moved the path). Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
|
|
988
|
+
triggerId
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function fileGlobMatchCount(pattern, knownPaths) {
|
|
993
|
+
const isMatch = picomatch(pattern, { dot: true });
|
|
994
|
+
let n = 0;
|
|
995
|
+
for (const p of knownPaths) if (isMatch(p)) n += 1;
|
|
996
|
+
return n;
|
|
997
|
+
}
|
|
998
|
+
var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
|
|
999
|
+
function collectRunnerAnchoredPatterns(graph, findings) {
|
|
1000
|
+
const active = activeTriggerIds(graph);
|
|
1001
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1002
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
1003
|
+
if (!active.has(triggerId)) continue;
|
|
1004
|
+
if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
|
|
1005
|
+
findings.push({
|
|
1006
|
+
level: "warning",
|
|
1007
|
+
code: "RUNNER_ANCHORED_PATTERN",
|
|
1008
|
+
message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) is anchored to one runner \u2014 it won't fire for the same task via another runner (e.g. \`npx\` vs \`pnpm\`). Drop the \`^<runner>\` anchor and key on the task (e.g. \`\\bvitest\\b\`).`,
|
|
1009
|
+
triggerId
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function collectBroadFileGlobs(graph, findings) {
|
|
1014
|
+
const active = activeTriggerIds(graph);
|
|
1015
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1016
|
+
if (trigger.kind !== "file_glob") continue;
|
|
1017
|
+
if (!active.has(triggerId)) continue;
|
|
1018
|
+
if (!isBroadFileGlob(trigger.pattern)) continue;
|
|
1019
|
+
findings.push({
|
|
1020
|
+
level: "warning",
|
|
1021
|
+
code: "BROAD_FILE_GLOB",
|
|
1022
|
+
message: `file_glob trigger "${triggerId}" (${trigger.pattern}) matches most of the repository, so it outranks nothing and crowds the recall budget. Narrow it to the directory or file class the rule is really about, or detach it with \`lessons untrigger\`.`,
|
|
1023
|
+
triggerId
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function collectBroadCommandPatterns(graph, findings) {
|
|
1028
|
+
const active = activeTriggerIds(graph);
|
|
1029
|
+
for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
|
|
1030
|
+
if (trigger.kind !== "command_pattern") continue;
|
|
1031
|
+
if (!active.has(triggerId)) continue;
|
|
1032
|
+
if (!isBroadCommandPattern(trigger.pattern)) continue;
|
|
1033
|
+
findings.push({
|
|
1034
|
+
level: "warning",
|
|
1035
|
+
code: "BROAD_COMMAND_PATTERN",
|
|
1036
|
+
message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) matches nearly every command, so the lesson fires on every recall. Key it on the action (e.g. \`\\bgit commit\\b\`), or detach it with \`lessons untrigger\`.`,
|
|
1037
|
+
triggerId
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
977
1040
|
}
|
|
978
1041
|
|
|
979
|
-
// src/lessons/
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1042
|
+
// src/lessons/capture-guardrails.ts
|
|
1043
|
+
var WIDE_GLOB_MATCH_COUNT = 40;
|
|
1044
|
+
var MAX_RECOMMENDED_TRIGGERS = 8;
|
|
1045
|
+
function isBroadGlob(pattern) {
|
|
1046
|
+
const p = pattern.trim();
|
|
1047
|
+
if (p === "*" || p === "**") return true;
|
|
1048
|
+
if (!p.includes("**")) return false;
|
|
1049
|
+
const basename5 = p.slice(p.lastIndexOf("/") + 1);
|
|
1050
|
+
return basename5.startsWith("*");
|
|
984
1051
|
}
|
|
985
|
-
function
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1052
|
+
function inspectCapturedLesson(graph, lessonId, knownPaths) {
|
|
1053
|
+
const lesson = graph.lessons[lessonId];
|
|
1054
|
+
if (lesson === void 0) return [];
|
|
1055
|
+
const warnings = [];
|
|
1056
|
+
if (lesson.triggers.length > MAX_RECOMMENDED_TRIGGERS) {
|
|
1057
|
+
warnings.push({
|
|
1058
|
+
code: "OVERSIZED_LESSON_TRIGGERS",
|
|
1059
|
+
message: `Lesson "${lessonId}" has ${lesson.triggers.length} triggers (recommended \u2264 ${MAX_RECOMMENDED_TRIGGERS}); broad trigger sets fire on too many edits and dilute recall \u2014 prefer a few specific triggers.`
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
const triggers = lesson.triggers.map((id) => graph.triggers[id]).filter((t) => t !== void 0);
|
|
1063
|
+
const broad = triggers.filter((t) => t.kind === "file_glob" && isBroadGlob(t.pattern)).map((t) => t.pattern);
|
|
1064
|
+
if (broad.length > 0) {
|
|
1065
|
+
warnings.push({
|
|
1066
|
+
code: "BROAD_GLOB_TRIGGER",
|
|
1067
|
+
message: `Lesson "${lessonId}" has broad file glob(s) (${broad.join(", ")}) that match large swaths of the tree; prefer a path specific to the lesson.`
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (triggers.length > 0 && triggers.every((t) => t.kind === "keyword")) {
|
|
1071
|
+
warnings.push({
|
|
1072
|
+
code: "KEYWORD_ONLY_LESSON",
|
|
1073
|
+
message: `Lesson "${lessonId}" has only keyword triggers; mandatory --file/--cmd recall surfaces these only when the keyword appears as a path/command token, so it fires less reliably \u2014 add a file_glob or command_pattern trigger for precise recall.`
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
const lowSignal = triggers.filter((t) => t.kind === "keyword" && isLowSignalKeyword(t.pattern)).map((t) => t.pattern);
|
|
1077
|
+
if (lowSignal.length > 0) {
|
|
1078
|
+
warnings.push({
|
|
1079
|
+
code: "LOW_SIGNAL_KEYWORD",
|
|
1080
|
+
message: `Lesson "${lessonId}" has long keyword trigger(s) (${lowSignal.join(", ")}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so a pattern past ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens rarely fires \u2014 use a short distinctive phrase.`
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
|
|
1084
|
+
if (stopworded.length > 0) {
|
|
1085
|
+
warnings.push({
|
|
1086
|
+
code: "STOPWORD_KEYWORD",
|
|
1087
|
+
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").`
|
|
1088
|
+
});
|
|
995
1089
|
}
|
|
1090
|
+
if (knownPaths !== void 0) {
|
|
1091
|
+
const dead = deadFileGlobIds(graph, knownPaths);
|
|
1092
|
+
const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
|
|
1093
|
+
if (deadHere.length > 0) {
|
|
1094
|
+
warnings.push({
|
|
1095
|
+
code: "DEAD_GLOB",
|
|
1096
|
+
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.`
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
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);
|
|
1100
|
+
if (wide.length > 0) {
|
|
1101
|
+
warnings.push({
|
|
1102
|
+
code: "WIDE_GLOB_MATCH",
|
|
1103
|
+
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.`
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return warnings;
|
|
996
1108
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1109
|
+
|
|
1110
|
+
// src/lessons/capture-near-duplicate.ts
|
|
1111
|
+
var NEAR_DUPLICATE_THRESHOLD = 0.6;
|
|
1112
|
+
function nearDuplicateWarning(graph, lessonId) {
|
|
1113
|
+
const subject = graph.lessons[lessonId];
|
|
1114
|
+
if (subject === void 0) return null;
|
|
1115
|
+
const subjectTokens = new Set(tokenize(subject.rule));
|
|
1116
|
+
if (subjectTokens.size === 0) return null;
|
|
1117
|
+
let best = null;
|
|
1118
|
+
for (const [id, other] of Object.entries(graph.lessons)) {
|
|
1119
|
+
if (id === lessonId || other.status !== "active") continue;
|
|
1120
|
+
const otherTokens = new Set(tokenize(other.rule));
|
|
1121
|
+
if (otherTokens.size === 0) continue;
|
|
1122
|
+
const score = jaccard(subjectTokens, otherTokens);
|
|
1123
|
+
if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
|
|
1124
|
+
best = { id, score };
|
|
1011
1125
|
}
|
|
1012
|
-
}
|
|
1126
|
+
}
|
|
1127
|
+
if (best === null) return null;
|
|
1013
1128
|
return {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
// the shared budget instead: when it runs out we report a safe non-match.
|
|
1017
|
-
test(input, budget) {
|
|
1018
|
-
const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
|
|
1019
|
-
if (b.remaining <= 0) return false;
|
|
1020
|
-
let current = /* @__PURE__ */ new Set();
|
|
1021
|
-
for (let pos = 0; pos <= input.length; pos += 1) {
|
|
1022
|
-
closure(current, start, input, pos, b);
|
|
1023
|
-
if (current.has(accept)) return true;
|
|
1024
|
-
if (b.remaining <= 0) return false;
|
|
1025
|
-
if (pos === input.length) break;
|
|
1026
|
-
const ch = input[pos];
|
|
1027
|
-
const next = /* @__PURE__ */ new Set();
|
|
1028
|
-
for (const s of current) {
|
|
1029
|
-
b.remaining -= 1;
|
|
1030
|
-
for (const t of states[s].chars) {
|
|
1031
|
-
if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
current = next;
|
|
1035
|
-
}
|
|
1036
|
-
return current.has(accept);
|
|
1037
|
-
}
|
|
1129
|
+
code: "NEAR_DUPLICATE_LESSON",
|
|
1130
|
+
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).`
|
|
1038
1131
|
};
|
|
1039
1132
|
}
|
|
1133
|
+
function jaccard(a, b) {
|
|
1134
|
+
let intersection = 0;
|
|
1135
|
+
for (const t of a) if (b.has(t)) intersection += 1;
|
|
1136
|
+
return intersection / (a.size + b.size - intersection);
|
|
1137
|
+
}
|
|
1040
1138
|
|
|
1041
|
-
// src/
|
|
1042
|
-
var
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1139
|
+
// src/core/errors.ts
|
|
1140
|
+
var AgentsMeshError = class extends Error {
|
|
1141
|
+
code;
|
|
1142
|
+
constructor(code, message, options) {
|
|
1143
|
+
super(message, options);
|
|
1144
|
+
this.name = "AgentsMeshError";
|
|
1145
|
+
this.code = code;
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
var LockAcquisitionError = class extends AgentsMeshError {
|
|
1149
|
+
lockPath;
|
|
1150
|
+
holder;
|
|
1151
|
+
/** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
|
|
1152
|
+
label;
|
|
1153
|
+
constructor(lockPath, holder, options) {
|
|
1154
|
+
const label = options?.label ?? "lock";
|
|
1155
|
+
super(
|
|
1156
|
+
"AM_LOCK_ACQUISITION_FAILED",
|
|
1157
|
+
`Could not acquire ${label} at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
|
|
1158
|
+
options
|
|
1159
|
+
);
|
|
1160
|
+
this.name = "LockAcquisitionError";
|
|
1161
|
+
this.lockPath = lockPath;
|
|
1162
|
+
this.holder = holder;
|
|
1163
|
+
this.label = label;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
var FileSystemError = class extends AgentsMeshError {
|
|
1167
|
+
path;
|
|
1168
|
+
errnoCode;
|
|
1169
|
+
constructor(path, message, options) {
|
|
1170
|
+
super("AM_FILESYSTEM", message, options);
|
|
1171
|
+
this.name = "FileSystemError";
|
|
1172
|
+
this.path = path;
|
|
1173
|
+
this.errnoCode = options?.errnoCode;
|
|
1046
1174
|
}
|
|
1047
1175
|
};
|
|
1048
1176
|
|
|
1049
|
-
// src/
|
|
1050
|
-
var
|
|
1051
|
-
var
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1177
|
+
// src/utils/filesystem/process-lock.ts
|
|
1178
|
+
var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
1179
|
+
var DEFAULT_RETRIES = 30;
|
|
1180
|
+
var DEFAULT_RETRY_DELAY_MS = 200;
|
|
1181
|
+
var YOUNG_LOCK_GRACE_MS = 2e3;
|
|
1182
|
+
async function acquireProcessLock(lockPath, opts = {}) {
|
|
1183
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
1184
|
+
const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1185
|
+
const stale = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
1186
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1187
|
+
let attempt = 0;
|
|
1188
|
+
while (true) {
|
|
1189
|
+
const acquired = await tryAcquire(lockPath);
|
|
1190
|
+
if (acquired) return acquired;
|
|
1191
|
+
const existing = await inspectLock(lockPath);
|
|
1192
|
+
if (existing !== "young" && isStale(existing, stale)) {
|
|
1193
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
1194
|
+
});
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (attempt >= retries) {
|
|
1198
|
+
const holder = existing === "young" ? null : existing;
|
|
1199
|
+
throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
|
|
1200
|
+
}
|
|
1201
|
+
attempt++;
|
|
1202
|
+
await sleep(delay);
|
|
1059
1203
|
}
|
|
1060
|
-
if (items.length === 0) return { k: "empty" };
|
|
1061
|
-
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1062
1204
|
}
|
|
1063
|
-
function
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1205
|
+
async function tryAcquire(lockPath) {
|
|
1206
|
+
try {
|
|
1207
|
+
await mkdir(lockPath, { recursive: false });
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
if (err.code === "EEXIST") return null;
|
|
1210
|
+
throw err;
|
|
1211
|
+
}
|
|
1212
|
+
const metadataPath = join(lockPath, "holder.json");
|
|
1213
|
+
const metadata = {
|
|
1214
|
+
pid: process.pid,
|
|
1215
|
+
started: Date.now(),
|
|
1216
|
+
hostname: getHostname()
|
|
1217
|
+
};
|
|
1218
|
+
await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
1219
|
+
let released = false;
|
|
1220
|
+
const cleanup = () => {
|
|
1221
|
+
if (released) return;
|
|
1222
|
+
released = true;
|
|
1223
|
+
try {
|
|
1224
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const signalHandler = (signal) => {
|
|
1229
|
+
cleanup();
|
|
1230
|
+
process.kill(process.pid, signal);
|
|
1231
|
+
};
|
|
1232
|
+
process.once("SIGINT", signalHandler);
|
|
1233
|
+
process.once("SIGTERM", signalHandler);
|
|
1234
|
+
process.once("exit", cleanup);
|
|
1235
|
+
return async () => {
|
|
1236
|
+
if (released) return;
|
|
1237
|
+
released = true;
|
|
1238
|
+
process.off("SIGINT", signalHandler);
|
|
1239
|
+
process.off("SIGTERM", signalHandler);
|
|
1240
|
+
process.off("exit", cleanup);
|
|
1241
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
1242
|
+
});
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
async function inspectLock(lockPath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
|
|
1248
|
+
const parsed = JSON.parse(raw);
|
|
1249
|
+
if (!isLockMetadata(parsed)) return null;
|
|
1250
|
+
return parsed;
|
|
1251
|
+
} catch {
|
|
1252
|
+
try {
|
|
1253
|
+
const info = await stat(lockPath);
|
|
1254
|
+
const ageMs = Date.now() - info.mtimeMs;
|
|
1255
|
+
if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
return null;
|
|
1079
1259
|
}
|
|
1080
1260
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (
|
|
1085
|
-
|
|
1086
|
-
return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
|
|
1087
|
-
}
|
|
1088
|
-
const hex = src.slice(i, i + 4);
|
|
1089
|
-
return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
|
|
1261
|
+
function isStale(meta, staleMs) {
|
|
1262
|
+
if (!meta) return true;
|
|
1263
|
+
const sameHost = !meta.hostname || meta.hostname === getHostname();
|
|
1264
|
+
if (sameHost && !isProcessAlive(meta.pid)) return true;
|
|
1265
|
+
return Date.now() - meta.started > staleMs;
|
|
1090
1266
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1267
|
+
function isProcessAlive(pid) {
|
|
1268
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1269
|
+
try {
|
|
1270
|
+
process.kill(pid, 0);
|
|
1271
|
+
return true;
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
return err.code === "EPERM";
|
|
1095
1274
|
}
|
|
1096
|
-
return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
|
|
1097
1275
|
}
|
|
1098
|
-
function
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
return { ch: escapeLiteral(e), len: 0 };
|
|
1276
|
+
function describeHolder(meta) {
|
|
1277
|
+
if (!meta) return "unknown (unreadable lock metadata)";
|
|
1278
|
+
const host = meta.hostname ? `${meta.hostname}:` : "";
|
|
1279
|
+
return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
|
|
1103
1280
|
}
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
case "v":
|
|
1115
|
-
return "\v";
|
|
1116
|
-
case "0":
|
|
1117
|
-
return "\0";
|
|
1118
|
-
default:
|
|
1119
|
-
return c;
|
|
1120
|
-
}
|
|
1281
|
+
function isLockMetadata(value) {
|
|
1282
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1283
|
+
const v = value;
|
|
1284
|
+
return typeof v.pid === "number" && typeof v.started === "number";
|
|
1285
|
+
}
|
|
1286
|
+
function getHostname() {
|
|
1287
|
+
return hostname();
|
|
1288
|
+
}
|
|
1289
|
+
function sleep(ms) {
|
|
1290
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1121
1291
|
}
|
|
1122
1292
|
|
|
1123
|
-
// src/lessons/
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1293
|
+
// src/lessons/lessons-lock.ts
|
|
1294
|
+
var LESSONS_LOCK_FILENAME = ".lessons.lock";
|
|
1295
|
+
function lessonsLockPath(projectRoot) {
|
|
1296
|
+
return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
|
|
1297
|
+
}
|
|
1298
|
+
async function acquireLessonsLock(projectRoot, opts = {}) {
|
|
1299
|
+
const lockPath = lessonsLockPath(projectRoot);
|
|
1300
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
1301
|
+
return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/lessons/validate-checks.ts
|
|
1305
|
+
function collectDanglingRefs(graph, findings) {
|
|
1306
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1307
|
+
for (const topicId of lesson.topics) {
|
|
1308
|
+
if (graph.topics[topicId] === void 0) {
|
|
1309
|
+
findings.push({
|
|
1310
|
+
level: "error",
|
|
1311
|
+
code: "DANGLING_TOPIC",
|
|
1312
|
+
message: `Lesson "${lessonId}" references unknown topic "${topicId}".`,
|
|
1313
|
+
lessonId,
|
|
1314
|
+
topicId
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1133
1317
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1318
|
+
for (const triggerId of lesson.triggers) {
|
|
1319
|
+
if (graph.triggers[triggerId] === void 0) {
|
|
1320
|
+
findings.push({
|
|
1321
|
+
level: "error",
|
|
1322
|
+
code: "DANGLING_TRIGGER",
|
|
1323
|
+
message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
|
|
1324
|
+
lessonId,
|
|
1325
|
+
triggerId
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
|
|
1330
|
+
findings.push({
|
|
1331
|
+
level: "error",
|
|
1332
|
+
code: "DANGLING_SUPERSEDER",
|
|
1333
|
+
message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
|
|
1334
|
+
lessonId
|
|
1335
|
+
});
|
|
1140
1336
|
}
|
|
1141
|
-
if (items.length === 0) return { k: "empty" };
|
|
1142
|
-
return items.length === 1 ? items[0] : { k: "concat", items };
|
|
1143
1337
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1338
|
+
}
|
|
1339
|
+
function collectDuplicateRefs(graph, findings) {
|
|
1340
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1341
|
+
for (const topicId of firstDuplicates(lesson.topics)) {
|
|
1342
|
+
findings.push({
|
|
1343
|
+
level: "error",
|
|
1344
|
+
code: "DUPLICATE_TOPIC_REF",
|
|
1345
|
+
message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
|
|
1346
|
+
lessonId,
|
|
1347
|
+
topicId
|
|
1348
|
+
});
|
|
1151
1349
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1350
|
+
for (const triggerId of firstDuplicates(lesson.triggers)) {
|
|
1351
|
+
findings.push({
|
|
1352
|
+
level: "error",
|
|
1353
|
+
code: "DUPLICATE_TRIGGER_REF",
|
|
1354
|
+
message: `Lesson "${lessonId}" references trigger "${triggerId}" more than once.`,
|
|
1355
|
+
lessonId,
|
|
1356
|
+
triggerId
|
|
1357
|
+
});
|
|
1155
1358
|
}
|
|
1156
|
-
return atom;
|
|
1157
1359
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
|
|
1166
|
-
throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
|
|
1167
|
-
}
|
|
1168
|
-
return { min, max };
|
|
1360
|
+
}
|
|
1361
|
+
function firstDuplicates(ids) {
|
|
1362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1363
|
+
const dup = /* @__PURE__ */ new Set();
|
|
1364
|
+
for (const id of ids) {
|
|
1365
|
+
if (seen.has(id)) dup.add(id);
|
|
1366
|
+
else seen.add(id);
|
|
1169
1367
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
return { k: "assert", kind: "start" };
|
|
1182
|
-
}
|
|
1183
|
-
if (c === "$") {
|
|
1184
|
-
i += 1;
|
|
1185
|
-
return { k: "assert", kind: "end" };
|
|
1186
|
-
}
|
|
1187
|
-
if (c === void 0 || c === "*" || c === "+" || c === "?" || c === ")") {
|
|
1188
|
-
throw new UnsupportedRegexError(`Unexpected '${c ?? "<end>"}' in pattern`);
|
|
1368
|
+
return [...dup];
|
|
1369
|
+
}
|
|
1370
|
+
function collectStatusInvariants(graph, findings) {
|
|
1371
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1372
|
+
if (lesson.status === "superseded" && lesson.supersededBy === void 0) {
|
|
1373
|
+
findings.push({
|
|
1374
|
+
level: "error",
|
|
1375
|
+
code: "SUPERSEDED_WITHOUT_TARGET",
|
|
1376
|
+
message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
|
|
1377
|
+
lessonId
|
|
1378
|
+
});
|
|
1189
1379
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (c2 === "=" || c2 === "!" || c2 === "<") {
|
|
1198
|
-
if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
|
|
1199
|
-
throw new UnsupportedRegexError("Lookaround assertions are not supported");
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
if (c2 === ":") i += 2;
|
|
1203
|
-
else if (c2 === "<") {
|
|
1204
|
-
i += 2;
|
|
1205
|
-
while (i < src.length && src[i] !== ">") i += 1;
|
|
1206
|
-
i += 1;
|
|
1207
|
-
}
|
|
1380
|
+
if (lesson.status === "active" && lesson.supersededBy !== void 0) {
|
|
1381
|
+
findings.push({
|
|
1382
|
+
level: "error",
|
|
1383
|
+
code: "ACTIVE_WITH_SUPERSEDER",
|
|
1384
|
+
message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
|
|
1385
|
+
lessonId
|
|
1386
|
+
});
|
|
1208
1387
|
}
|
|
1209
|
-
const inner = parseAlt();
|
|
1210
|
-
if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
|
|
1211
|
-
i += 1;
|
|
1212
|
-
return inner;
|
|
1213
1388
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const { ch, len } = c === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c);
|
|
1227
|
-
i += len;
|
|
1228
|
-
return { k: "char", ch };
|
|
1389
|
+
}
|
|
1390
|
+
function collectLifecycleInvariants(graph, findings) {
|
|
1391
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1392
|
+
if (lesson.supersededBy === void 0) continue;
|
|
1393
|
+
if (lesson.supersededBy === lessonId) {
|
|
1394
|
+
findings.push({
|
|
1395
|
+
level: "error",
|
|
1396
|
+
code: "SELF_SUPERSEDED",
|
|
1397
|
+
message: `Lesson "${lessonId}" is superseded by itself.`,
|
|
1398
|
+
lessonId
|
|
1399
|
+
});
|
|
1400
|
+
continue;
|
|
1229
1401
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
tests.push(parseClassMember());
|
|
1402
|
+
const target = graph.lessons[lesson.supersededBy];
|
|
1403
|
+
if (target !== void 0 && target.status !== "active") {
|
|
1404
|
+
findings.push({
|
|
1405
|
+
level: "error",
|
|
1406
|
+
code: "INACTIVE_SUPERSEDER",
|
|
1407
|
+
message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target.status} \u2014 the chain dead-ends with no live replacement.`,
|
|
1408
|
+
lessonId
|
|
1409
|
+
});
|
|
1239
1410
|
}
|
|
1240
|
-
if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
|
|
1241
|
-
i += 1;
|
|
1242
|
-
const base = (c) => tests.some((t) => t(c));
|
|
1243
|
-
return { k: "class", test: negate ? (c) => !base(c) : base };
|
|
1244
1411
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const e2 = eat();
|
|
1264
|
-
const r = classEscapeChar(src, i, e2);
|
|
1265
|
-
i += r.len;
|
|
1266
|
-
hi = r.ch;
|
|
1267
|
-
} else {
|
|
1268
|
-
hi = eat();
|
|
1412
|
+
collectSupersedeCycles(graph, findings);
|
|
1413
|
+
}
|
|
1414
|
+
function collectSupersedeCycles(graph, findings) {
|
|
1415
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1416
|
+
for (const startId of Object.keys(graph.lessons)) {
|
|
1417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1418
|
+
let cur = startId;
|
|
1419
|
+
while (cur !== void 0) {
|
|
1420
|
+
if (seen.has(cur)) {
|
|
1421
|
+
if (cur !== startId || reported.has(cur)) break;
|
|
1422
|
+
reported.add(cur);
|
|
1423
|
+
findings.push({
|
|
1424
|
+
level: "error",
|
|
1425
|
+
code: "SUPERSEDE_CYCLE",
|
|
1426
|
+
message: `Lesson "${startId}" is part of a supersededBy cycle.`,
|
|
1427
|
+
lessonId: startId
|
|
1428
|
+
});
|
|
1429
|
+
break;
|
|
1269
1430
|
}
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
return p >= a && p <= b;
|
|
1275
|
-
};
|
|
1431
|
+
seen.add(cur);
|
|
1432
|
+
const next = graph.lessons[cur]?.supersededBy;
|
|
1433
|
+
if (next === cur) break;
|
|
1434
|
+
cur = next;
|
|
1276
1435
|
}
|
|
1277
|
-
return (c) => c === lo;
|
|
1278
1436
|
}
|
|
1279
|
-
const ast = parseAlt();
|
|
1280
|
-
if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
|
|
1281
|
-
return ast;
|
|
1282
1437
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
matcher = null;
|
|
1438
|
+
function collectReachability(graph, findings) {
|
|
1439
|
+
for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
|
|
1440
|
+
if (lesson.status === "active" && lesson.triggers.length === 0) {
|
|
1441
|
+
findings.push({
|
|
1442
|
+
level: "warning",
|
|
1443
|
+
code: "UNREACHABLE_LESSON",
|
|
1444
|
+
message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
|
|
1445
|
+
lessonId
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1294
1448
|
}
|
|
1295
|
-
cache.set(pattern, matcher);
|
|
1296
|
-
return matcher;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// src/lessons/regex-safety.ts
|
|
1300
|
-
var MAX_PATTERN_LENGTH = 1e3;
|
|
1301
|
-
function isSafeRegexPattern(pattern) {
|
|
1302
|
-
if (pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
1303
|
-
return compileLinearMatcher(pattern) !== null;
|
|
1304
1449
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1307
|
-
|
|
1450
|
+
function collectOrphans(graph, findings) {
|
|
1451
|
+
const referencedTopics = /* @__PURE__ */ new Set();
|
|
1452
|
+
const referencedTriggers = /* @__PURE__ */ new Set();
|
|
1453
|
+
for (const lesson of Object.values(graph.lessons)) {
|
|
1454
|
+
for (const t of lesson.topics) referencedTopics.add(t);
|
|
1455
|
+
for (const t of lesson.triggers) referencedTriggers.add(t);
|
|
1456
|
+
}
|
|
1457
|
+
for (const topicId of Object.keys(graph.topics)) {
|
|
1458
|
+
if (!referencedTopics.has(topicId)) {
|
|
1459
|
+
findings.push({
|
|
1460
|
+
level: "warning",
|
|
1461
|
+
code: "ORPHAN_TOPIC",
|
|
1462
|
+
message: `Topic "${topicId}" is not referenced by any lesson.`,
|
|
1463
|
+
topicId
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
for (const triggerId of Object.keys(graph.triggers)) {
|
|
1468
|
+
if (!referencedTriggers.has(triggerId)) {
|
|
1469
|
+
findings.push({
|
|
1470
|
+
level: "warning",
|
|
1471
|
+
code: "ORPHAN_TRIGGER",
|
|
1472
|
+
message: `Trigger "${triggerId}" is not referenced by any lesson.`,
|
|
1473
|
+
triggerId
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1308
1477
|
}
|
|
1309
1478
|
|
|
1310
1479
|
// src/lessons/validate-quality.ts
|
|
@@ -1410,6 +1579,11 @@ function collectFanout(graph, findings) {
|
|
|
1410
1579
|
});
|
|
1411
1580
|
}
|
|
1412
1581
|
}
|
|
1582
|
+
function normalizeRule2(rule) {
|
|
1583
|
+
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/lessons/validate-keywords.ts
|
|
1413
1587
|
function collectLowSignalKeywords(graph, findings) {
|
|
1414
1588
|
const activeTriggerIds2 = /* @__PURE__ */ new Set();
|
|
1415
1589
|
for (const lesson of Object.values(graph.lessons)) {
|
|
@@ -1423,7 +1597,7 @@ function collectLowSignalKeywords(graph, findings) {
|
|
|
1423
1597
|
findings.push({
|
|
1424
1598
|
level: "warning",
|
|
1425
1599
|
code: "LOW_SIGNAL_KEYWORD",
|
|
1426
|
-
message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a
|
|
1600
|
+
message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so it rarely fires \u2014 use a short distinctive phrase.`,
|
|
1427
1601
|
triggerId
|
|
1428
1602
|
});
|
|
1429
1603
|
}
|
|
@@ -1448,9 +1622,6 @@ function collectStopwordKeywords(graph, findings) {
|
|
|
1448
1622
|
});
|
|
1449
1623
|
}
|
|
1450
1624
|
}
|
|
1451
|
-
function normalizeRule2(rule) {
|
|
1452
|
-
return rule.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1453
|
-
}
|
|
1454
1625
|
|
|
1455
1626
|
// src/lessons/validate.ts
|
|
1456
1627
|
function validateLessonsGraph(graph, options = {}) {
|
|
@@ -1478,6 +1649,8 @@ function validateLessonsGraph(graph, options = {}) {
|
|
|
1478
1649
|
collectLowSignalKeywords(graph, findings);
|
|
1479
1650
|
collectStopwordKeywords(graph, findings);
|
|
1480
1651
|
collectRunnerAnchoredPatterns(graph, findings);
|
|
1652
|
+
collectBroadCommandPatterns(graph, findings);
|
|
1653
|
+
collectBroadFileGlobs(graph, findings);
|
|
1481
1654
|
if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
|
|
1482
1655
|
const ok = findings.every((f) => f.level !== "error");
|
|
1483
1656
|
return { ok, findings };
|
|
@@ -1521,52 +1694,7 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
|
|
|
1521
1694
|
return mutateLessonsGraphLocked(projectRoot, mutator, options);
|
|
1522
1695
|
}
|
|
1523
1696
|
|
|
1524
|
-
// src/lessons/trigger-effectiveness.ts
|
|
1525
|
-
function ineffectiveTriggers(graph, triggerIds) {
|
|
1526
|
-
const out = [];
|
|
1527
|
-
for (const id of triggerIds) {
|
|
1528
|
-
const trigger = graph.triggers[id];
|
|
1529
|
-
if (trigger === void 0) continue;
|
|
1530
|
-
const reason = ineffectiveReason(trigger.kind, trigger.pattern);
|
|
1531
|
-
if (reason !== null) out.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
|
|
1532
|
-
}
|
|
1533
|
-
return out;
|
|
1534
|
-
}
|
|
1535
|
-
function ineffectiveReason(kind, pattern) {
|
|
1536
|
-
if (kind === "keyword") {
|
|
1537
|
-
if (tokenize(pattern).length === 0) {
|
|
1538
|
-
return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
|
|
1539
|
-
}
|
|
1540
|
-
if (keywordNeedleLosesTokens(pattern)) {
|
|
1541
|
-
return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
|
|
1542
|
-
}
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
if (kind === "command_pattern") {
|
|
1546
|
-
let valid = true;
|
|
1547
|
-
try {
|
|
1548
|
-
new RegExp(pattern);
|
|
1549
|
-
} catch {
|
|
1550
|
-
valid = false;
|
|
1551
|
-
}
|
|
1552
|
-
if (!valid) {
|
|
1553
|
-
return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
|
|
1554
|
-
}
|
|
1555
|
-
if (!isSafeRegexPattern(pattern)) {
|
|
1556
|
-
return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
|
|
1557
|
-
}
|
|
1558
|
-
return null;
|
|
1559
|
-
}
|
|
1560
|
-
return null;
|
|
1561
|
-
}
|
|
1562
|
-
function blockingDeadTriggers(graph, triggerIds) {
|
|
1563
|
-
return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
1697
|
// src/lessons/add.ts
|
|
1567
|
-
function countInputTriggers(triggers) {
|
|
1568
|
-
return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
|
|
1569
|
-
}
|
|
1570
1698
|
async function addLesson(projectRoot, input, options = {}) {
|
|
1571
1699
|
return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
|
|
1572
1700
|
retries: options.retries
|
|
@@ -1574,10 +1702,7 @@ async function addLesson(projectRoot, input, options = {}) {
|
|
|
1574
1702
|
}
|
|
1575
1703
|
function addLessonInto(graph, input, options) {
|
|
1576
1704
|
const ruleKey = normalizeRule(input.rule);
|
|
1577
|
-
const trimmedRule = input.rule
|
|
1578
|
-
if (trimmedRule.length > MAX_RULE_LENGTH) {
|
|
1579
|
-
throw new RuleTooLongError(trimmedRule.length, MAX_RULE_LENGTH);
|
|
1580
|
-
}
|
|
1705
|
+
const trimmedRule = assertRuleShape(input.rule);
|
|
1581
1706
|
const existingId = findExistingLessonByRule(graph, ruleKey);
|
|
1582
1707
|
const isNewTopic = graph.topics[input.topic] === void 0;
|
|
1583
1708
|
if (isNewTopic) {
|
|
@@ -1587,29 +1712,23 @@ function addLessonInto(graph, input, options) {
|
|
|
1587
1712
|
}
|
|
1588
1713
|
graph.topics[input.topic] = { summary: options.topicSummary };
|
|
1589
1714
|
}
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
|
|
1593
|
-
if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
|
|
1594
|
-
throw new NoTriggerError();
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1715
|
+
const existing = existingId !== null ? graph.lessons[existingId] : void 0;
|
|
1716
|
+
assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
|
|
1597
1717
|
const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
|
|
1598
|
-
if (!
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1718
|
+
if (!skipsTriggerGates(input, options)) {
|
|
1719
|
+
assertRecallable(
|
|
1720
|
+
graph,
|
|
1721
|
+
existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
|
|
1722
|
+
);
|
|
1604
1723
|
}
|
|
1605
1724
|
if (existingId !== null) {
|
|
1606
|
-
const
|
|
1725
|
+
const existing2 = graph.lessons[existingId];
|
|
1607
1726
|
graph.lessons[existingId] = {
|
|
1608
|
-
...
|
|
1609
|
-
topics: union(
|
|
1610
|
-
triggers: union(
|
|
1611
|
-
evidence: union(
|
|
1612
|
-
...
|
|
1727
|
+
...existing2,
|
|
1728
|
+
topics: union(existing2.topics, [input.topic]),
|
|
1729
|
+
triggers: union(existing2.triggers, triggerIds),
|
|
1730
|
+
evidence: union(existing2.evidence, input.evidence ?? []),
|
|
1731
|
+
...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
|
|
1613
1732
|
// Re-capturing a rule with --scope always promotes it to always-on.
|
|
1614
1733
|
...input.scope === "always" ? { scope: "always" } : {}
|
|
1615
1734
|
};
|
|
@@ -1814,14 +1933,16 @@ function deriveHaystackTokens(query) {
|
|
|
1814
1933
|
if (query.command !== void 0) parts.push(query.command);
|
|
1815
1934
|
if (parts.length === 0) return [];
|
|
1816
1935
|
const out = [];
|
|
1817
|
-
for (const raw of parts.join(" ")
|
|
1818
|
-
if (raw.length === 0) continue;
|
|
1936
|
+
for (const raw of splitTokens(parts.join(" "))) {
|
|
1819
1937
|
out.push(raw.toLowerCase());
|
|
1820
1938
|
const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
|
|
1821
1939
|
if (sub.length > 1) out.push(...sub);
|
|
1822
1940
|
}
|
|
1823
1941
|
return out;
|
|
1824
1942
|
}
|
|
1943
|
+
function splitTokens(text) {
|
|
1944
|
+
return text.split(/[^A-Za-z0-9]+/).filter((t) => t.length > 0);
|
|
1945
|
+
}
|
|
1825
1946
|
function containsRun(needle, hay) {
|
|
1826
1947
|
if (needle.length === 0) return false;
|
|
1827
1948
|
for (let i = 0; i + needle.length <= hay.length; i += 1) {
|
|
@@ -1837,10 +1958,11 @@ function containsRun(needle, hay) {
|
|
|
1837
1958
|
return false;
|
|
1838
1959
|
}
|
|
1839
1960
|
function keywordMatches(pattern, query) {
|
|
1840
|
-
|
|
1961
|
+
const needle = tokenize(pattern);
|
|
1962
|
+
if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
|
|
1841
1963
|
return true;
|
|
1842
1964
|
}
|
|
1843
|
-
return containsRun(
|
|
1965
|
+
return containsRun(needle, deriveHaystackTokens(query));
|
|
1844
1966
|
}
|
|
1845
1967
|
|
|
1846
1968
|
// src/lessons/query.ts
|
|
@@ -2043,6 +2165,15 @@ function buildFanout(graph) {
|
|
|
2043
2165
|
}
|
|
2044
2166
|
return fanout;
|
|
2045
2167
|
}
|
|
2168
|
+
var KEYWORD_NARROWNESS = 0.4;
|
|
2169
|
+
function buildNarrowness(graph) {
|
|
2170
|
+
const narrowness = /* @__PURE__ */ new Map();
|
|
2171
|
+
for (const [id, trigger] of Object.entries(graph.triggers)) {
|
|
2172
|
+
if (trigger.kind === "file_glob") narrowness.set(id, globNarrowness(trigger.pattern));
|
|
2173
|
+
else narrowness.set(id, trigger.kind === "keyword" ? KEYWORD_NARROWNESS : 1);
|
|
2174
|
+
}
|
|
2175
|
+
return narrowness;
|
|
2176
|
+
}
|
|
2046
2177
|
function buildTopicCoherence(matches) {
|
|
2047
2178
|
const topicCount = /* @__PURE__ */ new Map();
|
|
2048
2179
|
for (const { lesson } of matches) {
|
|
@@ -2061,7 +2192,7 @@ function buildTopicCoherence(matches) {
|
|
|
2061
2192
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
2062
2193
|
var DEFAULT_RECALL_MAX_TOKENS = 400;
|
|
2063
2194
|
var RRF_K = 60;
|
|
2064
|
-
var SPECIFICITY_WEIGHT =
|
|
2195
|
+
var SPECIFICITY_WEIGHT = 5;
|
|
2065
2196
|
var TOPIC_COHERENCE_WEIGHT = 2;
|
|
2066
2197
|
var BM25_WEIGHT = 1;
|
|
2067
2198
|
var EFFECTIVENESS_WEIGHT = 1;
|
|
@@ -2088,12 +2219,15 @@ function rankLessons(graph, query, matches, options = {}) {
|
|
|
2088
2219
|
const terms = queryTerms(query);
|
|
2089
2220
|
const corpus = buildCorpus(graph);
|
|
2090
2221
|
const fanout = buildFanout(graph);
|
|
2222
|
+
const narrowness = buildNarrowness(graph);
|
|
2091
2223
|
const coherence = buildTopicCoherence(matches);
|
|
2092
2224
|
const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
|
|
2093
2225
|
const scored = matches.map(({ id, lesson }) => {
|
|
2094
2226
|
const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
|
|
2095
2227
|
let specificity = 0;
|
|
2096
|
-
for (const t of hitTriggers)
|
|
2228
|
+
for (const t of hitTriggers) {
|
|
2229
|
+
specificity = Math.max(specificity, (narrowness.get(t) ?? 1) / fanout.get(t));
|
|
2230
|
+
}
|
|
2097
2231
|
return {
|
|
2098
2232
|
id,
|
|
2099
2233
|
lesson,
|
|
@@ -2710,9 +2844,9 @@ var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
|
|
|
2710
2844
|
var LINE_REF_PATTERNS = [
|
|
2711
2845
|
new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
|
|
2712
2846
|
// " See L128." / " See L140, L149"
|
|
2713
|
-
new RegExp(String.raw`\s*\((?:${LINE_REFS})\)
|
|
2847
|
+
new RegExp(String.raw`\s*\((?:${LINE_REFS})\)`, "g"),
|
|
2714
2848
|
// " (L174)" / " (L92, L163)"
|
|
2715
|
-
new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]
|
|
2849
|
+
new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]`, "g")
|
|
2716
2850
|
// " [L161, L208]"
|
|
2717
2851
|
];
|
|
2718
2852
|
var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
|
|
@@ -2950,6 +3084,32 @@ function isCoveredByExisting(candidate, existing) {
|
|
|
2950
3084
|
}
|
|
2951
3085
|
return false;
|
|
2952
3086
|
}
|
|
3087
|
+
var OPENER = /^---[ \t]*\r?\n/;
|
|
3088
|
+
var CLOSER = /^---[ \t]*\r?$/m;
|
|
3089
|
+
function splitFrontmatter(content) {
|
|
3090
|
+
const opener = OPENER.exec(content);
|
|
3091
|
+
if (opener === null) return null;
|
|
3092
|
+
const yamlStart = opener[0].length;
|
|
3093
|
+
const closer = CLOSER.exec(content.slice(yamlStart));
|
|
3094
|
+
if (closer === null) return null;
|
|
3095
|
+
const closeStart = yamlStart + closer.index;
|
|
3096
|
+
const closeEnd = closeStart + closer[0].length;
|
|
3097
|
+
return {
|
|
3098
|
+
yaml: content.slice(yamlStart, closeStart),
|
|
3099
|
+
body: content.slice(closeEnd).trim(),
|
|
3100
|
+
prefix: content.slice(0, closeEnd)
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function serializeFrontmatter(frontmatter, body) {
|
|
3104
|
+
const keys = Object.keys(frontmatter);
|
|
3105
|
+
if (keys.length === 0) return body;
|
|
3106
|
+
const yamlStr = stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
3107
|
+
return `---
|
|
3108
|
+
${yamlStr}
|
|
3109
|
+
---
|
|
3110
|
+
|
|
3111
|
+
${body}`;
|
|
3112
|
+
}
|
|
2953
3113
|
var LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
|
|
2954
3114
|
var LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
|
|
2955
3115
|
function escapeRegExp(value) {
|
|
@@ -2962,10 +3122,8 @@ function stripManagedBlock(content, start, end) {
|
|
|
2962
3122
|
return content.replace(managedBlockPattern(start, end), "").trim();
|
|
2963
3123
|
}
|
|
2964
3124
|
function splitFrontmatterPrefix(content) {
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
if (close === -1) return { prefix: "", body: content.trim() };
|
|
2968
|
-
return { prefix: content.slice(0, close + 3), body: content.slice(close + 3).trim() };
|
|
3125
|
+
const split = splitFrontmatter(content);
|
|
3126
|
+
return split === null ? { prefix: "", body: content.trim() } : { prefix: split.prefix, body: split.body };
|
|
2969
3127
|
}
|
|
2970
3128
|
function insertAtBodyTop(content, block) {
|
|
2971
3129
|
const { prefix, body } = splitFrontmatterPrefix(content);
|
|
@@ -3019,16 +3177,6 @@ ${rule}`, "").replace(rule, ""),
|
|
|
3019
3177
|
content
|
|
3020
3178
|
);
|
|
3021
3179
|
}
|
|
3022
|
-
function serializeFrontmatter(frontmatter, body) {
|
|
3023
|
-
const keys = Object.keys(frontmatter);
|
|
3024
|
-
if (keys.length === 0) return body;
|
|
3025
|
-
const yamlStr = stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
3026
|
-
return `---
|
|
3027
|
-
${yamlStr}
|
|
3028
|
-
---
|
|
3029
|
-
|
|
3030
|
-
${body}`;
|
|
3031
|
-
}
|
|
3032
3180
|
|
|
3033
3181
|
// src/lessons/skill.ts
|
|
3034
3182
|
var LESSONS_SKILL_NAME = "lessons";
|