@productbrain/mcp 0.0.1-beta.2110 → 0.0.1-beta.2122

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 "";
@@ -3630,6 +3431,20 @@ function renderWhyLine(e) {
3630
3431
  }
3631
3432
  return { line: null, why, whySourceKey: null };
3632
3433
  }
3434
+ function renderVerificationLine(e) {
3435
+ if (!e.verificationStatus) return null;
3436
+ const verifiedBy = typeof e.verifiedBy === "string" ? e.verifiedBy : void 0;
3437
+ const attestation = e.attestation && typeof e.attestation === "object" ? e.attestation : void 0;
3438
+ const strength = attestation && typeof attestation.strength === "string" ? attestation.strength : void 0;
3439
+ const basis = attestation && typeof attestation.basis === "string" ? attestation.basis : void 0;
3440
+ let who;
3441
+ if (basis && basis !== "attested") {
3442
+ who = verifiedBy ? ` (${verifiedBy})` : "";
3443
+ } else {
3444
+ who = verifiedBy ? ` \u2014 by ${verifiedBy}${strength ? ` (${strength})` : ""}` : "";
3445
+ }
3446
+ return `**Verification:** ${String(e.verificationStatus)}${who}`;
3447
+ }
3633
3448
  var ENTRIES_ACTIONS = ["list", "get", "batch", "search"];
