@productbrain/mcp 0.0.1-beta.2100 → 0.0.1-beta.2115

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.
@@ -675,117 +675,12 @@ async function isGoverned(slug) {
675
675
  }
676
676
 
677
677
  // src/tools/smart-capture.ts
678
- var COMMON_CHECKS = {
679
- clearName: {
680
- id: "clear-name",
681
- label: "Clear, specific name (not vague)",
682
- check: (ctx) => ctx.name.length > 10 && !["new tension", "new entry", "untitled", "test"].includes(ctx.name.toLowerCase()),
683
- suggestion: () => "Rename to something specific \u2014 describe the actual problem or concept."
684
- },
685
- hasDescription: {
686
- id: "has-description",
687
- label: "Description provided (>50 chars)",
688
- check: (ctx) => ctx.description.length > 50,
689
- suggestion: () => "Add a fuller description explaining context and impact."
690
- },
691
- hasRelations: {
692
- id: "has-relations",
693
- label: "At least 1 relation created",
694
- check: (ctx) => ctx.linksCreated.length >= 1,
695
- suggestion: () => "Use `graph action=suggest` and `relations action=create` to add more connections."
696
- },
697
- diverseRelations: {
698
- id: "diverse-relations",
699
- label: "Relations span multiple collections",
700
- check: (ctx) => {
701
- const colls = new Set(ctx.linksCreated.map((l) => l.targetCollection));
702
- return colls.size >= 2;
703
- },
704
- suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
705
- },
706
- hasType: {
707
- id: "has-type",
708
- label: "Has canonical type",
709
- check: (ctx) => !!ctx.data?.canonicalKey || !!ctx.canonicalKey,
710
- suggestion: () => "Classify this entry with a canonical type for better context assembly. Use update-entry to set canonicalKey."
711
- }
712
- };
713
678
  var GENERIC_PROFILE = {
714
679
  governedDraft: false,
715
680
  descriptionField: "description",
716
681
  defaults: [],
717
- recommendedRelationTypes: ["related_to", "references"],
718
- qualityChecks: [
719
- COMMON_CHECKS.clearName,
720
- COMMON_CHECKS.hasDescription,
721
- COMMON_CHECKS.hasRelations,
722
- COMMON_CHECKS.hasType
723
- ]
682
+ recommendedRelationTypes: ["related_to", "references"]
724
683
  };
