@productbrain/mcp 0.0.1-beta.69 → 0.0.1-beta.70
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.
|
@@ -2004,6 +2004,635 @@ import { z as z9 } from "zod";
|
|
|
2004
2004
|
|
|
2005
2005
|
// src/tools/wrapup.ts
|
|
2006
2006
|
import { z as z8 } from "zod";
|
|
2007
|
+
|
|
2008
|
+
// src/lib/coherence/data.ts
|
|
2009
|
+
var REGISTRY_MANIFEST = [
|
|
2010
|
+
// ── Canonical UI registries ──────────────────────────────────────
|
|
2011
|
+
{
|
|
2012
|
+
id: "ui-colors",
|
|
2013
|
+
path: "src/lib/utils/collection-colors.ts",
|
|
2014
|
+
exportName: "COLLECTION_COLORS",
|
|
2015
|
+
description: "CSS color mapping for UI components",
|
|
2016
|
+
expectedCoverage: "curated",
|
|
2017
|
+
keyExtraction: "object-keys"
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
id: "routes",
|
|
2021
|
+
path: "src/lib/navigation.ts",
|
|
2022
|
+
exportName: "COLLECTION_ROUTES",
|
|
2023
|
+
description: "Collection slug to dedicated route path",
|
|
2024
|
+
expectedCoverage: "curated",
|
|
2025
|
+
keyExtraction: "object-keys"
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
id: "gap-paths",
|
|
2029
|
+
path: "src/lib/navigation.ts",
|
|
2030
|
+
exportName: "GAP_PATHS",
|
|
2031
|
+
description: "Readiness gap ID to workspace-relative route path",
|
|
2032
|
+
expectedCoverage: "curated",
|
|
2033
|
+
keyExtraction: "object-keys"
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
id: "sidebar-collections",
|
|
2037
|
+
path: "src/lib/navigation.ts",
|
|
2038
|
+
exportName: "SIDEBAR_COLLECTION_SLUGS",
|
|
2039
|
+
description: "Collection slugs with dedicated sidebar entries",
|
|
2040
|
+
expectedCoverage: "subset",
|
|
2041
|
+
keyExtraction: "set-values"
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
id: "display-fields",
|
|
2045
|
+
path: "src/lib/components/EntryPreviewModal.svelte",
|
|
2046
|
+
exportName: "DISPLAY_FIELDS",
|
|
2047
|
+
description: "Fields displayed in entry preview modal per collection",
|
|
2048
|
+
expectedCoverage: "curated",
|
|
2049
|
+
keyExtraction: "object-keys"
|
|
2050
|
+
},
|
|
2051
|
+
// ── Brain Chat registries ────────────────────────────────────────
|
|
2052
|
+
{
|
|
2053
|
+
id: "brain-colors",
|
|
2054
|
+
path: "src/lib/components/brain/brain-types.ts",
|
|
2055
|
+
exportName: "COLLECTION_COLORS",
|
|
2056
|
+
description: "Brain Chat collection color palette",
|
|
2057
|
+
expectedCoverage: "subset",
|
|
2058
|
+
keyExtraction: "object-keys"
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
id: "brain-icons",
|
|
2062
|
+
path: "src/lib/components/brain/brain-types.ts",
|
|
2063
|
+
exportName: "COLLECTION_ICONS",
|
|
2064
|
+
description: "Brain Chat collection icons",
|
|
2065
|
+
expectedCoverage: "subset",
|
|
2066
|
+
keyExtraction: "object-keys"
|
|
2067
|
+
},
|
|
2068
|
+
// ── Graph visualization registries ───────────────────────────────
|
|
2069
|
+
{
|
|
2070
|
+
id: "graph-colors",
|
|
2071
|
+
path: "src/lib/utils/graphToVisNetwork.ts",
|
|
2072
|
+
exportName: "GRAPH_COLLECTION_COLORS",
|
|
2073
|
+
description: "Graph visualization hex color palette",
|
|
2074
|
+
expectedCoverage: "subset",
|
|
2075
|
+
keyExtraction: "object-keys"
|
|
2076
|
+
},
|
|
2077
|
+
{
|
|
2078
|
+
id: "graph-type-colors",
|
|
2079
|
+
path: "src/lib/components/graph/graph.types.ts",
|
|
2080
|
+
exportName: "COLLECTION_COLORS",
|
|
2081
|
+
description: "Chain Graph collection colors (distinct palette)",
|
|
2082
|
+
expectedCoverage: "subset",
|
|
2083
|
+
keyExtraction: "object-keys"
|
|
2084
|
+
},
|
|
2085
|
+
// ── Studio registries ────────────────────────────────────────────
|
|
2086
|
+
{
|
|
2087
|
+
id: "renderers",
|
|
2088
|
+
path: "src/lib/components/studio/renderers/index.ts",
|
|
2089
|
+
exportName: "COLLECTION_RENDERERS",
|
|
2090
|
+
description: "Custom Svelte renderer component per collection",
|
|
2091
|
+
expectedCoverage: "subset",
|
|
2092
|
+
keyExtraction: "object-keys"
|
|
2093
|
+
},
|
|
2094
|
+
// ── Backend registries ───────────────────────────────────────────
|
|
2095
|
+
{
|
|
2096
|
+
id: "classification-map",
|
|
2097
|
+
path: "convex/lib/collectionClassification.ts",
|
|
2098
|
+
exportName: "CLASSIFICATION_MAP",
|
|
2099
|
+
description: "Collection classification: governance, nav group, ID prefix",
|
|
2100
|
+
expectedCoverage: "all",
|
|
2101
|
+
keyExtraction: "object-keys"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
id: "default-collections",
|
|
2105
|
+
path: "convex/lib/collectionClassification.ts",
|
|
2106
|
+
exportName: "DEFAULT_COLLECTIONS",
|
|
2107
|
+
description: "Default collection definitions with field schemas and purposes",
|
|
2108
|
+
expectedCoverage: "all",
|
|
2109
|
+
keyExtraction: "array-slug"
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
id: "classification-meta",
|
|
2113
|
+
path: "convex/mcpKnowledge/classifierPrompt.ts",
|
|
2114
|
+
exportName: "CLASSIFICATION_META",
|
|
2115
|
+
description: "LLM classifier decision checks per collection",
|
|
2116
|
+
expectedCoverage: "all",
|
|
2117
|
+
keyExtraction: "object-keys"
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
id: "collection-floors",
|
|
2121
|
+
path: "convex/workspaceReadiness.ts",
|
|
2122
|
+
exportName: "COLLECTION_FLOORS",
|
|
2123
|
+
description: "Minimum user-authored entries required per collection",
|
|
2124
|
+
expectedCoverage: "curated",
|
|
2125
|
+
keyExtraction: "object-keys"
|
|
2126
|
+
}
|
|
2127
|
+
];
|
|
2128
|
+
var COLLECTION_REGISTRY_RULES = [
|
|
2129
|
+
// ── Sync: authoritative registries must agree ─────────────────────
|
|
2130
|
+
{
|
|
2131
|
+
id: "sync-classification-defaults",
|
|
2132
|
+
type: "sync",
|
|
2133
|
+
description: "CLASSIFICATION_MAP and DEFAULT_COLLECTIONS should cover the same collections",
|
|
2134
|
+
subject: "classification-map",
|
|
2135
|
+
reference: "default-collections"
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
id: "sync-classification-classifier",
|
|
2139
|
+
type: "sync",
|
|
2140
|
+
description: "CLASSIFICATION_MAP and CLASSIFICATION_META should cover the same collections",
|
|
2141
|
+
subject: "classification-map",
|
|
2142
|
+
reference: "classification-meta"
|
|
2143
|
+
},
|
|
2144
|
+
// ── Coverage: downstream registries should cover important collections ──
|
|
2145
|
+
{
|
|
2146
|
+
id: "coverage-floors",
|
|
2147
|
+
type: "coverage",
|
|
2148
|
+
description: "COLLECTION_FLOORS should cover all classified collections",
|
|
2149
|
+
subject: "collection-floors",
|
|
2150
|
+
reference: "classification-map"
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
id: "coverage-ui-colors",
|
|
2154
|
+
type: "coverage",
|
|
2155
|
+
description: "UI colors should cover collections that have sidebar routes",
|
|
2156
|
+
subject: "ui-colors",
|
|
2157
|
+
reference: "routes"
|
|
2158
|
+
},
|
|
2159
|
+
// ── Subset: specialized registries should only reference known collections ──
|
|
2160
|
+
{
|
|
2161
|
+
id: "subset-brain-colors",
|
|
2162
|
+
type: "subset",
|
|
2163
|
+
description: "Brain colors should only reference classified collections",
|
|
2164
|
+
subject: "brain-colors",
|
|
2165
|
+
reference: "classification-map"
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
id: "subset-brain-icons",
|
|
2169
|
+
type: "subset",
|
|
2170
|
+
description: "Brain icons should only reference classified collections",
|
|
2171
|
+
subject: "brain-icons",
|
|
2172
|
+
reference: "classification-map"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
id: "subset-renderers",
|
|
2176
|
+
type: "subset",
|
|
2177
|
+
description: "Renderers should only reference classified collections",
|
|
2178
|
+
subject: "renderers",
|
|
2179
|
+
reference: "classification-map"
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
id: "subset-display-fields",
|
|
2183
|
+
type: "subset",
|
|
2184
|
+
description: "Display fields should only reference classified collections",
|
|
2185
|
+
subject: "display-fields",
|
|
2186
|
+
reference: "classification-map"
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
id: "subset-graph-colors",
|
|
2190
|
+
type: "subset",
|
|
2191
|
+
description: "Graph colors should only reference classified collections",
|
|
2192
|
+
subject: "graph-colors",
|
|
2193
|
+
reference: "classification-map"
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
id: "subset-graph-type-colors",
|
|
2197
|
+
type: "subset",
|
|
2198
|
+
description: "Graph type colors should only reference classified collections",
|
|
2199
|
+
subject: "graph-type-colors",
|
|
2200
|
+
reference: "classification-map"
|
|
2201
|
+
},
|
|
2202
|
+
// ── Implication: presence in one registry implies presence in another ──
|
|
2203
|
+
{
|
|
2204
|
+
id: "implication-routes-need-colors",
|
|
2205
|
+
type: "implication",
|
|
2206
|
+
description: "Collections with routes should also have UI colors",
|
|
2207
|
+
subject: "routes",
|
|
2208
|
+
reference: "ui-colors"
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
id: "implication-sidebar-needs-route",
|
|
2212
|
+
type: "implication",
|
|
2213
|
+
description: "Sidebar collections should have a route defined",
|
|
2214
|
+
subject: "sidebar-collections",
|
|
2215
|
+
reference: "routes"
|
|
2216
|
+
}
|
|
2217
|
+
];
|
|
2218
|
+
|
|
2219
|
+
// src/lib/coherence/engine.ts
|
|
2220
|
+
var MAX_VIOLATIONS = 50;
|
|
2221
|
+
var SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
|
|
2222
|
+
function severityFromCoverage(coverage) {
|
|
2223
|
+
switch (coverage) {
|
|
2224
|
+
case "all":
|
|
2225
|
+
return "error";
|
|
2226
|
+
case "curated":
|
|
2227
|
+
return "warning";
|
|
2228
|
+
case "subset":
|
|
2229
|
+
return "info";
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function evaluateCoverage({ rule, subjectRef, subjectKeys, referenceKeys }) {
|
|
2233
|
+
const severity = rule.severityOverride ?? severityFromCoverage(subjectRef.expectedCoverage);
|
|
2234
|
+
const violations = [];
|
|
2235
|
+
for (const slug of referenceKeys) {
|
|
2236
|
+
if (!subjectKeys.has(slug)) {
|
|
2237
|
+
violations.push({
|
|
2238
|
+
ruleId: rule.id,
|
|
2239
|
+
ruleType: "coverage",
|
|
2240
|
+
registryId: rule.subject,
|
|
2241
|
+
collectionSlug: slug,
|
|
2242
|
+
message: `"${slug}" missing from ${subjectRef.exportName} (${subjectRef.path})`,
|
|
2243
|
+
severity,
|
|
2244
|
+
fix: `Add "${slug}" to ${subjectRef.exportName} in ${subjectRef.path}`
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return violations;
|
|
2249
|
+
}
|
|
2250
|
+
function evaluateSync({ rule, subjectRef, subjectKeys, referenceRef, referenceKeys }) {
|
|
2251
|
+
const violations = [];
|
|
2252
|
+
for (const slug of referenceKeys) {
|
|
2253
|
+
if (!subjectKeys.has(slug)) {
|
|
2254
|
+
violations.push({
|
|
2255
|
+
ruleId: rule.id,
|
|
2256
|
+
ruleType: "sync",
|
|
2257
|
+
registryId: rule.subject,
|
|
2258
|
+
collectionSlug: slug,
|
|
2259
|
+
message: `"${slug}" in ${referenceRef.exportName} but missing from ${subjectRef.exportName}`,
|
|
2260
|
+
severity: "error",
|
|
2261
|
+
fix: `Add "${slug}" to ${subjectRef.exportName} in ${subjectRef.path}`
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
for (const slug of subjectKeys) {
|
|
2266
|
+
if (!referenceKeys.has(slug)) {
|
|
2267
|
+
violations.push({
|
|
2268
|
+
ruleId: rule.id,
|
|
2269
|
+
ruleType: "sync",
|
|
2270
|
+
registryId: referenceRef.id,
|
|
2271
|
+
collectionSlug: slug,
|
|
2272
|
+
message: `"${slug}" in ${subjectRef.exportName} but missing from ${referenceRef.exportName}`,
|
|
2273
|
+
severity: "error",
|
|
2274
|
+
fix: `Add "${slug}" to ${referenceRef.exportName} in ${referenceRef.path}`
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return violations;
|
|
2279
|
+
}
|
|
2280
|
+
function evaluateSubset({ rule, subjectRef, subjectKeys, referenceRef, referenceKeys }) {
|
|
2281
|
+
const severity = rule.severityOverride ?? severityFromCoverage(subjectRef.expectedCoverage);
|
|
2282
|
+
const violations = [];
|
|
2283
|
+
for (const slug of subjectKeys) {
|
|
2284
|
+
if (!referenceKeys.has(slug)) {
|
|
2285
|
+
violations.push({
|
|
2286
|
+
ruleId: rule.id,
|
|
2287
|
+
ruleType: "subset",
|
|
2288
|
+
registryId: rule.subject,
|
|
2289
|
+
collectionSlug: slug,
|
|
2290
|
+
message: `"${slug}" in ${subjectRef.exportName} is not a classified collection`,
|
|
2291
|
+
severity,
|
|
2292
|
+
fix: `Remove "${slug}" from ${subjectRef.exportName}, or add it to ${referenceRef.exportName}`
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
return violations;
|
|
2297
|
+
}
|
|
2298
|
+
function evaluateImplication({ rule, subjectRef, subjectKeys, referenceRef, referenceKeys }) {
|
|
2299
|
+
const severity = rule.severityOverride ?? severityFromCoverage(referenceRef.expectedCoverage);
|
|
2300
|
+
const violations = [];
|
|
2301
|
+
for (const slug of subjectKeys) {
|
|
2302
|
+
if (!referenceKeys.has(slug)) {
|
|
2303
|
+
violations.push({
|
|
2304
|
+
ruleId: rule.id,
|
|
2305
|
+
ruleType: "implication",
|
|
2306
|
+
registryId: referenceRef.id,
|
|
2307
|
+
collectionSlug: slug,
|
|
2308
|
+
message: `"${slug}" has ${subjectRef.description} but no ${referenceRef.description}`,
|
|
2309
|
+
severity,
|
|
2310
|
+
fix: `Add "${slug}" to ${referenceRef.exportName} in ${referenceRef.path}`
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return violations;
|
|
2315
|
+
}
|
|
2316
|
+
var EVALUATORS = {
|
|
2317
|
+
coverage: evaluateCoverage,
|
|
2318
|
+
sync: evaluateSync,
|
|
2319
|
+
subset: evaluateSubset,
|
|
2320
|
+
implication: evaluateImplication
|
|
2321
|
+
};
|
|
2322
|
+
function evaluateCoherence(manifest, rules, resolved, checkedAt) {
|
|
2323
|
+
const registryMap = new Map(manifest.map((r) => [r.id, r]));
|
|
2324
|
+
const allViolations = [];
|
|
2325
|
+
for (const rule of rules) {
|
|
2326
|
+
const subjectRef = registryMap.get(rule.subject);
|
|
2327
|
+
if (!subjectRef) continue;
|
|
2328
|
+
const subjectKeys = resolved[rule.subject];
|
|
2329
|
+
if (!subjectKeys) continue;
|
|
2330
|
+
if (!rule.reference) continue;
|
|
2331
|
+
const referenceRef = registryMap.get(rule.reference);
|
|
2332
|
+
if (!referenceRef) continue;
|
|
2333
|
+
const referenceKeys = resolved[rule.reference];
|
|
2334
|
+
if (!referenceKeys) continue;
|
|
2335
|
+
allViolations.push(
|
|
2336
|
+
...EVALUATORS[rule.type]({ rule, subjectRef, subjectKeys, referenceRef, referenceKeys })
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
allViolations.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
2340
|
+
const capped = allViolations.slice(0, MAX_VIOLATIONS);
|
|
2341
|
+
return {
|
|
2342
|
+
checkedAt: checkedAt ?? Date.now(),
|
|
2343
|
+
totalRegistries: manifest.length,
|
|
2344
|
+
totalRules: rules.length,
|
|
2345
|
+
violations: capped,
|
|
2346
|
+
summary: {
|
|
2347
|
+
errors: capped.filter((v) => v.severity === "error").length,
|
|
2348
|
+
warnings: capped.filter((v) => v.severity === "warning").length,
|
|
2349
|
+
infos: capped.filter((v) => v.severity === "info").length,
|
|
2350
|
+
total: capped.length
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
function toSnapshot(report, maxTopGaps = 10) {
|
|
2355
|
+
return {
|
|
2356
|
+
checkedAt: report.checkedAt,
|
|
2357
|
+
totalGaps: report.summary.total,
|
|
2358
|
+
topGaps: report.violations.filter((v) => v.severity !== "info").slice(0, maxTopGaps).map((v) => ({ registry: v.registryId, slug: v.collectionSlug, severity: v.severity }))
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/lib/coherence/resolver.ts
|
|
2363
|
+
import { readFileSync } from "fs";
|
|
2364
|
+
import { resolve } from "path";
|
|
2365
|
+
function resolveRegistries(projectRoot, manifest) {
|
|
2366
|
+
const resolved = {};
|
|
2367
|
+
for (const ref of manifest) {
|
|
2368
|
+
try {
|
|
2369
|
+
const filePath = resolve(projectRoot, ref.path);
|
|
2370
|
+
const content = readFileSync(filePath, "utf-8");
|
|
2371
|
+
const block = extractBlock(content, ref.exportName);
|
|
2372
|
+
if (!block) continue;
|
|
2373
|
+
const keys = extractKeys(block, ref.keyExtraction);
|
|
2374
|
+
if (keys.size > 0) {
|
|
2375
|
+
resolved[ref.id] = keys;
|
|
2376
|
+
}
|
|
2377
|
+
} catch {
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return resolved;
|
|
2381
|
+
}
|
|
2382
|
+
function extractBlock(content, exportName) {
|
|
2383
|
+
const escaped = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2384
|
+
const pattern = new RegExp(
|
|
2385
|
+
`(?:export\\s+)?(?:const|let|var)\\s+${escaped}\\b[^=]*=\\s*`
|
|
2386
|
+
);
|
|
2387
|
+
const match = pattern.exec(content);
|
|
2388
|
+
if (!match) return null;
|
|
2389
|
+
let pos = match.index + match[0].length;
|
|
2390
|
+
while (pos < content.length && /\s/.test(content[pos])) pos++;
|
|
2391
|
+
const ch = content[pos];
|
|
2392
|
+
if (ch === "{" || ch === "[") {
|
|
2393
|
+
return extractBalanced(content, pos, ch, ch === "{" ? "}" : "]");
|
|
2394
|
+
}
|
|
2395
|
+
if (content.slice(pos).startsWith("new Set") || content.slice(pos).startsWith("new\nSet") || content.slice(pos).startsWith("new\n Set")) {
|
|
2396
|
+
const setMatch = content.slice(pos).match(/^new\s+Set\s*\(\s*\[/);
|
|
2397
|
+
if (setMatch) {
|
|
2398
|
+
return extractBalanced(content, pos + setMatch[0].length - 1, "[", "]");
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
function extractBalanced(content, openPos, openChar, closeChar) {
|
|
2404
|
+
let depth = 1;
|
|
2405
|
+
let i = openPos + 1;
|
|
2406
|
+
while (i < content.length && depth > 0) {
|
|
2407
|
+
const ch = content[i];
|
|
2408
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2409
|
+
i = skipString(content, i, ch);
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
2413
|
+
while (i < content.length && content[i] !== "\n") i++;
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (ch === "/" && content[i + 1] === "*") {
|
|
2417
|
+
i += 2;
|
|
2418
|
+
while (i < content.length && !(content[i - 1] === "*" && content[i] === "/")) i++;
|
|
2419
|
+
i++;
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
if (ch === openChar) depth++;
|
|
2423
|
+
else if (ch === closeChar) depth--;
|
|
2424
|
+
i++;
|
|
2425
|
+
}
|
|
2426
|
+
if (depth !== 0) return null;
|
|
2427
|
+
return content.slice(openPos + 1, i - 1);
|
|
2428
|
+
}
|
|
2429
|
+
function skipString(content, start, quote) {
|
|
2430
|
+
let i = start + 1;
|
|
2431
|
+
while (i < content.length) {
|
|
2432
|
+
if (content[i] === "\\" && quote !== "`") {
|
|
2433
|
+
i += 2;
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (content[i] === quote) return i + 1;
|
|
2437
|
+
i++;
|
|
2438
|
+
}
|
|
2439
|
+
return i;
|
|
2440
|
+
}
|
|
2441
|
+
function extractKeys(block, extraction) {
|
|
2442
|
+
switch (extraction) {
|
|
2443
|
+
case "object-keys":
|
|
2444
|
+
return extractObjectKeys(block);
|
|
2445
|
+
case "array-slug":
|
|
2446
|
+
return extractArraySlugs(block);
|
|
2447
|
+
case "set-values":
|
|
2448
|
+
return extractSetValues(block);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function extractObjectKeys(block) {
|
|
2452
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2453
|
+
let depth = 0;
|
|
2454
|
+
let i = 0;
|
|
2455
|
+
while (i < block.length) {
|
|
2456
|
+
const ch = block[i];
|
|
2457
|
+
if (ch === "/" && block[i + 1] === "/") {
|
|
2458
|
+
while (i < block.length && block[i] !== "\n") i++;
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if (ch === "/" && block[i + 1] === "*") {
|
|
2462
|
+
i += 2;
|
|
2463
|
+
while (i < block.length && !(block[i - 1] === "*" && block[i] === "/")) i++;
|
|
2464
|
+
i++;
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
if (depth === 0) {
|
|
2468
|
+
const rest = block.slice(i);
|
|
2469
|
+
const quoted = rest.match(/^(['"])([\w-]+)\1\s*:/);
|
|
2470
|
+
if (quoted) {
|
|
2471
|
+
keys.add(quoted[2]);
|
|
2472
|
+
i += quoted[0].length;
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const ident = rest.match(/^([\w-]+)\s*:/);
|
|
2476
|
+
if (ident) {
|
|
2477
|
+
keys.add(ident[1]);
|
|
2478
|
+
i += ident[0].length;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2483
|
+
i = skipString(block, i, ch);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
2487
|
+
depth++;
|
|
2488
|
+
i++;
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
2492
|
+
depth--;
|
|
2493
|
+
i++;
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
i++;
|
|
2497
|
+
}
|
|
2498
|
+
return keys;
|
|
2499
|
+
}
|
|
2500
|
+
function extractArraySlugs(block) {
|
|
2501
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2502
|
+
for (const m of block.matchAll(/slug:\s*['"](\w[\w-]*)['"]/g)) {
|
|
2503
|
+
slugs.add(m[1]);
|
|
2504
|
+
}
|
|
2505
|
+
return slugs;
|
|
2506
|
+
}
|
|
2507
|
+
function extractSetValues(block) {
|
|
2508
|
+
const values = /* @__PURE__ */ new Set();
|
|
2509
|
+
for (const m of block.matchAll(/['"](\w[\w-]*)['"]/g)) {
|
|
2510
|
+
values.add(m[1]);
|
|
2511
|
+
}
|
|
2512
|
+
return values;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/lib/coherence/git-detection.ts
|
|
2516
|
+
import { execSync } from "child_process";
|
|
2517
|
+
function detectTouchedRegistries(projectRoot) {
|
|
2518
|
+
const manifestPaths = new Set(REGISTRY_MANIFEST.map((r) => r.path));
|
|
2519
|
+
const touched = /* @__PURE__ */ new Set();
|
|
2520
|
+
try {
|
|
2521
|
+
const raw = execSync("git diff --name-only HEAD 2>/dev/null || git diff --name-only", {
|
|
2522
|
+
cwd: projectRoot,
|
|
2523
|
+
encoding: "utf-8",
|
|
2524
|
+
timeout: 5e3
|
|
2525
|
+
});
|
|
2526
|
+
for (const line of raw.split("\n")) {
|
|
2527
|
+
const trimmed = line.trim();
|
|
2528
|
+
if (trimmed && manifestPaths.has(trimmed)) {
|
|
2529
|
+
touched.add(trimmed);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
} catch {
|
|
2533
|
+
}
|
|
2534
|
+
return touched;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/lib/coherence/index.ts
|
|
2538
|
+
function runCoherenceCheck(projectRoot) {
|
|
2539
|
+
const resolved = resolveRegistries(projectRoot, REGISTRY_MANIFEST);
|
|
2540
|
+
if (Object.keys(resolved).length < 2) return null;
|
|
2541
|
+
return evaluateCoherence(REGISTRY_MANIFEST, COLLECTION_REGISTRY_RULES, resolved);
|
|
2542
|
+
}
|
|
2543
|
+
function renderCoherenceLines(report) {
|
|
2544
|
+
const lines = ["## Codebase Coherence"];
|
|
2545
|
+
if (report.summary.total === 0) {
|
|
2546
|
+
lines.push("All registries aligned \u2014 no gaps detected.");
|
|
2547
|
+
lines.push("");
|
|
2548
|
+
return lines;
|
|
2549
|
+
}
|
|
2550
|
+
const parts = [];
|
|
2551
|
+
if (report.summary.errors > 0) parts.push(`${report.summary.errors} error${report.summary.errors === 1 ? "" : "s"}`);
|
|
2552
|
+
if (report.summary.warnings > 0) parts.push(`${report.summary.warnings} warning${report.summary.warnings === 1 ? "" : "s"}`);
|
|
2553
|
+
if (report.summary.infos > 0) parts.push(`${report.summary.infos} info`);
|
|
2554
|
+
lines.push(`**${report.summary.total} gap${report.summary.total === 1 ? "" : "s"}** across ${report.totalRegistries} registries (${parts.join(", ")})`);
|
|
2555
|
+
const errors = report.violations.filter((v) => v.severity === "error");
|
|
2556
|
+
const warnings = report.violations.filter((v) => v.severity === "warning");
|
|
2557
|
+
if (errors.length > 0) {
|
|
2558
|
+
lines.push("");
|
|
2559
|
+
lines.push("**Errors** (must fix):");
|
|
2560
|
+
for (const v of errors.slice(0, 5)) {
|
|
2561
|
+
lines.push(`- ${v.message}`);
|
|
2562
|
+
}
|
|
2563
|
+
if (errors.length > 5) lines.push(`- ...and ${errors.length - 5} more`);
|
|
2564
|
+
}
|
|
2565
|
+
if (warnings.length > 0) {
|
|
2566
|
+
lines.push("");
|
|
2567
|
+
lines.push("**Warnings**:");
|
|
2568
|
+
for (const v of warnings.slice(0, 3)) {
|
|
2569
|
+
lines.push(`- ${v.message}`);
|
|
2570
|
+
}
|
|
2571
|
+
if (warnings.length > 3) lines.push(`- ...and ${warnings.length - 3} more`);
|
|
2572
|
+
}
|
|
2573
|
+
lines.push("");
|
|
2574
|
+
return lines;
|
|
2575
|
+
}
|
|
2576
|
+
function checkAndRender(projectRoot) {
|
|
2577
|
+
const report = runCoherenceCheck(projectRoot);
|
|
2578
|
+
if (!report) return null;
|
|
2579
|
+
return {
|
|
2580
|
+
lines: renderCoherenceLines(report),
|
|
2581
|
+
snapshot: toSnapshot(report)
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function computeDelta(before, after) {
|
|
2585
|
+
const beforeSet = new Set(before.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2586
|
+
const afterSet = new Set(after.topGaps.map((g) => `${g.registry}::${g.slug}`));
|
|
2587
|
+
let gapsFixed = 0;
|
|
2588
|
+
for (const key of beforeSet) {
|
|
2589
|
+
if (!afterSet.has(key)) gapsFixed++;
|
|
2590
|
+
}
|
|
2591
|
+
let gapsIntroduced = 0;
|
|
2592
|
+
for (const key of afterSet) {
|
|
2593
|
+
if (!beforeSet.has(key)) gapsIntroduced++;
|
|
2594
|
+
}
|
|
2595
|
+
const netChange = after.totalGaps - before.totalGaps;
|
|
2596
|
+
const verdict = netChange < 0 ? "improved" : netChange > 0 ? "degraded" : "unchanged";
|
|
2597
|
+
return { before, after, gapsFixed, gapsIntroduced, netChange, verdict };
|
|
2598
|
+
}
|
|
2599
|
+
function renderCoherenceDelta(delta) {
|
|
2600
|
+
const lines = ["### Coherence Delta"];
|
|
2601
|
+
const p = (n) => n === 1 ? "" : "s";
|
|
2602
|
+
if (delta.verdict === "improved") {
|
|
2603
|
+
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" : ""}.`
|
|
2605
|
+
);
|
|
2606
|
+
} else if (delta.verdict === "degraded") {
|
|
2607
|
+
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.`
|
|
2609
|
+
);
|
|
2610
|
+
} else {
|
|
2611
|
+
lines.push(
|
|
2612
|
+
`Registry coherence unchanged. ${delta.after.totalGaps} gap${p(delta.after.totalGaps)} remain${delta.after.totalGaps === 1 ? "s" : ""}` + (delta.gapsFixed > 0 ? ` (${delta.gapsFixed} fixed, ${delta.gapsIntroduced} introduced)` : "") + "."
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
lines.push("");
|
|
2616
|
+
return lines;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// src/lib/resolve-project-root.ts
|
|
2620
|
+
import { existsSync } from "fs";
|
|
2621
|
+
import { resolve as resolve2 } from "path";
|
|
2622
|
+
function resolveProjectRoot() {
|
|
2623
|
+
const candidates = [
|
|
2624
|
+
process.env.WORKSPACE_PATH,
|
|
2625
|
+
process.cwd(),
|
|
2626
|
+
resolve2(process.cwd(), "..")
|
|
2627
|
+
].filter(Boolean);
|
|
2628
|
+
for (const dir of candidates) {
|
|
2629
|
+
const resolved = resolve2(dir);
|
|
2630
|
+
if (existsSync(resolve2(resolved, "convex/schema.ts"))) return resolved;
|
|
2631
|
+
}
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/tools/wrapup.ts
|
|
2007
2636
|
async function mapWithConcurrency(items, mapper, concurrency = 3) {
|
|
2008
2637
|
const results = new Array(items.length);
|
|
2009
2638
|
let index = 0;
|
|
@@ -2023,6 +2652,19 @@ async function mapWithConcurrency(items, mapper, concurrency = 3) {
|
|
|
2023
2652
|
);
|
|
2024
2653
|
return results;
|
|
2025
2654
|
}
|
|
2655
|
+
async function fetchSessionCoherenceSnapshot(sessionId) {
|
|
2656
|
+
try {
|
|
2657
|
+
const session = await mcpCall("agent.getSession", {
|
|
2658
|
+
sessionId
|
|
2659
|
+
});
|
|
2660
|
+
const raw = session?.coherenceSnapshot;
|
|
2661
|
+
if (raw && typeof raw.checkedAt === "number" && typeof raw.totalGaps === "number") {
|
|
2662
|
+
return raw;
|
|
2663
|
+
}
|
|
2664
|
+
} catch {
|
|
2665
|
+
}
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2026
2668
|
async function suggestLinksForEntries(entries) {
|
|
2027
2669
|
const suggestions = [];
|
|
2028
2670
|
const results = await mapWithConcurrency(
|
|
@@ -2117,6 +2759,46 @@ async function runWrapupReview() {
|
|
|
2117
2759
|
lines.push("_Capturing these will make future sessions more effective._");
|
|
2118
2760
|
lines.push("");
|
|
2119
2761
|
}
|
|
2762
|
+
let coherenceVerdict;
|
|
2763
|
+
try {
|
|
2764
|
+
const projectRoot = resolveProjectRoot();
|
|
2765
|
+
if (projectRoot) {
|
|
2766
|
+
const touchedFiles = detectTouchedRegistries(projectRoot);
|
|
2767
|
+
const sessionSnapshot = await fetchSessionCoherenceSnapshot(sessionId);
|
|
2768
|
+
const shouldCheck = touchedFiles.size > 0 || sessionSnapshot !== null;
|
|
2769
|
+
if (shouldCheck) {
|
|
2770
|
+
const report = runCoherenceCheck(projectRoot);
|
|
2771
|
+
if (report) {
|
|
2772
|
+
const afterSnapshot = toSnapshot(report);
|
|
2773
|
+
if (sessionSnapshot) {
|
|
2774
|
+
const delta = computeDelta(sessionSnapshot, afterSnapshot);
|
|
2775
|
+
coherenceVerdict = delta.verdict;
|
|
2776
|
+
const deltaLines = renderCoherenceDelta(delta);
|
|
2777
|
+
lines.push(...deltaLines);
|
|
2778
|
+
if (touchedFiles.size > 0) {
|
|
2779
|
+
lines.push(
|
|
2780
|
+
`_This session touched ${touchedFiles.size} registry file${touchedFiles.size === 1 ? "" : "s"}: ${[...touchedFiles].slice(0, 3).map((f) => `\`${f}\``).join(", ")}${touchedFiles.size > 3 ? ` and ${touchedFiles.size - 3} more` : ""}._`
|
|
2781
|
+
);
|
|
2782
|
+
lines.push("");
|
|
2783
|
+
}
|
|
2784
|
+
} else if (touchedFiles.size > 0) {
|
|
2785
|
+
lines.push("### Coherence Check");
|
|
2786
|
+
lines.push(
|
|
2787
|
+
`This session touched ${touchedFiles.size} registry file${touchedFiles.size === 1 ? "" : "s"} but no session-start snapshot exists \u2014 showing absolute state.`
|
|
2788
|
+
);
|
|
2789
|
+
const { summary } = report;
|
|
2790
|
+
if (summary.total === 0) {
|
|
2791
|
+
lines.push("All registries aligned \u2014 no gaps.");
|
|
2792
|
+
} else {
|
|
2793
|
+
lines.push(`**${summary.total} gap${summary.total === 1 ? "" : "s"}**: ${summary.errors} error${summary.errors === 1 ? "" : "s"}, ${summary.warnings} warning${summary.warnings === 1 ? "" : "s"}, ${summary.infos} info.`);
|
|
2794
|
+
}
|
|
2795
|
+
lines.push("");
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
} catch {
|
|
2801
|
+
}
|
|
2120
2802
|
if (data.drafts.length > 0) {
|
|
2121
2803
|
const draftIds = data.drafts.map((d) => `\`${d.entryId}\``).join(", ");
|
|
2122
2804
|
lines.push("### Actions", "");
|
|
@@ -2125,7 +2807,7 @@ async function runWrapupReview() {
|
|
|
2125
2807
|
lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
|
|
2126
2808
|
}
|
|
2127
2809
|
const gapCount = getSessionGaps().length;
|
|
2128
|
-
return { text: lines.join("\n"), data, suggestions, gapCount };
|
|
2810
|
+
return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict };
|
|
2129
2811
|
}
|
|
2130
2812
|
async function runWrapupCommitAll(data, cachedSuggestions) {
|
|
2131
2813
|
requireWriteAccess();
|
|
@@ -2250,7 +2932,7 @@ function registerWrapupTools(server) {
|
|
|
2250
2932
|
}
|
|
2251
2933
|
);
|
|
2252
2934
|
}
|
|
2253
|
-
const { text, data, suggestions, failureCode, gapCount } = await runWrapupReview();
|
|
2935
|
+
const { text, data, suggestions, failureCode, gapCount, coherenceVerdict } = await runWrapupReview();
|
|
2254
2936
|
lastReviewData = data;
|
|
2255
2937
|
lastReviewSuggestions = suggestions;
|
|
2256
2938
|
lastReviewSessionId = getAgentSessionId();
|
|
@@ -2262,15 +2944,17 @@ ${text}` : text;
|
|
|
2262
2944
|
}
|
|
2263
2945
|
const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
|
|
2264
2946
|
const gapsSummary = gapCount ? `, ${gapCount} knowledge gaps detected` : "";
|
|
2947
|
+
const coherenceSummary = coherenceVerdict ? `, coherence: ${coherenceVerdict}` : "";
|
|
2265
2948
|
return successResult(
|
|
2266
2949
|
fullText,
|
|
2267
|
-
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}.`,
|
|
2950
|
+
`Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions${gapsSummary}${coherenceSummary}.`,
|
|
2268
2951
|
{
|
|
2269
2952
|
drafts: data.drafts.length,
|
|
2270
2953
|
committed: data.committed.length,
|
|
2271
2954
|
uncommitted: data.summary.uncommitted,
|
|
2272
2955
|
suggestedLinks: suggestions.length,
|
|
2273
|
-
knowledgeGaps: gapCount ?? 0
|
|
2956
|
+
knowledgeGaps: gapCount ?? 0,
|
|
2957
|
+
...coherenceVerdict ? { coherenceVerdict } : {}
|
|
2274
2958
|
},
|
|
2275
2959
|
next
|
|
2276
2960
|
);
|
|
@@ -3271,11 +3955,11 @@ This block is the final deliverable of the review. Everything else is supporting
|
|
|
3271
3955
|
num: "02",
|
|
3272
3956
|
label: "Standards & Chain Coherence",
|
|
3273
3957
|
type: "open",
|
|
3274
|
-
instruction: "Run quality check, chain-review gate (if applicable),
|
|
3275
|
-
facilitatorGuidance: "For the BET/entry: quality action=check entryId='<ID>'. If the work produced a chain artifact: chain-review action=gate. Call review-against-rules with the relevant domain (e.g. 'Governance & Decision-Making'). Present: quality score, gate result, rule compliance
|
|
3958
|
+
instruction: "Run quality check, chain-review gate (if applicable), review-against-rules, and codebase coherence gate. Is the implementation coherent with the Chain SSOT? Did it introduce or fix registry drift?",
|
|
3959
|
+
facilitatorGuidance: "For the BET/entry: quality action=check entryId='<ID>'. If the work produced a chain artifact: chain-review action=gate. Call review-against-rules with the relevant domain (e.g. 'Governance & Decision-Making'). **Codebase Coherence Gate (BET-99/FEAT-183):** Compare the Codebase Coherence section from Round 01's orient output against the current state. If orient showed coherence gaps and this implementation touched registry files (collection colors, routes, classification, renderers, etc.), check whether gaps increased or decreased. Gaps increased \u2192 flag as 'Coherence: WARN \u2014 N new gaps introduced' and list them. Gaps decreased \u2192 celebrate as 'Coherence: PASS \u2014 N gaps fixed.' No change \u2192 note as 'Coherence: PASS \u2014 no change.' If Round 01 did not show a coherence section, skip the gate \u2014 do not block on missing baseline. Present: quality score, gate result, rule compliance, coherence gate verdict.",
|
|
3276
3960
|
outputSchema: {
|
|
3277
3961
|
field: "standards",
|
|
3278
|
-
description: "Quality score, gate result, rule compliance",
|
|
3962
|
+
description: "Quality score, gate result, rule compliance, coherence gate verdict (improved/degraded/unchanged)",
|
|
3279
3963
|
format: "structured"
|
|
3280
3964
|
},
|
|
3281
3965
|
maxDurationHint: "5 min"
|
|
@@ -6468,23 +7152,11 @@ No constellation entries were committed.`
|
|
|
6468
7152
|
}
|
|
6469
7153
|
|
|
6470
7154
|
// src/tools/verify.ts
|
|
6471
|
-
import { existsSync, readFileSync } from "fs";
|
|
6472
|
-
import { resolve } from "path";
|
|
7155
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
7156
|
+
import { resolve as resolve3 } from "path";
|
|
6473
7157
|
import { z as z13 } from "zod";
|
|
6474
|
-
function resolveProjectRoot() {
|
|
6475
|
-
const candidates = [
|
|
6476
|
-
process.env.WORKSPACE_PATH,
|
|
6477
|
-
process.cwd(),
|
|
6478
|
-
resolve(process.cwd(), "..")
|
|
6479
|
-
].filter(Boolean);
|
|
6480
|
-
for (const dir of candidates) {
|
|
6481
|
-
const resolved = resolve(dir);
|
|
6482
|
-
if (existsSync(resolve(resolved, "convex/schema.ts"))) return resolved;
|
|
6483
|
-
}
|
|
6484
|
-
return null;
|
|
6485
|
-
}
|
|
6486
7158
|
function parseConvexSchema(schemaPath) {
|
|
6487
|
-
const content =
|
|
7159
|
+
const content = readFileSync2(schemaPath, "utf-8");
|
|
6488
7160
|
const tables = /* @__PURE__ */ new Map();
|
|
6489
7161
|
let currentTable = null;
|
|
6490
7162
|
for (const line of content.split("\n")) {
|
|
@@ -6523,8 +7195,8 @@ function classifyRef(cleaned) {
|
|
|
6523
7195
|
}
|
|
6524
7196
|
function checkFileRef(ref, root) {
|
|
6525
7197
|
const filePart = ref.split(/\s+/)[0];
|
|
6526
|
-
const fullPath =
|
|
6527
|
-
if (
|
|
7198
|
+
const fullPath = resolve3(root, filePart);
|
|
7199
|
+
if (existsSync2(fullPath)) return { result: "verified", reason: "exists" };
|
|
6528
7200
|
return { result: "drifted", reason: `not found: ${filePart}` };
|
|
6529
7201
|
}
|
|
6530
7202
|
function checkSchemaRef(ref, schema) {
|
|
@@ -6636,7 +7308,7 @@ function registerVerifyTools(server) {
|
|
|
6636
7308
|
"Cannot find project root. Set WORKSPACE_PATH in .env.mcp."
|
|
6637
7309
|
);
|
|
6638
7310
|
}
|
|
6639
|
-
const schema = parseConvexSchema(
|
|
7311
|
+
const schema = parseConvexSchema(resolve3(projectRoot, "convex/schema.ts"));
|
|
6640
7312
|
await server.sendLoggingMessage({
|
|
6641
7313
|
level: "info",
|
|
6642
7314
|
data: `Verifying "${collection}" against ${schema.size} schema tables at ${projectRoot}`,
|
|
@@ -7011,6 +7683,14 @@ function buildPlannedWorkSection(work, priorSessions, recoveryBlock) {
|
|
|
7011
7683
|
}
|
|
7012
7684
|
|
|
7013
7685
|
// src/tools/orient-shared.ts
|
|
7686
|
+
function buildCoherenceSection(projectRoot) {
|
|
7687
|
+
try {
|
|
7688
|
+
const root = projectRoot ?? resolveProjectRoot() ?? process.cwd();
|
|
7689
|
+
return checkAndRender(root);
|
|
7690
|
+
} catch {
|
|
7691
|
+
return null;
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7014
7694
|
function runAlignmentCheck(task, activeBets, taskContextHits) {
|
|
7015
7695
|
const betNames = activeBets.map((b) => b.name);
|
|
7016
7696
|
const taskWords = task.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
@@ -7283,10 +7963,13 @@ function buildInterviewResponse(workspaceName) {
|
|
|
7283
7963
|
}
|
|
7284
7964
|
|
|
7285
7965
|
// src/tools/start.ts
|
|
7286
|
-
async function tryMarkOriented(agentSessionId) {
|
|
7966
|
+
async function tryMarkOriented(agentSessionId, coherenceSnapshot) {
|
|
7287
7967
|
if (!agentSessionId) return { oriented: false, orientationStatus: "no_session" };
|
|
7288
7968
|
try {
|
|
7289
|
-
await mcpCall("agent.markOriented", {
|
|
7969
|
+
await mcpCall("agent.markOriented", {
|
|
7970
|
+
sessionId: agentSessionId,
|
|
7971
|
+
...coherenceSnapshot ? { coherenceSnapshot } : {}
|
|
7972
|
+
});
|
|
7290
7973
|
setSessionOriented(true);
|
|
7291
7974
|
return { oriented: true, orientationStatus: "complete" };
|
|
7292
7975
|
} catch {
|
|
@@ -7640,6 +8323,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
7640
8323
|
const isHighReadiness = readiness !== null && readiness.score >= 50;
|
|
7641
8324
|
const stage = readiness?.stage ?? null;
|
|
7642
8325
|
const captureBehaviorNote = wsFullCtx.governanceMode === "open" ? "_In Open mode, user-authored captures commit immediately unless you ask me to keep them as drafts._" : "_Everything I capture stays pending until you confirm._";
|
|
8326
|
+
const coherence = buildCoherenceSection();
|
|
7643
8327
|
lines.push(`# ${wsCtx.workspaceName}`);
|
|
7644
8328
|
lines.push("_Product Brain is ready._");
|
|
7645
8329
|
lines.push("");
|
|
@@ -7711,6 +8395,9 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
7711
8395
|
standards: wsStandards.map(mapGovEntry),
|
|
7712
8396
|
businessRules: wsBusinessRules.map(mapGovEntry)
|
|
7713
8397
|
}, task));
|
|
8398
|
+
if (coherence) {
|
|
8399
|
+
lines.push(...coherence.lines);
|
|
8400
|
+
}
|
|
7714
8401
|
const plannedWork = await queryPlannedWork();
|
|
7715
8402
|
if (hasPlannedWork(plannedWork)) {
|
|
7716
8403
|
lines.push("");
|
|
@@ -7746,6 +8433,9 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
7746
8433
|
lines.push("");
|
|
7747
8434
|
}
|
|
7748
8435
|
lines.push(...buildOperatingProtocol());
|
|
8436
|
+
if (coherence) {
|
|
8437
|
+
lines.push(...coherence.lines);
|
|
8438
|
+
}
|
|
7749
8439
|
if (priorSessions.length > 0 && !recoveryBlock) {
|
|
7750
8440
|
const last = priorSessions[0];
|
|
7751
8441
|
const date = last.startedAt ? new Date(last.startedAt).toISOString().split("T")[0] : "unknown";
|
|
@@ -7767,7 +8457,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors, task) {
|
|
|
7767
8457
|
for (const err of errors) lines.push(`- ${err}`);
|
|
7768
8458
|
lines.push("");
|
|
7769
8459
|
}
|
|
7770
|
-
const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId);
|
|
8460
|
+
const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId, coherence?.snapshot);
|
|
7771
8461
|
const stageLabel = stage ?? "active";
|
|
7772
8462
|
const gapCount = readiness?.gaps?.length ?? 0;
|
|
7773
8463
|
const summaryParts = [`Product Brain ready. Stage: ${stageLabel}.`];
|
|
@@ -8888,8 +9578,8 @@ Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntr
|
|
|
8888
9578
|
}
|
|
8889
9579
|
|
|
8890
9580
|
// src/tools/architecture.ts
|
|
8891
|
-
import { existsSync as
|
|
8892
|
-
import { resolve as
|
|
9581
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
9582
|
+
import { resolve as resolve4, relative, dirname, normalize } from "path";
|
|
8893
9583
|
import { z as z19 } from "zod";
|
|
8894
9584
|
var COLLECTION_SLUG = "architecture";
|
|
8895
9585
|
var COLLECTION_FIELDS = [
|
|
@@ -9224,7 +9914,7 @@ Use \`architecture action=show\` to view the map.`
|
|
|
9224
9914
|
};
|
|
9225
9915
|
}
|
|
9226
9916
|
if (action === "check") {
|
|
9227
|
-
const projectRoot =
|
|
9917
|
+
const projectRoot = resolveProjectRoot();
|
|
9228
9918
|
if (!projectRoot) {
|
|
9229
9919
|
return {
|
|
9230
9920
|
content: [{
|
|
@@ -9261,18 +9951,6 @@ Use \`architecture action=show\` to view the map.`
|
|
|
9261
9951
|
);
|
|
9262
9952
|
trackWriteTool(archAdminTool);
|
|
9263
9953
|
}
|
|
9264
|
-
function resolveProjectRoot2() {
|
|
9265
|
-
const candidates = [
|
|
9266
|
-
process.env.WORKSPACE_PATH,
|
|
9267
|
-
process.cwd(),
|
|
9268
|
-
resolve2(process.cwd(), "..")
|
|
9269
|
-
].filter(Boolean);
|
|
9270
|
-
for (const dir of candidates) {
|
|
9271
|
-
const resolved = resolve2(dir);
|
|
9272
|
-
if (existsSync2(resolve2(resolved, "convex/schema.ts"))) return resolved;
|
|
9273
|
-
}
|
|
9274
|
-
return null;
|
|
9275
|
-
}
|
|
9276
9954
|
function scanDependencies(projectRoot, layers, nodes) {
|
|
9277
9955
|
const layerMap = /* @__PURE__ */ new Map();
|
|
9278
9956
|
for (const l of layers) layerMap.set(l.entryId, l);
|
|
@@ -9291,7 +9969,7 @@ function scanDependencies(projectRoot, layers, nodes) {
|
|
|
9291
9969
|
const nodeViolations = [];
|
|
9292
9970
|
let nodeFileCount = 0;
|
|
9293
9971
|
for (const fp of filePaths) {
|
|
9294
|
-
const absPath =
|
|
9972
|
+
const absPath = resolve4(projectRoot, fp);
|
|
9295
9973
|
const files = collectFiles(absPath);
|
|
9296
9974
|
for (const file of files) {
|
|
9297
9975
|
nodeFileCount++;
|
|
@@ -9368,7 +10046,7 @@ function parseFilePaths(node) {
|
|
|
9368
10046
|
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
9369
10047
|
}
|
|
9370
10048
|
function collectFiles(absPath) {
|
|
9371
|
-
if (!
|
|
10049
|
+
if (!existsSync3(absPath)) return [];
|
|
9372
10050
|
const stat = statSync(absPath);
|
|
9373
10051
|
if (stat.isFile()) {
|
|
9374
10052
|
return isScannableFile(absPath) ? [absPath] : [];
|
|
@@ -9378,7 +10056,7 @@ function collectFiles(absPath) {
|
|
|
9378
10056
|
const walk = (dir) => {
|
|
9379
10057
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
9380
10058
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
9381
|
-
const full =
|
|
10059
|
+
const full = resolve4(dir, entry.name);
|
|
9382
10060
|
if (entry.isDirectory()) walk(full);
|
|
9383
10061
|
else if (isScannableFile(full)) results.push(full);
|
|
9384
10062
|
}
|
|
@@ -9391,7 +10069,7 @@ function isScannableFile(p) {
|
|
|
9391
10069
|
}
|
|
9392
10070
|
function parseImports(filePath) {
|
|
9393
10071
|
try {
|
|
9394
|
-
const content =
|
|
10072
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
9395
10073
|
const re = /(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
9396
10074
|
const imports = [];
|
|
9397
10075
|
let match;
|
|
@@ -9405,10 +10083,10 @@ function parseImports(filePath) {
|
|
|
9405
10083
|
}
|
|
9406
10084
|
var EXTENSIONS = [".ts", ".js", ".svelte", "/index.ts", "/index.js", "/index.svelte"];
|
|
9407
10085
|
function tryResolveWithExtension(absPath) {
|
|
9408
|
-
if (
|
|
10086
|
+
if (existsSync3(absPath) && statSync(absPath).isFile()) return absPath;
|
|
9409
10087
|
for (const ext of EXTENSIONS) {
|
|
9410
10088
|
const withExt = absPath + ext;
|
|
9411
|
-
if (
|
|
10089
|
+
if (existsSync3(withExt)) return withExt;
|
|
9412
10090
|
}
|
|
9413
10091
|
return null;
|
|
9414
10092
|
}
|
|
@@ -9419,11 +10097,11 @@ function resolveImport(imp, fromFile, root) {
|
|
|
9419
10097
|
else if (imp.startsWith("$env/") || imp.startsWith("$app/")) return null;
|
|
9420
10098
|
else if (imp.startsWith("./") || imp.startsWith("../")) {
|
|
9421
10099
|
const fromDir = dirname(fromFile);
|
|
9422
|
-
const abs2 =
|
|
10100
|
+
const abs2 = resolve4(fromDir, imp);
|
|
9423
10101
|
rel = relative(root, abs2);
|
|
9424
10102
|
}
|
|
9425
10103
|
if (!rel) return null;
|
|
9426
|
-
const abs =
|
|
10104
|
+
const abs = resolve4(root, rel);
|
|
9427
10105
|
const actual = tryResolveWithExtension(abs);
|
|
9428
10106
|
return actual ? relative(root, actual) : rel;
|
|
9429
10107
|
}
|
|
@@ -10125,6 +10803,13 @@ function registerHealthTools(server) {
|
|
|
10125
10803
|
const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
|
|
10126
10804
|
lines.push(`Last session (${date}): ${created} created, ${modified} modified`);
|
|
10127
10805
|
}
|
|
10806
|
+
let coherenceSnapshot;
|
|
10807
|
+
const coherence = buildCoherenceSection();
|
|
10808
|
+
if (coherence) {
|
|
10809
|
+
lines.push("");
|
|
10810
|
+
lines.push(...coherence.lines);
|
|
10811
|
+
coherenceSnapshot = coherence.snapshot;
|
|
10812
|
+
}
|
|
10128
10813
|
if (orientEntries) {
|
|
10129
10814
|
lines.push("");
|
|
10130
10815
|
if (task) {
|
|
@@ -10144,7 +10829,10 @@ function registerHealthTools(server) {
|
|
|
10144
10829
|
}
|
|
10145
10830
|
if (agentSessionId) {
|
|
10146
10831
|
try {
|
|
10147
|
-
await mcpCall("agent.markOriented", {
|
|
10832
|
+
await mcpCall("agent.markOriented", {
|
|
10833
|
+
sessionId: agentSessionId,
|
|
10834
|
+
...coherenceSnapshot ? { coherenceSnapshot } : {}
|
|
10835
|
+
});
|
|
10148
10836
|
setSessionOriented(true);
|
|
10149
10837
|
} catch {
|
|
10150
10838
|
}
|
|
@@ -10205,6 +10893,11 @@ function registerHealthTools(server) {
|
|
|
10205
10893
|
const stratum = e.stratum ?? "?";
|
|
10206
10894
|
return `- \`${e.entryId ?? e._id}\` [${type} \xB7 ${stratum}] ${e.name}`;
|
|
10207
10895
|
};
|
|
10896
|
+
let fullCoherenceSnapshot2;
|
|
10897
|
+
const fullCoherence = buildCoherenceSection();
|
|
10898
|
+
if (fullCoherence) {
|
|
10899
|
+
fullCoherenceSnapshot2 = fullCoherence.snapshot;
|
|
10900
|
+
}
|
|
10208
10901
|
if (task) {
|
|
10209
10902
|
lines.push(`**Brain stage: ${orientStage}.** Working on: **${task}**`);
|
|
10210
10903
|
lines.push("");
|
|
@@ -10265,6 +10958,9 @@ function registerHealthTools(server) {
|
|
|
10265
10958
|
);
|
|
10266
10959
|
lines.push(...buildAlignmentCheckLines(result));
|
|
10267
10960
|
}
|
|
10961
|
+
if (fullCoherence) {
|
|
10962
|
+
lines.push(...fullCoherence.lines);
|
|
10963
|
+
}
|
|
10268
10964
|
if (orientEntries) {
|
|
10269
10965
|
if (orientEntries.activeBets?.length > 0) {
|
|
10270
10966
|
lines.push("## Active bets \u2014 current scope");
|
|
@@ -10344,6 +11040,9 @@ function registerHealthTools(server) {
|
|
|
10344
11040
|
lines.push("");
|
|
10345
11041
|
}
|
|
10346
11042
|
lines.push(...buildOperatingProtocol());
|
|
11043
|
+
if (fullCoherence) {
|
|
11044
|
+
lines.push(...fullCoherence.lines);
|
|
11045
|
+
}
|
|
10347
11046
|
if (priorSessions.length > 0 && !recoveryBlock) {
|
|
10348
11047
|
const last = priorSessions[0];
|
|
10349
11048
|
const date = last.startedAt ? new Date(last.startedAt).toISOString().split("T")[0] : "unknown";
|
|
@@ -10366,7 +11065,10 @@ function registerHealthTools(server) {
|
|
|
10366
11065
|
}
|
|
10367
11066
|
if (agentSessionId) {
|
|
10368
11067
|
try {
|
|
10369
|
-
await mcpCall("agent.markOriented", {
|
|
11068
|
+
await mcpCall("agent.markOriented", {
|
|
11069
|
+
sessionId: agentSessionId,
|
|
11070
|
+
...fullCoherenceSnapshot ? { coherenceSnapshot: fullCoherenceSnapshot } : {}
|
|
11071
|
+
});
|
|
10370
11072
|
setSessionOriented(true);
|
|
10371
11073
|
lines.push("---");
|
|
10372
11074
|
lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
|
|
@@ -10400,16 +11102,16 @@ function registerHealthTools(server) {
|
|
|
10400
11102
|
}
|
|
10401
11103
|
|
|
10402
11104
|
// src/resources/index.ts
|
|
10403
|
-
import { existsSync as
|
|
10404
|
-
import { dirname as dirname2, join, resolve as
|
|
11105
|
+
import { existsSync as existsSync4 } from "fs";
|
|
11106
|
+
import { dirname as dirname2, join, resolve as resolve5 } from "path";
|
|
10405
11107
|
import { fileURLToPath } from "url";
|
|
10406
11108
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10407
11109
|
var MODULE_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
10408
11110
|
var UI_VIEW_BASE_CANDIDATES = [
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
11111
|
+
resolve5(MODULE_DIR, "views"),
|
|
11112
|
+
resolve5(MODULE_DIR, "..", "views"),
|
|
11113
|
+
resolve5(MODULE_DIR, "..", "..", "dist", "views"),
|
|
11114
|
+
resolve5(MODULE_DIR, "..", "..", "..", "mcp-views", "dist")
|
|
10413
11115
|
];
|
|
10414
11116
|
function formatEntryMarkdown(entry) {
|
|
10415
11117
|
const id = entry.entryId ? `${entry.entryId}: ` : "";
|
|
@@ -11755,4 +12457,4 @@ export {
|
|
|
11755
12457
|
SERVER_VERSION,
|
|
11756
12458
|
createProductBrainServer
|
|
11757
12459
|
};
|
|
11758
|
-
//# sourceMappingURL=chunk-
|
|
12460
|
+
//# sourceMappingURL=chunk-JOJWCU7A.js.map
|