3634
3449
  var entriesSchema = z4.object({
3635
3450
  action: z4.enum(ENTRIES_ACTIONS).describe(
@@ -3656,6 +3471,14 @@ var entriesGetOutputSchema = z4.object({
3656
3471
  origin: z4.string().optional(),
3657
3472
  originDetail: z4.string().optional(),
3658
3473
  verificationStatus: z4.string().optional(),
3474
+ // Attestation-model finding (PR #341 review): the server's honest verifier label and
3475
+ // derived attestation strength/basis — connectors NEVER re-derive strength (spec §5),
3476
+ // they only render what chain.getEntry ships. Mirrors packages/cli EntryFromApi.
3477
+ verifiedBy: z4.string().optional(),
3478
+ attestation: z4.object({
3479
+ strength: z4.enum(["human-direct", "delegated", "system", "unattested"]),
3480
+ basis: z4.string().optional()
3481
+ }).optional(),
3659
3482
  sourceRef: z4.string().optional(),
3660
3483
  sourceExcerpt: z4.string().optional(),
3661
3484
  why: z4.string().optional(),
@@ -3700,6 +3523,13 @@ var entriesBatchOutputSchema = z4.object({
3700
3523
  origin: z4.string().optional(),
3701
3524
  originDetail: z4.string().optional(),
3702
3525
  verificationStatus: z4.string().optional(),
3526
+ // Attestation-model finding (PR #341 review): mirror entriesGetOutputSchema — batch
3527
+ // entries now carry the same honest verifiedBy/attestation fields.
3528
+ verifiedBy: z4.string().optional(),
3529
+ attestation: z4.object({
3530
+ strength: z4.enum(["human-direct", "delegated", "system", "unattested"]),
3531
+ basis: z4.string().optional()
3532
+ }).optional(),
3703
3533
  sourceRef: z4.string().optional(),
3704
3534
  sourceExcerpt: z4.string().optional(),
3705
3535
  // TEN-2191: mirror entriesGetOutputSchema — batch entries now carry why/whyQuality.
@@ -3786,7 +3616,8 @@ async function handleGet(entryId) {
3786
3616
  const detail = e.originDetail ? ` (${e.originDetail})` : "";
3787
3617
  lines.push(`**Origin:** ${e.origin}${detail}`);
3788
3618
  }
3789
- if (e.verificationStatus) lines.push(`**Verification:** ${e.verificationStatus}`);
3619
+ const verificationLine = renderVerificationLine(e);
3620
+ if (verificationLine) lines.push(verificationLine);
3790
3621
  if (e.sourceRef) lines.push(`**Source ref:** ${e.sourceRef}`);
3791
3622
  if (e.sourceExcerpt) lines.push(`**Source excerpt:** ${e.sourceExcerpt}`);
3792
3623
  if (epistemic) {
@@ -3837,6 +3668,11 @@ async function handleGet(entryId) {
3837
3668
  ...e.origin ? { origin: String(e.origin) } : {},
3838
3669
  ...e.originDetail ? { originDetail: String(e.originDetail) } : {},
3839
3670
  ...e.verificationStatus ? { verificationStatus: String(e.verificationStatus) } : {},
3671
+ // Attestation-model finding (PR #341 review): forward the server's honest verifier
3672
+ // label + derived attestation strength/basis on the structured payload too — the
3673
+ // agent surface must see the same fields the text render displays (spec §5).
3674
+ ...typeof e.verifiedBy === "string" ? { verifiedBy: e.verifiedBy } : {},
3675
+ ...e.attestation && typeof e.attestation === "object" ? { attestation: e.attestation } : {},
3840
3676
  ...e.sourceRef ? { sourceRef: String(e.sourceRef) } : {},
3841
3677
  ...e.sourceExcerpt ? { sourceExcerpt: String(e.sourceExcerpt) } : {},
3842
3678
  // TEN-2191: surface the WHY and its quality in the structured payload too.
@@ -3897,9 +3733,8 @@ async function handleBatch(entryIds) {
3897
3733
  const detail = entry.originDetail ? ` (${entry.originDetail})` : "";
3898
3734
  lines.push(`**Origin:** ${entry.origin}${detail}`);
3899
3735
  }
3900
- if (entry.verificationStatus) {
3901
- lines.push(`**Verification:** ${entry.verificationStatus}`);
3902
- }
3736
+ const entryVerificationLine = renderVerificationLine(entry);
3737
+ if (entryVerificationLine) lines.push(entryVerificationLine);
3903
3738
  if (entry.sourceRef) {
3904
3739
  lines.push(`**Source ref:** ${entry.sourceRef}`);
3905
3740
  }
@@ -3955,6 +3790,10 @@ async function handleBatch(entryIds) {
3955
3790
  ...entry.origin ? { origin: String(entry.origin) } : {},
3956
3791
  ...entry.originDetail ? { originDetail: String(entry.originDetail) } : {},
3957
3792
  ...entry.verificationStatus ? { verificationStatus: String(entry.verificationStatus) } : {},
3793
+ // Attestation-model finding (PR #341 review): mirror handleGet — forward the
3794
+ // honest verifier label + derived attestation on batch's structured payload too.
3795
+ ...typeof entry.verifiedBy === "string" ? { verifiedBy: entry.verifiedBy } : {},
3796
+ ...entry.attestation && typeof entry.attestation === "object" ? { attestation: entry.attestation } : {},
3958
3797
  ...entry.sourceRef ? { sourceRef: String(entry.sourceRef) } : {},
3959
3798
  ...entry.sourceExcerpt ? { sourceExcerpt: String(entry.sourceExcerpt) } : {},
3960
3799
  ...entry.workflowStatus ? { workflowStatus: String(entry.workflowStatus) } : {},
@@ -5787,8 +5626,10 @@ import { z as z8 } from "zod/v3";
5787
5626
  var COLLECTIONS_ACTIONS = ["list", "create", "update", "describe", "audit", "export"];
5788
5627
  var qualityCriterionSchema = z8.object({
5789
5628
  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'.")
5629
+ // WP-480 S1: `max_length` must mirror the Convex rule union, or the tool rejects a
5630
+ // valid criterion before the request ever reaches the server.
5631
+ 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)."),
5632
+ 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
5633
  });
5793
5634
  var fieldSchema = z8.object({
5794
5635
  key: z8.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
@@ -7635,20 +7476,24 @@ import { z as z12 } from "zod/v3";
7635
7476
  var QUALITY_ACTIONS = ["check", "re-evaluate"];
7636
7477
  var qualitySchema = z12.object({
7637
7478
  action: z12.enum(QUALITY_ACTIONS).describe(
7638
- "'check': score an entry against quality criteria. 're-evaluate': trigger fresh evaluation."
7479
+ "'check': read the entry's server quality verdict (tier + criteria). 're-evaluate': trigger fresh evaluation."
7639
7480
  ),
7640
7481
  entryId: z12.string().describe("Entry ID, e.g. 'TEN-graph-db', '<PREFIX>-<n>'"),
7641
7482
  context: z12.enum(["capture", "commit", "review"]).default("review").optional().describe("For re-evaluate: evaluation context")
7642
7483
  });
7643
7484
  var qualityCheckOutputSchema = z12.object({
7644
7485
  entryId: z12.string(),
7645
- score: z12.number(),
7646
- maxScore: z12.number(),
7486
+ /** WP-480 S1: false = the ID does not resolve to an entry (typo/deleted) — distinct from "no verdict yet". */
7487
+ entryFound: z12.boolean().optional(),
7488
+ hasVerdict: z12.boolean(),
7489
+ /** WP-480 S1: the verdict was judged against content the entry no longer has — re-evaluate for a current one. */
7490
+ stale: z12.boolean().optional(),
7491
+ tier: z12.string().optional(),
7492
+ passed: z12.boolean().optional(),
7647
7493
  criteria: z12.array(z12.object({
7648
- name: z12.string(),
7649
- score: z12.number(),
7650
- maxScore: z12.number(),
7651
- met: z12.boolean()
7494
+ id: z12.string(),
7495
+ passed: z12.boolean(),
7496
+ hint: z12.string().optional()
7652
7497
  }))
7653
7498
  });
7654
7499
  var qualityReevaluateOutputSchema = z12.object({
@@ -7663,7 +7508,7 @@ function registerQualityTools(server) {
7663
7508
  "quality",
7664
7509
  {
7665
7510
  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.",
7511
+ 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
7512
  inputSchema: qualitySchema,
7668
7513
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
7669
7514
  },
@@ -7684,70 +7529,100 @@ function registerQualityTools(server) {
7684
7529
  );
7685
7530
  trackWriteTool(qualityTool);
7686
7531
  }
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
-
7532
+ async function appendLinkSuggestions(entryId, lines) {
7533
+ let relationCount = -1;
7534
+ try {
7535
+ const relations = await kernelQuery("chain.listEntryRelations", { entryId });
7536
+ relationCount = relations?.length ?? 0;
7537
+ } catch {
7538
+ return;
7539
+ }
7540
+ if (relationCount !== 0) return;
7541
+ try {
7542
+ const suggestions = await kernelQuery("chain.graphSuggestLinks", {
7543
+ entryId,
7544
+ maxHops: 2,
7545
+ limit: 3
7546
+ });
7547
+ if ((suggestions?.suggestions?.length ?? 0) > 0) {
7548
+ 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");
7549
+ lines.push(`
7703
7550
  ## Suggested Links to Improve Quality
7704
- ${linkHints}`;
7705
- }
7706
- } catch {
7551
+ ${linkHints}`);
7707
7552
  }
7553
+ } catch {
7708
7554
  }
7555
+ }
7556
+ async function handleCheck(entryId) {
7557
+ const res = await kernelQuery("quality.getLatestVerdictForEntry", { entryId });
7558
+ if (!res || !res.entryFound) {
7559
+ return successResult(
7560
+ `# Quality Check: ${entryId}
7561
+
7562
+ Entry \`${entryId}\` not found. Use \`search\` to find the right ID.`,
7563
+ `Entry ${entryId} not found \u2014 search for the correct ID.`,
7564
+ { entryId, entryFound: false, hasVerdict: false, criteria: [] },
7565
+ [{ tool: "search", description: "Find the entry", parameters: { query: entryId } }]
7566
+ );
7567
+ }
7568
+ const verdict = res.verdict;
7569
+ if (!verdict) {
7570
+ const lines2 = [
7571
+ `# Quality Check: ${entryId}`,
7572
+ "",
7573
+ `No quality verdict yet for \`${entryId}\`. Run \`quality action=re-evaluate entryId="${entryId}"\` to evaluate it.`
7574
+ ];
7575
+ await appendLinkSuggestions(entryId, lines2);
7576
+ return successResult(
7577
+ lines2.join("\n"),
7578
+ `No quality verdict yet for ${entryId} \u2014 re-evaluate to produce one.`,
7579
+ { entryId, entryFound: true, hasVerdict: false, criteria: [] },
7580
+ [{ tool: "quality", description: "Evaluate now", parameters: { action: "re-evaluate", entryId } }]
7581
+ );
7582
+ }
7583
+ const criteria = verdict.criteria ?? [];
7584
+ const lines = [`# Quality Check: ${entryId}`, ""];
7585
+ if (res.stale) {
7586
+ lines.push(
7587
+ `> 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.`,
7588
+ ""
7589
+ );
7590
+ }
7591
+ const verdictSection = formatRubricVerdictSection(verdict);
7592
+ lines.push(verdictSection || `Verdict tier \`${verdict.tier ?? "unknown"}\` for \`${entryId}\` \u2014 no rubric criteria to report.`);
7593
+ await appendLinkSuggestions(entryId, lines);
7709
7594
  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
- }
7595
+ const wsForTracking = await getWorkspaceContext();
7596
+ trackQualityCheck(wsForTracking.workspaceId, {
7597
+ entry_id: entryId,
7598
+ entry_type: verdict.canonicalKey ?? "",
7599
+ tier: verdict.tier ?? "",
7600
+ passed: verdict.passed ?? false,
7601
+ source: verdict.source ?? "",
7602
+ llm_status: verdict.llmStatus,
7603
+ llm_duration_ms: verdict.llmDurationMs,
7604
+ llm_error: verdict.llmError,
7605
+ has_roger_martin: !!verdict.rogerMartin
7606
+ });
7729
7607
  } catch {
7730
7608
  }
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
- };
7609
+ const passedCount = criteria.filter((c) => c.passed).length;
7610
+ return successResult(
7611
+ lines.join("\n"),
7612
+ `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." : ""),
7613
+ {
7614
+ entryId,
7615
+ entryFound: true,
7616
+ hasVerdict: true,
7617
+ stale: res.stale ?? false,
7618
+ tier: verdict.tier,
7619
+ passed: verdict.passed,
7620
+ criteria: criteria.map((c) => ({ id: c.id, passed: c.passed, hint: c.hint }))
7621
+ },
7622
+ [
7623
+ { tool: "graph", description: "Suggest connections", parameters: { action: "suggest", entryId } }
7624
+ ]
7625
+ );
7751
7626
  }
7752
7627
  async function handleReEvaluate(entryId, context) {
7753
7628
  requireWriteAccess();
@@ -10088,8 +9963,19 @@ function registerVerifyTools(server) {
10088
9963
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
10089
9964
  },
10090
9965
  thinWrapper(async ({ entryId }) => {
10091
- const result = await kernelMutation("chain.verifyEntry", { entryId });
10092
- return successResult(`Entry ${entryId} verified.`, `Entry ${entryId} verified.`, result);
9966
+ const agentSessionId = getAgentSessionId();
9967
+ const result = await kernelMutation(
9968
+ "chain.verifyEntry",
9969
+ {
9970
+ entryId,
9971
+ ...agentSessionId ? { sessionId: agentSessionId } : {}
9972
+ }
9973
+ );
9974
+ const basis = result.attestation?.basis;
9975
+ const strength = result.attestation?.strength;
9976
+ const verifiedBySuffix = result.verifiedBy ? basis && basis !== "attested" ? ` (${result.verifiedBy})` : ` \u2014 by ${result.verifiedBy}${strength ? ` (${strength})` : ""}` : "";
9977
+ const message = result.alreadyVerified ? `Entry ${entryId} is already verified${verifiedBySuffix}.` : `Entry ${entryId} verified${verifiedBySuffix}.`;
9978
+ return successResult(message, message, result);
10093
9979
  })
10094
9980
  );
10095
9981
  trackWriteTool(verifyEntryTool);
@@ -14813,4 +14699,4 @@ export {
14813
14699
  createProductBrainServer,
14814
14700
  initFeatureFlags
14815
14701
  };
14816
- //# sourceMappingURL=chunk-QTY3D7P4.js.map
14702
+ //# sourceMappingURL=chunk-3CONR5XB.js.map