725
- var NON_DATA_CRITERION_FIELDS = /* @__PURE__ */ new Set(["grounding", "governanceWhy", "governanceSteering"]);
726
- function buildFieldQualityChecks(col) {
727
- const checks = [];
728
- const fields = col.fields ?? [];
729
- const seen = /* @__PURE__ */ new Set();
730
- const addCheck = (check) => {
731
- if (seen.has(check.id)) return;
732
- seen.add(check.id);
733
- checks.push(check);
734
- };
735
- const labelFor = (fieldKey) => {
736
- return fields.find((field) => field.key === fieldKey)?.label ?? fieldKey;
737
- };
738
- for (const field of fields) {
739
- if (field.required === true) {
740
- addCheck({
741
- id: `field-${field.key}-required`,
742
- label: `${field.key} provided`,
743
- check: (ctx) => isNonEmptyValue(ctx.data[field.key]),
744
- suggestion: () => `Fill in ${labelFor(field.key)}.`
745
- });
746
- }
747
- if (typeof field.minLength === "number") {
748
- addCheck({
749
- id: `field-${field.key}-min-length`,
750
- label: `${field.key} min ${field.minLength} chars`,
751
- check: (ctx) => {
752
- const value = ctx.data[field.key];
753
- return typeof value === "string" && value.length >= field.minLength;
754
- },
755
- suggestion: () => `Add more detail to ${labelFor(field.key)} (minimum ${field.minLength} characters).`
756
- });
757
- }
758
- if (typeof field.maxLength === "number") {
759
- addCheck({
760
- id: `field-${field.key}-max-length`,
761
- label: `${field.key} max ${field.maxLength} chars`,
762
- check: (ctx) => {
763
- const value = ctx.data[field.key];
764
- return typeof value !== "string" || value.length <= field.maxLength;
765
- },
766
- suggestion: () => `Shorten ${labelFor(field.key)} to ${field.maxLength} characters or fewer.`
767
- });
768
- }
769
- }
770
- for (const criterion of col.qualityCriteria ?? []) {
771
- if (criterion.active === false) continue;
772
- if (NON_DATA_CRITERION_FIELDS.has(criterion.field)) continue;
773
- const id = criterion.rule === "required" ? `field-${criterion.field}-required` : criterion.rule === "min_length" ? `field-${criterion.field}-min-length` : `quality-${criterion.field}-${criterion.rule}`;
774
- addCheck({
775
- id,
776
- label: `${criterion.field} ${criterion.rule === "required" ? "provided" : criterion.rule === "min_length" ? `min ${criterion.value} chars` : `matches ${criterion.value}`}`,
777
- check: (ctx) => {
778
- const val = ctx.data[criterion.field];
779
- if (criterion.rule === "required") return isNonEmptyValue(val);
780
- if (criterion.rule === "min_length") return typeof val === "string" && val.length >= Number(criterion.value ?? 0);
781
- if (criterion.rule === "pattern") return typeof val === "string" && new RegExp(criterion.value ?? "").test(val);
782
- return true;
783
- },
784
- suggestion: () => `Fill in ${labelFor(criterion.field)} (${criterion.rule}).`
785
- });
786
- }
787
- return checks;
788
- }
789
684
  async function buildRuntimeProfile(slug) {
790
685
  const col = await getCollectionBySlug(slug);
791
686
  if (!col) return GENERIC_PROFILE;
@@ -794,20 +689,11 @@ async function buildRuntimeProfile(slug) {
794
689
  (field) => field.zone === "body" || field.displayHint === "textarea" || field.displayHint === "section"
795
690
  )?.key;
796
691
  const descriptionField = col.descriptionFieldKey || layoutDescriptionField || "description";
797
- const fieldChecks = buildFieldQualityChecks(col);
798
- const qualityChecks = [
799
- COMMON_CHECKS.clearName,
800
- COMMON_CHECKS.hasDescription,
801
- ...fieldChecks,
802
- COMMON_CHECKS.hasRelations,
803
- COMMON_CHECKS.hasType
804
- ];
805
692
  return {
806
693
  governedDraft: col.governed === true,
807
694
  descriptionField,
808
695
  defaults: [],
809
- recommendedRelationTypes: GENERIC_PROFILE.recommendedRelationTypes,
810
- qualityChecks
696
+ recommendedRelationTypes: GENERIC_PROFILE.recommendedRelationTypes
811
697
  };
812
698
  }
813
699
  var profileCache = new ScopedCache(COLLECTION_CACHE_TTL_MS);
@@ -879,85 +765,6 @@ function inferRelationType(_sourceCollection, _targetCollection, profile) {
879
765
  const reason = `profile default (${type})`;
880
766
  return { type, reason };
881
767
  }
