@productbrain/mcp 0.0.1-beta.70 → 0.0.1-beta.71
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.
|
@@ -2363,6 +2363,7 @@ function toSnapshot(report, maxTopGaps = 10) {
|
|
|
2363
2363
|
import { readFileSync } from "fs";
|
|
2364
2364
|
import { resolve } from "path";
|
|
2365
2365
|
function resolveRegistries(projectRoot, manifest) {
|
|
2366
|
+
if (!projectRoot) return {};
|
|
2366
2367
|
const resolved = {};
|
|
2367
2368
|
for (const ref of manifest) {
|
|
2368
2369
|
try {
|
|
@@ -2374,7 +2375,10 @@ function resolveRegistries(projectRoot, manifest) {
|
|
|
2374
2375
|
if (keys.size > 0) {
|
|
2375
2376
|
resolved[ref.id] = keys;
|
|
2376
2377
|
}
|
|
2377
|
-
} catch {
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2380
|
+
console.debug(`[coherence] Skipping ${ref.id} (${ref.path}):`, err instanceof Error ? err.message : err);
|
|
2381
|
+
}
|
|
2378
2382
|
}
|
|
2379
2383
|
}
|
|
2380
2384
|
return resolved;
|
|
@@ -2409,14 +2413,19 @@ function extractBalanced(content, openPos, openChar, closeChar) {
|
|
|
2409
2413
|
i = skipString(content, i, ch);
|
|
2410
2414
|
continue;
|
|
2411
2415
|
}
|
|
2412
|
-
if (ch === "/" && content[i + 1] === "/") {
|
|
2416
|
+
if (ch === "/" && i + 1 < content.length && content[i + 1] === "/") {
|
|
2413
2417
|
while (i < content.length && content[i] !== "\n") i++;
|
|
2414
2418
|
continue;
|
|
2415
2419
|
}
|
|
2416
|
-
if (ch === "/" && content[i + 1] === "*") {
|
|
2420
|
+
if (ch === "/" && i + 1 < content.length && content[i + 1] === "*") {
|
|
2417
2421
|
i += 2;
|
|
2418
|
-
while (i < content.length
|
|
2419
|
-
|
|
2422
|
+
while (i < content.length) {
|
|
2423
|
+
if (content[i] === "/" && i > 0 && content[i - 1] === "*") {
|
|
2424
|
+
i++;
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
i++;
|
|
2428
|
+
}
|
|
2420
2429
|
continue;
|
|
2421
2430
|
}
|
|
2422
2431
|
if (ch === openChar) depth++;
|
|
@@ -2429,11 +2438,12 @@ function extractBalanced(content, openPos, openChar, closeChar) {
|
|
|
2429
2438
|
function skipString(content, start, quote) {
|
|
2430
2439
|
let i = start + 1;
|
|
2431
2440
|
while (i < content.length) {
|
|
2432
|
-
|
|
2441
|
+
const ch = content[i];
|
|
2442
|
+
if (ch === "\\") {
|
|
2433
2443
|
i += 2;
|
|
2434
2444
|
continue;
|
|
2435
2445
|
}
|
|
2436
|
-
if (
|
|
2446
|
+
if (ch === quote) return i + 1;
|
|
2437
2447
|
i++;
|
|
2438
2448
|
}
|
|
2439
2449
|
return i;
|
|
@@ -2454,14 +2464,19 @@ function extractObjectKeys(block) {
|
|
|
2454
2464
|
let i = 0;
|
|
2455
2465
|
while (i < block.length) {
|
|
2456
2466
|
const ch = block[i];
|
|
2457
|
-
if (ch === "/" && block[i + 1] === "/") {
|
|
2467
|
+
if (ch === "/" && i + 1 < block.length && block[i + 1] === "/") {
|
|
2458
2468
|
while (i < block.length && block[i] !== "\n") i++;
|
|
2459
2469
|
continue;
|
|
2460
2470
|
}
|
|
2461
|
-
if (ch === "/" && block[i + 1] === "*") {
|
|
2471
|
+
if (ch === "/" && i + 1 < block.length && block[i + 1] === "*") {
|
|
2462
2472
|
i += 2;
|
|
2463
|
-
while (i < block.length
|
|
2464
|
-
|
|
2473
|
+
while (i < block.length) {
|
|
2474
|
+
if (block[i] === "/" && i > 0 && block[i - 1] === "*") {
|
|
2475
|
+
i++;
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
i++;
|
|
2479
|
+
}
|
|
2465
2480
|
continue;
|
|
2466
2481
|
}
|
|
2467
2482
|
if (depth === 0) {
|
|
@@ -2515,6 +2530,7 @@ function extractSetValues(block) {
|
|
|
2515
2530
|
// src/lib/coherence/git-detection.ts
|
|
2516
2531
|
import { execSync } from "child_process";
|
|
2517
2532
|
function detectTouchedRegistries(projectRoot) {
|
|
2533
|
+
if (!projectRoot) return /* @__PURE__ */ new Set();
|
|
2518
2534
|
const manifestPaths = new Set(REGISTRY_MANIFEST.map((r) => r.path));
|
|
2519
2535
|
const touched = /* @__PURE__ */ new Set();
|
|
2520
2536
|
try {
|
|
@@ -2529,7 +2545,10 @@ function detectTouchedRegistries(projectRoot) {
|
|
|
2529
2545
|
touched.add(trimmed);
|
|
2530
2546
|
}
|
|
2531
2547
|
}
|
|
2532
|
-
} catch {
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2550
|
+
console.debug("[coherence] Git detection failed:", err instanceof Error ? err.message : err);
|
|
2551
|
+
}
|
|
2533
2552
|
}
|
|
2534
2553
|
return touched;
|
|
2535
2554
|
}
|
|
@@ -2599,13 +2618,16 @@ function computeDelta(before, after) {
|
|
|
2599
2618
|
function renderCoherenceDelta(delta) {
|
|
2600
2619
|
const lines = ["### Coherence Delta"];
|
|
2601
2620
|
const p = (n) => n === 1 ? "" : "s";
|
|
2621
|
+
const absNet = Math.abs(delta.netChange);
|
|
2622
|
+
const approx = absNet > delta.gapsFixed + delta.gapsIntroduced;
|
|
2623
|
+
const prefix = approx ? "at least " : "";
|
|
2602
2624
|
if (delta.verdict === "improved") {
|
|
2603
2625
|
lines.push(
|
|
2604
|
-
`Registry coherence **improved**: ${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
|
|
2626
|
+
`Registry coherence **improved**: ${prefix}${delta.gapsFixed} gap${p(delta.gapsFixed)} fixed` + (delta.gapsIntroduced > 0 ? `, ${delta.gapsIntroduced} introduced` : "") + ` (net ${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}.`
|
|
2605
2627
|
);
|
|
2606
2628
|
} else if (delta.verdict === "degraded") {
|
|
2607
2629
|
lines.push(
|
|
2608
|
-
`**Registry coherence degraded**: ${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
|
|
2630
|
+
`**Registry coherence degraded**: ${prefix}${delta.gapsIntroduced} new gap${p(delta.gapsIntroduced)} introduced` + (delta.gapsFixed > 0 ? `, ${delta.gapsFixed} fixed` : "") + ` (net +${delta.netChange}). ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} total.`
|
|
2609
2631
|
);
|
|
2610
2632
|
} else {
|
|
2611
2633
|
lines.push(
|
|
@@ -2615,6 +2637,58 @@ function renderCoherenceDelta(delta) {
|
|
|
2615
2637
|
lines.push("");
|
|
2616
2638
|
return lines;
|
|
2617
2639
|
}
|
|
2640
|
+
function findPersistentGaps(delta, currentViolations) {
|
|
2641
|
+
const beforeSet = new Set(delta.before.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2642
|
+
const afterSet = new Set(delta.after.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2643
|
+
const persistentKeys = /* @__PURE__ */ new Set();
|
|
2644
|
+
for (const key of beforeSet) {
|
|
2645
|
+
if (afterSet.has(key)) persistentKeys.add(key);
|
|
2646
|
+
}
|
|
2647
|
+
if (persistentKeys.size === 0) return [];
|
|
2648
|
+
const violationMap = /* @__PURE__ */ new Map();
|
|
2649
|
+
for (const v of currentViolations) {
|
|
2650
|
+
violationMap.set(`${v.registryId}::${v.collectionSlug}`, v);
|
|
2651
|
+
}
|
|
2652
|
+
return [...persistentKeys].map((key) => {
|
|
2653
|
+
const [registry = "", slug = ""] = key.split("::", 2);
|
|
2654
|
+
return {
|
|
2655
|
+
registry,
|
|
2656
|
+
slug,
|
|
2657
|
+
severity: delta.after.topGaps.find((g) => g.registry === registry && g.slug === slug)?.severity ?? "warning",
|
|
2658
|
+
violation: violationMap.get(key) ?? null
|
|
2659
|
+
};
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
function renderPersistentGapOffers(gaps) {
|
|
2663
|
+
if (gaps.length === 0) return [];
|
|
2664
|
+
const lines = [
|
|
2665
|
+
"### Persistent Coherence Gaps",
|
|
2666
|
+
"",
|
|
2667
|
+
"These gaps existed before and after this session \u2014 they represent structural registry drift.",
|
|
2668
|
+
"Want me to capture them as tensions on the Chain?",
|
|
2669
|
+
""
|
|
2670
|
+
];
|
|
2671
|
+
const SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
|
|
2672
|
+
const errorsFirst = [...gaps].sort(
|
|
2673
|
+
(a, b) => SEVERITY_RANK2[a.severity] - SEVERITY_RANK2[b.severity]
|
|
2674
|
+
);
|
|
2675
|
+
for (const gap of errorsFirst.slice(0, 5)) {
|
|
2676
|
+
const v = gap.violation;
|
|
2677
|
+
const fixHint = v?.fix ? ` \u2014 ${v.fix}` : "";
|
|
2678
|
+
lines.push(
|
|
2679
|
+
`- **\`${gap.slug}\`** missing from \`${gap.registry}\` (${gap.severity})${fixHint}`
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
if (errorsFirst.length > 5) {
|
|
2683
|
+
lines.push(`- ...and ${errorsFirst.length - 5} more`);
|
|
2684
|
+
}
|
|
2685
|
+
lines.push("");
|
|
2686
|
+
lines.push(
|
|
2687
|
+
'_Say "capture these as tensions" to commit them to the Chain, or skip to continue._'
|
|
2688
|
+
);
|
|
2689
|
+
lines.push("");
|
|
2690
|
+
return lines;
|
|
2691
|
+
}
|
|
2618
2692
|
|
|
2619
2693
|
// src/lib/resolve-project-root.ts
|
|
2620
2694
|
import { existsSync } from "fs";
|
|
@@ -2623,6 +2697,8 @@ function resolveProjectRoot() {
|
|
|
2623
2697
|
const candidates = [
|
|
2624
2698
|
process.env.WORKSPACE_PATH,
|
|
2625
2699
|
process.cwd(),
|
|
2700
|
+
// Parent of cwd: handles monorepo subpackages (e.g. packages/mcp-server)
|
|
2701
|
+
// where the Convex schema lives one level up.
|
|
2626
2702
|
resolve2(process.cwd(), "..")
|
|
2627
2703
|
].filter(Boolean);
|
|
2628
2704
|
for (const dir of candidates) {
|
|
@@ -2652,16 +2728,24 @@ async function mapWithConcurrency(items, mapper, concurrency = 3) {
|
|
|
2652
2728
|
);
|
|
2653
2729
|
return results;
|
|
2654
2730
|
}
|
|
2731
|
+
function isValidTopGap(g) {
|
|
2732
|
+
if (typeof g !== "object" || g === null) return false;
|
|
2733
|
+
const obj = g;
|
|
2734
|
+
return typeof obj.registry === "string" && typeof obj.slug === "string" && typeof obj.severity === "string" && (obj.severity === "error" || obj.severity === "warning" || obj.severity === "info");
|
|
2735
|
+
}
|
|
2655
2736
|
async function fetchSessionCoherenceSnapshot(sessionId) {
|
|
2656
2737
|
try {
|
|
2657
2738
|
const session = await mcpCall("agent.getSession", {
|
|
2658
2739
|
sessionId
|
|
2659
2740
|
});
|
|
2660
2741
|
const raw = session?.coherenceSnapshot;
|
|
2661
|
-
if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number") {
|
|
2742
|
+
if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number" && Array.isArray(raw.topGaps) && raw.topGaps.every(isValidTopGap)) {
|
|
2662
2743
|
return raw;
|
|
2663
2744
|
}
|
|
2664
|
-
} catch {
|
|
2745
|
+
} catch (err) {
|
|
2746
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2747
|
+
console.debug("[coherence] Snapshot fetch failed:", err instanceof Error ? err.message : err);
|
|
2748
|
+
}
|
|
2665
2749
|
}
|
|
2666
2750
|
return null;
|
|
2667
2751
|
}
|
|
@@ -2760,6 +2844,7 @@ async function runWrapupReview() {
|
|
|
2760
2844
|
lines.push("");
|
|
2761
2845
|
}
|
|
2762
2846
|
let coherenceVerdict;
|
|
2847
|
+
let persistentGaps;
|
|
2763
2848
|
try {
|
|
2764
2849
|
const projectRoot = resolveProjectRoot();
|
|
2765
2850
|
if (projectRoot) {
|
|
@@ -2781,6 +2866,10 @@ async function runWrapupReview() {
|
|
|
2781
2866
|
);
|
|
2782
2867
|
lines.push("");
|
|
2783
2868
|
}
|
|
2869
|
+
persistentGaps = findPersistentGaps(delta, report.violations);
|
|
2870
|
+
if (persistentGaps.length > 0) {
|
|
2871
|
+
lines.push(...renderPersistentGapOffers(persistentGaps));
|
|
2872
|
+
}
|
|
2784
2873
|
} else if (touchedFiles.size > 0) {
|
|
2785
2874
|
lines.push("### Coherence Check");
|
|
2786
2875
|
lines.push(
|
|
@@ -2797,7 +2886,10 @@ async function runWrapupReview() {
|
|
|
2797
2886
|
}
|
|
2798
2887
|
}
|
|
2799
2888
|
}
|
|
2800
|
-
} catch {
|
|
2889
|
+
} catch (err) {
|
|
2890
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
2891
|
+
console.debug("[coherence] Wrapup coherence check failed:", err instanceof Error ? err.message : err);
|
|
2892
|
+
}
|
|
2801
2893
|
}
|
|
2802
2894
|
if (data.drafts.length > 0) {
|
|
2803
2895
|
const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
|
|
@@ -2807,7 +2899,7 @@ async function runWrapupReview() {
|
|
|
2807
2899
|
lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
|
|
2808
2900
|
}
|
|
2809
2901
|
const gapCount = getSessionGaps().length;
|
|
2810
|
-
return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict };
|
|
2902
|
+
return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict, persistentGaps };
|
|
2811
2903
|
}
|
|
2812
2904
|
async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
2813
2905
|
requireWriteAccess();
|
|
@@ -2932,7 +3024,7 @@ function registerWrapupTools(server) {
|
|
|
2932
3024
|
}
|
|
2933
3025
|
);
|
|
2934
3026
|
}
|
|
2935
|
-
const { text, data, suggestions, failureCode, gapCount, coherenceVerdict } = await runWrapupReview();
|
|
3027
|
+
const { text, data, suggestions, failureCode, gapCount, coherenceVerdict, persistentGaps } = await runWrapupReview();
|
|
2936
3028
|
lastReviewData = data;
|
|
2937
3029
|
lastReviewSuggestions = suggestions;
|
|
2938
3030
|
lastReviewSessionId = getAgentSessionId();
|
|
@@ -2945,16 +3037,18 @@ ${text}` : text;
|
|
|
2945
3037
|
const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
|
|
2946
3038
|
const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
|
|
2947
3039
|
const coherenceSummary = coherenceVerdict ? `, coherence: ${coherenceVerdict}` : "";
|
|
3040
|
+
const persistentSummary = persistentGaps?.length ? `, ${persistentGaps.length} persistent coherence gaps` : "";
|
|
2948
3041
|
return successResult(
|
|
2949
3042
|
fullText,
|
|
2950
|
-
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}.`,
|
|
3043
|
+
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}${persistentSummary}.`,
|
|
2951
3044
|
{
|
|
2952
3045
|
drafts: data.drafts.length,
|
|
2953
3046
|
committed: data.committed.length,
|
|
2954
3047
|
uncommitted: data.summary.uncommitted,
|
|
2955
3048
|
suggestedLinks: suggestions.length,
|
|
2956
3049
|
knowledgeGaps: gapCount ?? 0,
|
|
2957
|
-
...coherenceVerdict ? { coherenceVerdict } : {}
|
|
3050
|
+
...coherenceVerdict ? { coherenceVerdict } : {},
|
|
3051
|
+
...persistentGaps?.length ? { persistentCoherenceGaps: persistentGaps.length } : {}
|
|
2958
3052
|
},
|
|
2959
3053
|
next
|
|
2960
3054
|
);
|
|
@@ -7687,16 +7781,19 @@ function buildCoherenceSection(projectRoot) {
|
|
|
7687
7781
|
try {
|
|
7688
7782
|
const root = projectRoot ?? resolveProjectRoot() ?? process.cwd();
|
|
7689
7783
|
return checkAndRender(root);
|
|
7690
|
-
} catch {
|
|
7784
|
+
} catch (err) {
|
|
7785
|
+
if (process.env.DEBUG_COHERENCE) {
|
|
7786
|
+
console.debug("[coherence] buildCoherenceSection failed:", err instanceof Error ? err.message : err);
|
|
7787
|
+
}
|
|
7691
7788
|
return null;
|
|
7692
7789
|
}
|
|
7693
7790
|
}
|
|
7694
7791
|
function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
7695
7792
|
const betNames = activeBets.map((b) => b.name);
|
|
7696
|
-
const taskWords = task
|
|
7793
|
+
const taskWords = extractKeywords(task);
|
|
7697
7794
|
const matchingBet = activeBets.find((b) => {
|
|
7698
|
-
const
|
|
7699
|
-
return taskWords.some((
|
|
7795
|
+
const words = (b.name ?? "").toLowerCase().split(/\s+/);
|
|
7796
|
+
return taskWords.some((tw) => words.some((w) => w === tw || w.startsWith(tw + "-")));
|
|
7700
7797
|
});
|
|
7701
7798
|
if (matchingBet) {
|
|
7702
7799
|
return { aligned: true, matchedBet: matchingBet.name, matchSource: "active_bet", betNames };
|
|
@@ -7711,11 +7808,11 @@ function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
|
7711
7808
|
}
|
|
7712
7809
|
function buildAlignmentCheckLines(result) {
|
|
7713
7810
|
const lines = ["## Alignment Check"];
|
|
7714
|
-
if (result.aligned && result.matchSource === "active_bet") {
|
|
7811
|
+
if (result.aligned && result.matchSource === "active_bet" && result.matchedBet) {
|
|
7715
7812
|
lines.push(
|
|
7716
7813
|
`Task aligns with active bet: **${result.matchedBet}**. Proceed.`
|
|
7717
7814
|
);
|
|
7718
|
-
} else if (result.aligned && result.matchSource === "task_context") {
|
|
7815
|
+
} else if (result.aligned && result.matchSource === "task_context" && result.matchedBet) {
|
|
7719
7816
|
const noActiveBets = result.betNames.length === 0;
|
|
7720
7817
|
lines.push(
|
|
7721
7818
|
`Task related to **${result.matchedBet}** via task context${noActiveBets ? " (no active bets in horizon=now)" : ""}. Proceed with caution \u2014 confirm scope with the user if uncertain.`
|
|
@@ -7745,7 +7842,15 @@ var RULE5_COMPACT = '**Validate against governance.** Before proposing or buildi
|
|
|
7745
7842
|
var MAX_ENTRIES_NO_TASK = 3;
|
|
7746
7843
|
var MAX_ENTRIES_WITH_TASK = 5;
|
|
7747
7844
|
function extractKeywords(text) {
|
|
7748
|
-
|
|
7845
|
+
const normalized = text.toLowerCase().replace(/[^\w\s-]/g, " ");
|
|
7846
|
+
const tokens = [];
|
|
7847
|
+
for (const raw of normalized.split(/\s+/)) {
|
|
7848
|
+
if (!raw) continue;
|
|
7849
|
+
const parts = raw.split(/[-_]/);
|
|
7850
|
+
if (parts.length > 1) tokens.push(raw.replace(/[-_]/g, ""));
|
|
7851
|
+
for (const p of parts) if (p) tokens.push(p);
|
|
7852
|
+
}
|
|
7853
|
+
return [...new Set(tokens.filter((w) => w.length > 2))];
|
|
7749
7854
|
}
|
|
7750
7855
|
function scoreEntry(entry, keywords) {
|
|
7751
7856
|
const text = `${entry.name} ${entry.description ?? ""}`.toLowerCase();
|
|
@@ -12457,4 +12562,4 @@ export {
|
|
|
12457
12562
|
SERVER_VERSION,
|
|
12458
12563
|
createProductBrainServer
|
|
12459
12564
|
};
|
|
12460
|
-
//# sourceMappingURL=chunk-
|
|
12565
|
+
//# sourceMappingURL=chunk-2GMFQHAF.js.map
|