882
- function scoreQuality(ctx, profile) {
883
- const checks = profile.qualityChecks.map((qc) => {
884
- const passed2 = qc.check(ctx);
885
- return {
886
- id: qc.id,
887
- label: qc.label,
888
- passed: passed2,
889
- suggestion: passed2 ? void 0 : qc.suggestion?.(ctx)
890
- };
891
- });
892
- const passed = checks.filter((c) => c.passed).length;
893
- const total = checks.length;
894
- const score = total > 0 ? Math.round(passed / total * 10) : 10;
895
- return { score, maxScore: 10, checks };
896
- }
897
- function formatQualityReport(result) {
898
- const failed = result.checks.filter((c) => !c.passed);
899
- const reason = failed.length > 0 ? ` because ${failed.map((c) => c.suggestion ?? c.label.toLowerCase()).join("; ")}` : "";
900
- const lines = [`## Quality: ${result.score}/${result.maxScore}${reason}`];
901
- for (const check of result.checks) {
902
- const icon = check.passed ? "[x]" : "[ ]";
903
- const suggestion = check.passed ? "" : ` \u2014 ${check.suggestion ?? check.label}`;
904
- lines.push(`${icon} ${check.label}${suggestion}`);
905
- }
906
- return lines.join("\n");
907
- }
908
- async function checkEntryQuality(entryId) {
909
- const entry = await kernelQuery("chain.getEntry", { entryId });
910
- if (!entry) {
911
- return {
912
- text: `Entry \`${entryId}\` not found. Try search to find the right ID.`,
913
- quality: { score: 0, maxScore: 10, checks: [] }
914
- };
915
- }
916
- const collections = await getCollections();
917
- const collMap = /* @__PURE__ */ new Map();
918
- for (const c of collections) collMap.set(c._id, c.slug);
919
- const collectionSlug = collMap.get(entry.collectionId) ?? "unknown";
920
- const profile = await getProfile(collectionSlug);
921
- const relations = await kernelQuery("chain.listEntryRelations", { entryId });
922
- const linksCreated = [];
923
- for (const r of relations) {
924
- const otherId = r.fromId === entry._id ? r.toId : r.fromId;
925
- linksCreated.push({
926
- targetEntryId: otherId,
927
- targetName: "",
928
- targetCollection: "",
929
- relationType: r.type
930
- });
931
- }
932
- const descField = profile.descriptionField;
933
- const description = typeof entry.data?.[descField] === "string" ? entry.data[descField] : "";
934
- const ctx = {
935
- collection: collectionSlug,
936
- name: entry.name,
937
- description,
938
- data: entry.data ?? {},
939
- entryId: entry.entryId ?? "",
940
- canonicalKey: entry.canonicalKey,
941
- linksCreated,
942
- linksSuggested: [],
943
- collectionFields: []
944
- };
945
- const quality = scoreQuality(ctx, profile);
946
- const lines = [
947
- `# Quality Check: ${entry.entryId ?? entry.name}`,
948
- `**${entry.name}** in \`${collectionSlug}\` [${entry.status}]`,
949
- "",
950
- formatQualityReport(quality)
951
- ];
952
- if (quality.score < 10) {
953
- const failedChecks = quality.checks.filter((c) => !c.passed && c.suggestion);
954
- if (failedChecks.length > 0) {
955
- lines.push("");
956
- lines.push(`_To improve: use \`update-entry\` to fill missing fields, or \`relations action=create\` to add connections._`);
957
- }
958
- }
959
- return { text: lines.join("\n"), quality };
960
- }
961
768
  var AUTO_LINK_CONFIDENCE_THRESHOLD = 35;
962
769
  var MAX_AUTO_LINKS = 5;
963
770
  var MAX_SUGGESTIONS = 5;
@@ -1269,7 +1076,8 @@ var captureSuccessOutputSchema = z2.object({
1269
1076
  // accept it or a refused response fails to validate on `status` (codex review, completing the
1270
1077
  // coherencyRefusal contract fix).
1271
1078
  status: z2.enum(["draft", "committed", "proposed", "draft_on_failure"]),
1272
- qualityScore: z2.number(),
1079
+ // WP-480 S1: `qualityScore` (required, client N/10) deleted — a named, accepted
1080
+ // breaking output-contract change. The server verdict stays as `qualityVerdict`.
1273
1081
  qualityVerdict: z2.record(z2.unknown()).optional(),
1274
1082
  classifier: captureClassifierSchema.optional(),
1275
1083
  studioUrl: z2.string().optional(),
@@ -1901,19 +1709,6 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
1901
1709
  if (advisedLinks.length > 0) {
1902
1710
  entryWarnings.push(`${advisedLinks.length} auto-link(s) advised, not written (untypeable) \u2014 accept with \`relations action=create\` to promote.`);
1903
1711
  }
1904
- const captureCtx = {
1905
- collection: resolvedCollection,
1906
- name,
1907
- description,
1908
- context,
1909
- data,
1910
- entryId: finalEntryId,
1911
- canonicalKey,
1912
- linksCreated,
1913
- linksSuggested,
1914
- collectionFields: col.fields ?? []
1915
- };
1916
- const quality = scoreQuality(captureCtx, profile);
1917
1712
  const tAfterQuality = Date.now();
1918
1713
  const cardinalityWarning = cardinalityCheck?.warning ?? null;
1919
1714
  if (contradictionWarnings.length > 0) {
@@ -2108,13 +1903,6 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2108
1903
  lines.push(`_Grounding: ${parts.join(", ")}. Review and link if relevant._`);
2109
1904
  }
2110
1905
  }
2111
- lines.push("");
2112
- lines.push(formatQualityReport(quality));
2113
- const failedChecks = quality.checks.filter((c) => !c.passed);
2114
- if (failedChecks.length > 0) {
2115
- lines.push("");
2116
- lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
2117
- }
2118
1906
  const fieldGuidanceSection = formatFieldGuidance(col.fields ?? []);
2119
1907
  if (fieldGuidanceSection) {
2120
1908
  lines.push("");
@@ -2194,17 +1982,18 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2194
1982
  lines.push("");
2195
1983
  lines.push(coachingSection);
2196
1984
  }
1985
+ const hasQualityGaps = verdictResult?.verdict != null && verdictResult.verdict.passed === false || formativeHints.length > 0;
2197
1986
  lines.push("");
2198
1987
  lines.push("## Next Steps");
2199
1988
  const eid = finalEntryId || "(check entry ID)";
2200
1989
  if (finalStatus === "committed") {
2201
1990
  lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover additional links`);
2202
- if (failedChecks.length > 0) {
1991
+ if (hasQualityGaps) {
2203
1992
  lines.push(`2. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 fill missing fields`);
2204
1993
  }
2205
1994
  } else if (finalStatus === "proposed") {
2206
1995
  lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover additional links to support the proposal`);
2207
- if (failedChecks.length > 0) {
1996
+ if (hasQualityGaps) {
2208
1997
  lines.push(`2. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 strengthen the entry before approval`);
2209
1998
  }
2210
1999
  } else {
@@ -2212,7 +2001,7 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2212
2001
  lines.push(`1. **Connect it:** \`graph action=suggest entryId="${eid}"\` \u2014 discover what this should link to`);
2213
2002
  }
2214
2003
  lines.push(`${userLinkResults.length === 0 ? "2" : "1"}. **Accept it:** \`commit-entry entryId="${eid}"\` \u2014 promote from draft to SSOT on the Chain`);
2215
- if (failedChecks.length > 0) {
2004
+ if (hasQualityGaps) {
2216
2005
  lines.push(`${userLinkResults.length === 0 ? "3" : "2"}. **Improve quality:** \`update-entry entryId="${eid}"\` \u2014 fill missing fields`);
2217
2006
  }
2218
2007
  }
@@ -2239,7 +2028,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2239
2028
  }
2240
2029
  next.push({ tool: "commit-entry", description: "Accept onto Chain", parameters: { entryId: finalEntryId } });
2241
2030
  }
2242
- const summary = finalStatus === "committed" ? `Captured and accepted ${finalEntryId} (${name}) onto ${resolvedCollection}. Quality ${quality.score}/10.` : finalStatus === "proposed" ? `Captured ${finalEntryId} (${name}) in ${resolvedCollection} and created a proposal for acceptance. Quality ${quality.score}/10.` : `Captured ${finalEntryId} (${name}) as draft in ${resolvedCollection}. Quality ${quality.score}/10.`;
2031
+ const qualityPhrase = verdictQualityPhrase(verdictResult);
2032
+ const summary = finalStatus === "committed" ? `Captured and accepted ${finalEntryId} (${name}) onto ${resolvedCollection}. ${qualityPhrase}` : finalStatus === "proposed" ? `Captured ${finalEntryId} (${name}) in ${resolvedCollection} and created a proposal for acceptance. ${qualityPhrase}` : `Captured ${finalEntryId} (${name}) as draft in ${resolvedCollection}. ${qualityPhrase}`;
2243
2033
  const expectedFields = (col.fields ?? []).map((f) => ({
2244
2034
  key: f.key,
2245
2035
  type: f.type,
@@ -2275,7 +2065,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2275
2065
  status: finalStatus,
2276
2066
  // WP-465 surface parity: thread full refusal (routes + mode) for coherency-refused auto-commits.
2277
2067
  ...commitRefusal ? { coherencyRefusal: commitRefusal } : {},
2278
- qualityScore: quality.score,
2068
+ // WP-480 S1: the server verdict rides `qualityVerdict` (named breaking
2069
+ // output-contract change, pre-launch).
2279
2070
  qualityVerdict: verdictResult?.verdict ? { ...verdictResult.verdict, source: verdictResult.source ?? "heuristic" } : void 0,
2280
2071
  ...classifierMeta && { classifier: classifierMeta },
2281
2072
  ...studioUrl && { studioUrl },
@@ -3012,6 +2803,16 @@ async function runSemanticConflictPreflight(name, description, collectionHint) {
3012
2803
  return [];
3013
2804
  }
3014
2805
  }
2806
+ function verdictQualityPhrase(result) {
2807
+ const v = result?.verdict;
2808
+ const criteria = v?.criteria ?? [];
2809
+ if (!v || criteria.length === 0) {
2810
+ return "Quality: not evaluated (no rubric for this type).";
2811
+ }
2812
+ const total = criteria.length;
2813
+ const passed = criteria.filter((c) => c.passed).length;
2814
+ return v.passed ? `Quality: all ${total} rubric criteria pass.` : `Quality: ${passed}/${total} rubric criteria pass \u2014 see the coaching above.`;
2815
+ }
3015
2816
  function formatRubricCoaching(result) {
3016
2817
  const { verdict, rogerMartin } = result;
3017
2818
  if (!verdict || verdict.criteria.length === 0) return "";
@@ -5787,8 +5588,10 @@ import { z as z8 } from "zod/v3";
5787
5588
  var COLLECTIONS_ACTIONS = ["list", "create", "update", "describe", "audit", "export"];
5788
5589
  var qualityCriterionSchema = z8.object({
5789
5590
  field: z8.string().describe("Entry data field key this criterion applies to, e.g. 'description', 'owner'"),
5790
- rule: z8.enum(["required", "min_length", "pattern"]).describe("'required': field must be non-empty (blocks accept). 'min_length': minimum string length (warns). 'pattern': regex match (warns)."),
5791
- value: z8.string().optional().describe("For min_length: the minimum length as a string integer. For pattern: the regex string. Unused for 'required'.")
5591
+ // WP-480 S1: `max_length` must mirror the Convex rule union, or the tool rejects a
5592
+ // valid criterion before the request ever reaches the server.
5593
+ rule: z8.enum(["required", "min_length", "max_length", "pattern"]).describe("'required': field must be non-empty (blocks accept). 'min_length': minimum string length (warns). 'max_length': maximum string length (warns). 'pattern': regex match (warns)."),
5594
+ value: z8.string().optional().describe("For min_length/max_length: the length bound as a string integer. For pattern: the regex string. Unused for 'required'.")
5792
5595
  });
5793
5596
  var fieldSchema = z8.object({
5794
5597
  key: z8.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
@@ -7635,20 +7438,24 @@ import { z as z12 } from "zod/v3";
7635
7438
  var QUALITY_ACTIONS = ["check", "re-evaluate"];
7636
7439
  var qualitySchema = z12.object({
7637
7440
  action: z12.enum(QUALITY_ACTIONS).describe(
7638
- "'check': score an entry against quality criteria. 're-evaluate': trigger fresh evaluation."
7441
+ "'check': read the entry's server quality verdict (tier + criteria). 're-evaluate': trigger fresh evaluation."
7639
7442
  ),
7640
7443
  entryId: z12.string().describe("Entry ID, e.g. 'TEN-graph-db', '<PREFIX>-<n>'"),
7641
7444
  context: z12.enum(["capture", "commit", "review"]).default("review").optional().describe("For re-evaluate: evaluation context")
7642
7445
  });
7643
7446
  var qualityCheckOutputSchema = z12.object({
7644
7447
  entryId: z12.string(),
7645
- score: z12.number(),
7646
- maxScore: z12.number(),
7448
+ /** WP-480 S1: false = the ID does not resolve to an entry (typo/deleted) — distinct from "no verdict yet". */
7449
+ entryFound: z12.boolean().optional(),
7450
+ hasVerdict: z12.boolean(),
7451
+ /** WP-480 S1: the verdict was judged against content the entry no longer has — re-evaluate for a current one. */
7452
+ stale: z12.boolean().optional(),
7453
+ tier: z12.string().optional(),
7454
+ passed: z12.boolean().optional(),
7647
7455
  criteria: z12.array(z12.object({
7648
- name: z12.string(),
7649
- score: z12.number(),
7650
- maxScore: z12.number(),
7651
- met: z12.boolean()
7456
+ id: z12.string(),
7457
+ passed: z12.boolean(),
7458
+ hint: z12.string().optional()
7652
7459
  }))
7653
7460
  });
7654
7461
  var qualityReevaluateOutputSchema = z12.object({
@@ -7663,7 +7470,7 @@ function registerQualityTools(server) {
7663
7470
  "quality",
7664
7471
  {
7665
7472
  title: "Quality",
7666
- description: "Assess and re-evaluate entry quality. Two actions:\n\n- **check**: Score an entry against collection-specific quality criteria. Returns a scorecard with actionable suggestions. Includes link suggestions when relations are missing.\n- **re-evaluate**: Trigger a fresh quality evaluation. Returns heuristic verdict immediately, schedules LLM evaluation in background.",
7473
+ description: "Assess and re-evaluate entry quality. Two actions:\n\n- **check**: Read the entry's server quality verdict (tier + criteria) \u2014 it does not re-score. Suggests links when the entry has no relations.\n- **re-evaluate**: Trigger a fresh quality evaluation. Returns heuristic verdict immediately, schedules LLM evaluation in background.",
7667
7474
  inputSchema: qualitySchema,
7668
7475
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
7669
7476
  },
@@ -7684,70 +7491,100 @@ function registerQualityTools(server) {
7684
7491
  );
7685
7492
  trackWriteTool(qualityTool);
7686
7493
  }
7687
- async function handleCheck(entryId) {
7688
- const result = await checkEntryQuality(entryId);
7689
- const needsRelations = result.quality.checks.some(
7690
- (c) => !c.passed && (c.id === "has-relations" || c.id === "diverse-relations")
7691
- );
7692
- if (needsRelations) {
7693
- try {
7694
- const suggestions = await kernelQuery("chain.graphSuggestLinks", {
7695
- entryId,
7696
- maxHops: 2,
7697
- limit: 3
7698
- });
7699
- if ((suggestions?.suggestions?.length ?? 0) > 0) {
7700
- const linkHints = (suggestions.suggestions ?? []).map((s) => ` \u2192 \`relations action=create from="${entryId}" to="${s.entryId}" type="${s.recommendedRelationType}"\` \u2014 ${s.name} [${s.collectionSlug}] (${s.score}/100)`).join("\n");
7701
- result.text += `
7702
-
7494
+ async function appendLinkSuggestions(entryId, lines) {
7495
+ let relationCount = -1;
7496
+ try {
7497
+ const relations = await kernelQuery("chain.listEntryRelations", { entryId });
7498
+ relationCount = relations?.length ?? 0;
7499
+ } catch {
7500
+ return;
7501
+ }
7502
+ if (relationCount !== 0) return;
7503
+ try {
7504
+ const suggestions = await kernelQuery("chain.graphSuggestLinks", {
7505
+ entryId,
7506
+ maxHops: 2,
7507
+ limit: 3
7508
+ });
7509
+ if ((suggestions?.suggestions?.length ?? 0) > 0) {
7510
+ const linkHints = (suggestions.suggestions ?? []).map((s) => ` \u2192 \`relations action=create from="${entryId}" to="${s.entryId}" type="${s.recommendedRelationType}"\` \u2014 ${s.name} [${s.collectionSlug}] (${s.score}/100)`).join("\n");
7511
+ lines.push(`
7703
7512
  ## Suggested Links to Improve Quality
7704
- ${linkHints}`;
7705
- }
7706
- } catch {
7513
+ ${linkHints}`);
7707
7514
  }
7515
+ } catch {
7708
7516
  }
7517
+ }
7518
+ async function handleCheck(entryId) {
7519
+ const res = await kernelQuery("quality.getLatestVerdictForEntry", { entryId });
7520
+ if (!res || !res.entryFound) {
7521
+ return successResult(
7522
+ `# Quality Check: ${entryId}
7523
+
7524
+ Entry \`${entryId}\` not found. Use \`search\` to find the right ID.`,
7525
+ `Entry ${entryId} not found \u2014 search for the correct ID.`,
7526
+ { entryId, entryFound: false, hasVerdict: false, criteria: [] },
7527
+ [{ tool: "search", description: "Find the entry", parameters: { query: entryId } }]
7528
+ );
7529
+ }
7530
+ const verdict = res.verdict;
7531
+ if (!verdict) {
7532
+ const lines2 = [
7533
+ `# Quality Check: ${entryId}`,
7534
+ "",
7535
+ `No quality verdict yet for \`${entryId}\`. Run \`quality action=re-evaluate entryId="${entryId}"\` to evaluate it.`
7536
+ ];
7537
+ await appendLinkSuggestions(entryId, lines2);
7538
+ return successResult(
7539
+ lines2.join("\n"),
7540
+ `No quality verdict yet for ${entryId} \u2014 re-evaluate to produce one.`,
7541
+ { entryId, entryFound: true, hasVerdict: false, criteria: [] },
7542
+ [{ tool: "quality", description: "Evaluate now", parameters: { action: "re-evaluate", entryId } }]
7543
+ );
7544
+ }
7545
+ const criteria = verdict.criteria ?? [];
7546
+ const lines = [`# Quality Check: ${entryId}`, ""];
7547
+ if (res.stale) {
7548
+ lines.push(
7549
+ `> STALE \u2014 this verdict was judged against an earlier version of \`${entryId}\`, which has been edited since. Run \`quality action=re-evaluate entryId="${entryId}"\` for a current verdict.`,
7550
+ ""
7551
+ );
7552
+ }
7553
+ const verdictSection = formatRubricVerdictSection(verdict);
7554
+ lines.push(verdictSection || `Verdict tier \`${verdict.tier ?? "unknown"}\` for \`${entryId}\` \u2014 no rubric criteria to report.`);
7555
+ await appendLinkSuggestions(entryId, lines);
7709
7556
  try {
7710
- const verdict = await kernelQuery("quality.getLatestVerdictForEntry", { entryId });
7711
- if (verdict && verdict.criteria && verdict.criteria.length > 0) {
7712
- result.text += "\n\n" + formatRubricVerdictSection(verdict);
7713
- try {
7714
- const wsForTracking = await getWorkspaceContext();
7715
- trackQualityCheck(wsForTracking.workspaceId, {
7716
- entry_id: entryId,
7717
- entry_type: verdict.canonicalKey ?? "",
7718
- tier: verdict.tier ?? "",
7719
- passed: verdict.passed ?? false,
7720
- source: verdict.source ?? "",
7721
- llm_status: verdict.llmStatus,
7722
- llm_duration_ms: verdict.llmDurationMs,
7723
- llm_error: verdict.llmError,
7724
- has_roger_martin: !!verdict.rogerMartin
7725
- });
7726
- } catch {
7727
- }
7728
- }
7557
+ const wsForTracking = await getWorkspaceContext();
7558
+ trackQualityCheck(wsForTracking.workspaceId, {
7559
+ entry_id: entryId,
7560
+ entry_type: verdict.canonicalKey ?? "",
7561
+ tier: verdict.tier ?? "",
7562
+ passed: verdict.passed ?? false,
7563
+ source: verdict.source ?? "",
7564
+ llm_status: verdict.llmStatus,
7565
+ llm_duration_ms: verdict.llmDurationMs,
7566
+ llm_error: verdict.llmError,
7567
+ has_roger_martin: !!verdict.rogerMartin
7568
+ });
7729
7569
  } catch {
7730
7570
  }
7731
- return {
7732
- content: [{ type: "text", text: result.text }],
7733
- structuredContent: success(
7734
- `Quality check for ${entryId}: ${result.quality.score}/${result.quality.maxScore}.`,
7735
- {
7736
- entryId,
7737
- score: result.quality.score,
7738
- maxScore: result.quality.maxScore,
7739
- criteria: result.quality.checks.map((c) => ({
7740
- name: c.label,
7741
- score: c.passed ? 1 : 0,
7742
- maxScore: 1,
7743
- met: c.passed
7744
- }))
7745
- },
7746
- [
7747
- { tool: "graph", description: "Suggest connections", parameters: { action: "suggest", entryId } }
7748
- ]
7749
- )
7750
- };
7571
+ const passedCount = criteria.filter((c) => c.passed).length;
7572
+ return successResult(
7573
+ lines.join("\n"),
7574
+ `Quality check for ${entryId}: ${verdict.tier ?? "unknown"} tier, ${passedCount}/${criteria.length} criteria pass.` + (res.stale ? " STALE \u2014 the entry was edited after this verdict; re-evaluate for a current one." : ""),
7575
+ {
7576
+ entryId,
7577
+ entryFound: true,
7578
+ hasVerdict: true,
7579
+ stale: res.stale ?? false,
7580
+ tier: verdict.tier,
7581
+ passed: verdict.passed,
7582
+ criteria: criteria.map((c) => ({ id: c.id, passed: c.passed, hint: c.hint }))
7583
+ },
7584
+ [
7585
+ { tool: "graph", description: "Suggest connections", parameters: { action: "suggest", entryId } }
7586
+ ]
7587
+ );
7751
7588
  }
7752
7589
  async function handleReEvaluate(entryId, context) {
7753
7590
  requireWriteAccess();
@@ -14813,4 +14650,4 @@ export {
14813
14650
  createProductBrainServer,
14814
14651
  initFeatureFlags
14815
14652
  };
14816
- //# sourceMappingURL=chunk-QTY3D7P4.js.map
14653
+ //# sourceMappingURL=chunk-5PUUKR7Q.js.map