@fenglimg/fabric-server 2.0.0-rc.25 → 2.0.0-rc.26

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.
@@ -1473,10 +1473,9 @@ async function reconcileKnowledge(projectRoot, opts) {
1473
1473
  // src/services/serve-lock.ts
1474
1474
  import fs from "fs";
1475
1475
  import path from "path";
1476
- import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
1476
+ import { createTranslator, resolveFabricLocale } from "@fenglimg/fabric-shared";
1477
1477
  import { IOFabricError as IOFabricError2 } from "@fenglimg/fabric-shared/errors";
1478
1478
  var LOCK_FILENAME = ".serve.lock";
1479
- var t = createTranslator(detectNodeLocale());
1480
1479
  var ServeLockHeldError = class extends IOFabricError2 {
1481
1480
  code = "SERVE_LOCK_HELD";
1482
1481
  httpStatus = 423;
@@ -1504,6 +1503,7 @@ function acquireLock(projectRoot, opts) {
1504
1503
  } catch {
1505
1504
  }
1506
1505
  if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
1506
+ const t = createTranslator(resolveFabricLocale(projectRoot));
1507
1507
  throw new ServeLockHeldError(
1508
1508
  `serve lock held by live PID ${state.pid}`,
1509
1509
  {
@@ -1554,6 +1554,7 @@ function checkLockOrThrow(projectRoot, opts) {
1554
1554
  return;
1555
1555
  }
1556
1556
  if (opts?.force) return;
1557
+ const t = createTranslator(resolveFabricLocale(projectRoot));
1557
1558
  throw new ServeLockHeldError(
1558
1559
  `serve lock held by live PID ${state.pid}`,
1559
1560
  {
@@ -1574,6 +1575,7 @@ import { minimatch } from "minimatch";
1574
1575
  import {
1575
1576
  agentsMetaSchema as agentsMetaSchema4,
1576
1577
  AgentsMetaCountersSchema,
1578
+ createTranslator as createTranslator2,
1577
1579
  forensicReportSchema,
1578
1580
  parseKnowledgeId as parseKnowledgeId2,
1579
1581
  knowledgeTestIndexSchema as knowledgeTestIndexSchema2,
@@ -1583,7 +1585,8 @@ import {
1583
1585
  BOOTSTRAP_MARKER_END,
1584
1586
  BOOTSTRAP_REGEX,
1585
1587
  ONBOARD_SLOT_NAMES,
1586
- ONBOARD_SLOT_TOTAL
1588
+ ONBOARD_SLOT_TOTAL,
1589
+ resolveFabricLocale as resolveFabricLocale2
1587
1590
  } from "@fenglimg/fabric-shared";
1588
1591
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1589
1592
  import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -1669,6 +1672,7 @@ var TARGET_FILE_PATHS = [
1669
1672
  ];
1670
1673
  async function runDoctorReport(target) {
1671
1674
  const projectRoot = normalizeTarget(target);
1675
+ const t = createTranslator2(resolveFabricLocale2(projectRoot));
1672
1676
  const framework = detectFramework(projectRoot);
1673
1677
  const entryPoints = collectEntryPoints(projectRoot);
1674
1678
  const [
@@ -1722,91 +1726,91 @@ async function runDoctorReport(target) {
1722
1726
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1723
1727
  const onboardCoverage = inspectOnboardCoverage(projectRoot);
1724
1728
  const checks = [
1725
- createBootstrapAnchorCheck(bootstrapAnchor),
1729
+ createBootstrapAnchorCheck(t, bootstrapAnchor),
1726
1730
  // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
1727
1731
  // the anchor check — both are bootstrap-file invariants. fixable_error
1728
1732
  // when any of the four target paths still carries the legacy marker.
1729
- createBootstrapMarkerMigrationCheck(bootstrapMarkerMigration),
1733
+ createBootstrapMarkerMigrationCheck(t, bootstrapMarkerMigration),
1730
1734
  // v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection sit immediately
1731
1735
  // after the marker migration check. Order: anchor existence → migration →
1732
1736
  // L1 (canonical ↔ snapshot) → L2 (snapshot+rules ↔ three-end blocks).
1733
- createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
1734
- createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
1735
- createKnowledgeDirMissingCheck(knowledgeDirMissing),
1737
+ createL1BootstrapSnapshotDriftCheck(t, l1BootstrapSnapshotDrift),
1738
+ createL2ManagedBlockDriftCheck(t, l2ManagedBlockDrift),
1739
+ createKnowledgeDirMissingCheck(t, knowledgeDirMissing),
1736
1740
  // v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
1737
1741
  // knowledge_dir_missing — both are knowledge-layout invariants. manual_error
1738
1742
  // kind; resolution is manual file deletion (rc.23 TASK-012 (F8a) removed
1739
1743
  // the baseline-emit pipeline, so no auto-fix exists).
1740
- createBaselineFilenameFormatCheck(baselineFilenameFormat),
1741
- createForensicCheck(forensic, framework.kind, entryPoints.length),
1744
+ createBaselineFilenameFormatCheck(t, baselineFilenameFormat),
1745
+ createForensicCheck(t, forensic, framework.kind, entryPoints.length),
1742
1746
  // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
1743
1747
  // is owned by the AI-side client init skill, not by `fabric install` CLI.
1744
1748
  // The file's absence is a legitimate post-init state when the skill has
1745
1749
  // not yet run, so flagging it as a doctor manual_error misrepresents
1746
1750
  // ownership.
1747
- createMetaCheck(meta),
1748
- createRuleContentRefCheck(meta),
1751
+ createMetaCheck(t, meta),
1752
+ createRuleContentRefCheck(t, meta),
1749
1753
  // v2.0 / rc.2: `createRuleSectionsCheck` removed — it parsed v1.x
1750
1754
  // [MANDATORY_INJECTION] sections out of legacy rule files, a structural
1751
1755
  // concept that has no v2 equivalent. rc.4 will introduce a dedicated v2
1752
1756
  // lint suite for the new knowledge frontmatter contract.
1753
- createKnowledgeTestIndexCheck(knowledgeTestIndex),
1754
- createEventLedgerCheck(eventLedger),
1755
- createEventLedgerPartialWriteCheck(eventLedger),
1756
- createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1757
- createMetaManuallyDivergedCheck(metaManuallyDiverged),
1758
- createKnowledgeDirUnindexedCheck(knowledgeDirUnindexed),
1759
- createStableIdCollisionCheck(stableIdCollision),
1760
- createCounterDesyncCheck(counterDesync),
1761
- createFilesystemEditFallbackCheck(filesystemEditFallback),
1757
+ createKnowledgeTestIndexCheck(t, knowledgeTestIndex),
1758
+ createEventLedgerCheck(t, eventLedger),
1759
+ createEventLedgerPartialWriteCheck(t, eventLedger),
1760
+ createMcpConfigInWrongFileCheck(t, mcpConfigInWrongFile),
1761
+ createMetaManuallyDivergedCheck(t, metaManuallyDiverged),
1762
+ createKnowledgeDirUnindexedCheck(t, knowledgeDirUnindexed),
1763
+ createStableIdCollisionCheck(t, stableIdCollision),
1764
+ createCounterDesyncCheck(t, counterDesync),
1765
+ createFilesystemEditFallbackCheck(t, filesystemEditFallback),
1762
1766
  // rc.4 TASK-001: read-side lint checks #16-18. Findings only — mutation
1763
1767
  // + event emission lands in TASK-003 behind --apply-lint.
1764
- createOrphanDemoteCheck(orphanDemote),
1765
- createStaleArchiveCheck(staleArchive),
1766
- createPendingOverdueCheck(pendingOverdue),
1768
+ createOrphanDemoteCheck(t, orphanDemote),
1769
+ createStaleArchiveCheck(t, staleArchive),
1770
+ createPendingOverdueCheck(t, pendingOverdue),
1767
1771
  // rc.4 TASK-002: read-side integrity checks #19-21. Stable_id duplicate
1768
1772
  // runs first in this trio — it is the most critical integrity break and
1769
1773
  // surfaces ahead of layer-mismatch / index-drift in the report so a
1770
1774
  // human operator triages the collision before reasoning about counter
1771
1775
  // state. Index drift is the only fixable_error of the three; stable_id
1772
1776
  // duplicate and layer mismatch require manual triage (rename / move).
1773
- createStableIdDuplicateCheck(stableIdDuplicate),
1774
- createLayerMismatchCheck(layerMismatch),
1775
- createIndexDriftCheck(indexDrift),
1777
+ createStableIdDuplicateCheck(t, stableIdDuplicate),
1778
+ createLayerMismatchCheck(t, layerMismatch),
1779
+ createIndexDriftCheck(t, indexDrift),
1776
1780
  // rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
1777
1781
  // does not bump report status. Recommends running the fabric-import skill
1778
1782
  // to backfill knowledge when the corpus is below the threshold floor.
1779
- createUnderseededCheck(underseeded),
1783
+ createUnderseededCheck(t, underseeded),
1780
1784
  // rc.5 TASK-013 (C4): relevance_paths hygiene checks #23/#24/#25.
1781
1785
  // All three are flag-only in rc.5 (no apply-lint mutations).
1782
1786
  // #23 narrow_no_paths — warning kind (silent recall risk)
1783
1787
  // #24 relevance_paths_dangling — warning kind (glob → zero matches)
1784
1788
  // #25 relevance_paths_drift — info kind (git-log heuristic; noisy)
1785
- createNarrowNoPathsCheck(narrowNoPaths),
1786
- createRelevancePathsDanglingCheck(relevancePathsDangling),
1787
- createRelevancePathsDriftCheck(relevancePathsDrift),
1789
+ createNarrowNoPathsCheck(t, narrowNoPaths),
1790
+ createRelevancePathsDanglingCheck(t, relevancePathsDangling),
1791
+ createRelevancePathsDriftCheck(t, relevancePathsDrift),
1788
1792
  // rc.6 TASK-023 (E6): narrow_too_few (lint #26). Info kind; both arms
1789
1793
  // (structural + telemetry) recommend the same fabric-import action.
1790
- createNarrowTooFewCheck(narrowTooFew),
1794
+ createNarrowTooFewCheck(t, narrowTooFew),
1791
1795
  // rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
1792
- createSessionHintsStaleCheck(sessionHintsStale),
1796
+ createSessionHintsStaleCheck(t, sessionHintsStale),
1793
1797
  // rc.23 TASK-010 (e): stale .fabric/.serve.lock advisory. Info kind —
1794
1798
  // does not bump report status. `--fix` unlinks the corpse and emits
1795
1799
  // `serve_lock_cleared`.
1796
- createStaleServeLockCheck(staleServeLock),
1800
+ createStaleServeLockCheck(t, staleServeLock),
1797
1801
  // v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
1798
1802
  // Info kind — applies to pending entries only; canonical entries get
1799
1803
  // the fields written verbatim by fab_review.approve/modify.
1800
- createRelevanceFieldsMissingCheck(relevanceFieldsMissing),
1804
+ createRelevanceFieldsMissingCheck(t, relevanceFieldsMissing),
1801
1805
  // rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
1802
1806
  // SKILL.md frontmatter that Codex CLI silently drops at load.
1803
- createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
1807
+ createSkillMdYamlInvalidCheck(t, skillMdYamlInvalid),
1804
1808
  // v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
1805
1809
  // Surfaces uncovered S5 onboard slots and recommends /fabric-archive
1806
1810
  // first-run phase. Sits adjacent to Skill markdown YAML — both are
1807
1811
  // Skill-adjacent advisories. --fix never mutates onboard state.
1808
- createOnboardCoverageCheck(onboardCoverage),
1809
- createPreexistingRootFilesCheck(preexistingRootFiles)
1812
+ createOnboardCoverageCheck(t, onboardCoverage),
1813
+ createPreexistingRootFilesCheck(t, preexistingRootFiles)
1810
1814
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1811
1815
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
1812
1816
  // parse time, so the soft-deprecation warn-and-fix path no longer has a
@@ -2568,21 +2572,25 @@ async function inspectBootstrapMarkerMigration(target) {
2568
2572
  }
2569
2573
  return { filesNeedingMigration };
2570
2574
  }
2571
- function createBootstrapMarkerMigrationCheck(inspection) {
2575
+ function createBootstrapMarkerMigrationCheck(t, inspection) {
2572
2576
  if (inspection.filesNeedingMigration.length === 0) {
2573
2577
  return okCheck(
2574
- "Bootstrap marker migration",
2575
- "No legacy fabric:knowledge-base markers detected in bootstrap target files."
2578
+ t("doctor.check.bootstrap_marker_migration.name"),
2579
+ t("doctor.check.bootstrap_marker_migration.ok")
2576
2580
  );
2577
2581
  }
2578
2582
  const list = inspection.filesNeedingMigration.join(", ");
2583
+ const count = inspection.filesNeedingMigration.length;
2579
2584
  return issueCheck(
2580
- "Bootstrap marker migration",
2585
+ t("doctor.check.bootstrap_marker_migration.name"),
2581
2586
  "error",
2582
2587
  "fixable_error",
2583
2588
  "bootstrap_marker_migration_required",
2584
- `${inspection.filesNeedingMigration.length} file${inspection.filesNeedingMigration.length === 1 ? "" : "s"} still carry the legacy fabric:knowledge-base bootstrap marker: ${list}.`,
2585
- "Run `fab doctor --fix` to migrate to fabric:bootstrap marker"
2589
+ t(`doctor.check.bootstrap_marker_migration.message.${count === 1 ? "singular" : "plural"}`, {
2590
+ count: String(count),
2591
+ list
2592
+ }),
2593
+ t("doctor.check.bootstrap_marker_migration.remediation")
2586
2594
  );
2587
2595
  }
2588
2596
  async function inspectL1BootstrapSnapshotDrift(target) {
@@ -2601,20 +2609,20 @@ async function inspectL1BootstrapSnapshotDrift(target) {
2601
2609
  }
2602
2610
  return { status: "drift", canonical: BOOTSTRAP_CANONICAL, onDisk };
2603
2611
  }
2604
- function createL1BootstrapSnapshotDriftCheck(inspection) {
2612
+ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
2605
2613
  if (inspection.status === "drift") {
2606
2614
  return issueCheck(
2607
- "Bootstrap snapshot drift",
2615
+ t("doctor.check.bootstrap_snapshot_drift.name"),
2608
2616
  "error",
2609
2617
  "fixable_error",
2610
2618
  "bootstrap_snapshot_drift",
2611
- ".fabric/AGENTS.md content diverges byte-for-byte from BOOTSTRAP_CANONICAL.",
2612
- "Run `fab doctor --fix` to restore canonical bootstrap snapshot"
2619
+ t("doctor.check.bootstrap_snapshot_drift.message.drift"),
2620
+ t("doctor.check.bootstrap_snapshot_drift.remediation.drift")
2613
2621
  );
2614
2622
  }
2615
2623
  return okCheck(
2616
- "Bootstrap snapshot drift",
2617
- inspection.status === "ok" ? ".fabric/AGENTS.md byte-equals BOOTSTRAP_CANONICAL." : ".fabric/AGENTS.md absent \u2014 delegated to bootstrap_anchor_missing."
2624
+ t("doctor.check.bootstrap_snapshot_drift.name"),
2625
+ inspection.status === "ok" ? t("doctor.check.bootstrap_snapshot_drift.ok.ok") : t("doctor.check.bootstrap_snapshot_drift.ok.missing_delegated")
2618
2626
  );
2619
2627
  }
2620
2628
  async function inspectL2ManagedBlockDrift(target) {
@@ -2706,39 +2714,46 @@ ${projectRules}`;
2706
2714
  }
2707
2715
  return { status: "drift", drifted };
2708
2716
  }
2709
- function createL2ManagedBlockDriftCheck(inspection) {
2717
+ function createL2ManagedBlockDriftCheck(t, inspection) {
2710
2718
  if (inspection.status === "drift") {
2711
2719
  const list = inspection.drifted.map((d) => d.path).join(", ");
2720
+ const count = inspection.drifted.length;
2712
2721
  return issueCheck(
2713
- "Managed block drift",
2722
+ t("doctor.check.managed_block_drift.name"),
2714
2723
  "error",
2715
2724
  "fixable_error",
2716
2725
  "managed_block_drift",
2717
- `${inspection.drifted.length} three-end managed block${inspection.drifted.length === 1 ? "" : "s"} diverge from expected body (snapshot + optional project-rules concat): ${list}.`,
2718
- "Run `fab doctor --fix` to restore three-end managed blocks from canonical"
2726
+ t(`doctor.check.managed_block_drift.message.${count === 1 ? "singular" : "plural"}`, {
2727
+ count: String(count),
2728
+ list
2729
+ }),
2730
+ t("doctor.check.managed_block_drift.remediation")
2719
2731
  );
2720
2732
  }
2721
2733
  return okCheck(
2722
- "Managed block drift",
2723
- inspection.status === "ok" ? "Three-end managed blocks byte-equal expectedBody." : "No three-end managed blocks detected \u2014 propagation pending or legacy-marker state."
2734
+ t("doctor.check.managed_block_drift.name"),
2735
+ inspection.status === "ok" ? t("doctor.check.managed_block_drift.ok.ok") : t("doctor.check.managed_block_drift.ok.no_managed_block")
2724
2736
  );
2725
2737
  }
2726
- function createBootstrapAnchorCheck(inspection) {
2738
+ function createBootstrapAnchorCheck(t, inspection) {
2727
2739
  if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
2728
2740
  return issueCheck(
2729
- "Bootstrap anchor",
2741
+ t("doctor.check.bootstrap_anchor.name"),
2730
2742
  "error",
2731
2743
  "fixable_error",
2732
2744
  "bootstrap_anchor_missing",
2733
- "Neither AGENTS.md nor CLAUDE.md exists at the repo root. Fabric requires a bootstrap anchor file at the project root.",
2734
- "Run `fabric install` to generate the AGENTS.md / CLAUDE.md bootstrap anchor at the repo root."
2745
+ t("doctor.check.bootstrap_anchor.message.missing"),
2746
+ t("doctor.check.bootstrap_anchor.remediation.missing")
2735
2747
  );
2736
2748
  }
2737
2749
  const present = [
2738
2750
  inspection.hasAgentsMd ? "AGENTS.md" : null,
2739
2751
  inspection.hasClaudeMd ? "CLAUDE.md" : null
2740
2752
  ].filter((entry) => entry !== null).join(", ");
2741
- return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
2753
+ return okCheck(
2754
+ t("doctor.check.bootstrap_anchor.name"),
2755
+ t("doctor.check.bootstrap_anchor.ok", { present })
2756
+ );
2742
2757
  }
2743
2758
  function inspectKnowledgeDirMissing(projectRoot) {
2744
2759
  const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
@@ -2799,154 +2814,269 @@ function inspectBaselineFilenameFormat(projectRoot) {
2799
2814
  offenders.sort((a, b) => a.path.localeCompare(b.path));
2800
2815
  return { offenders };
2801
2816
  }
2802
- function createBaselineFilenameFormatCheck(inspection) {
2817
+ function createBaselineFilenameFormatCheck(t, inspection) {
2803
2818
  if (inspection.offenders.length === 0) {
2804
2819
  return okCheck(
2805
- "Baseline filename format",
2806
- "All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
2820
+ t("doctor.check.baseline_filename_format.name"),
2821
+ t("doctor.check.baseline_filename_format.ok")
2807
2822
  );
2808
2823
  }
2809
2824
  const first = inspection.offenders[0];
2810
2825
  const detail = `${first.stable_id} at ${first.path}`;
2826
+ const count = inspection.offenders.length;
2811
2827
  return issueCheck(
2812
- "Baseline filename format",
2828
+ t("doctor.check.baseline_filename_format.name"),
2813
2829
  "error",
2814
2830
  "manual_error",
2815
2831
  "lint-baseline-filename-format",
2816
- `${inspection.offenders.length} baseline knowledge file${inspection.offenders.length === 1 ? "" : "s"} use${inspection.offenders.length === 1 ? "s" : ""} the deprecated bare-slug filename format and must be migrated to \`\${id}--\${slug}.md\`. First: ${detail}.`,
2817
- "Delete the legacy bare-slug baseline file(s) manually \u2014 the baseline pipeline was removed in rc.23 and is no longer an auto-fix path."
2832
+ t(`doctor.check.baseline_filename_format.message.${count === 1 ? "singular" : "plural"}`, {
2833
+ count: String(count),
2834
+ detail
2835
+ }),
2836
+ t("doctor.check.baseline_filename_format.remediation")
2818
2837
  );
2819
2838
  }
2820
- function createKnowledgeDirMissingCheck(inspection) {
2839
+ function createKnowledgeDirMissingCheck(t, inspection) {
2821
2840
  if (inspection.missingSubdirs.length > 0) {
2822
2841
  const list = inspection.missingSubdirs.join(", ");
2842
+ const count = inspection.missingSubdirs.length;
2823
2843
  return issueCheck(
2824
- "Knowledge layout",
2844
+ t("doctor.check.knowledge_dir_missing.name"),
2825
2845
  "error",
2826
2846
  "fixable_error",
2827
2847
  "knowledge_dir_missing",
2828
- `${inspection.missingSubdirs.length} required knowledge subdir${inspection.missingSubdirs.length === 1 ? " is" : "s are"} missing: ${list}.`,
2829
- "Run `fab doctor --fix` to create the missing .fabric/knowledge/* subdirectories."
2848
+ t(`doctor.check.knowledge_dir_missing.message.${count === 1 ? "singular" : "plural"}`, {
2849
+ count: String(count),
2850
+ list
2851
+ }),
2852
+ t("doctor.check.knowledge_dir_missing.remediation")
2830
2853
  );
2831
2854
  }
2832
2855
  return okCheck(
2833
- "Knowledge layout",
2834
- `All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
2856
+ t("doctor.check.knowledge_dir_missing.name"),
2857
+ t("doctor.check.knowledge_dir_missing.ok", { count: String(KNOWLEDGE_SUBDIRS3.length) })
2835
2858
  );
2836
2859
  }
2837
- function createForensicCheck(forensic, frameworkKind, entryPointCount) {
2860
+ function createForensicCheck(t, forensic, frameworkKind, entryPointCount) {
2838
2861
  if (!forensic.present) {
2839
2862
  return issueCheck(
2840
- "Scan evidence",
2863
+ t("doctor.check.forensic.name"),
2841
2864
  "error",
2842
2865
  "manual_error",
2843
2866
  "forensic_missing",
2844
- `${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`,
2845
- "Run `fab install` to regenerate .fabric/forensic.json."
2867
+ t(`doctor.check.forensic.message.missing.${entryPointCount === 1 ? "singular" : "plural"}`, {
2868
+ error: forensic.error ?? t("doctor.check.forensic.message.missing-default"),
2869
+ frameworkKind,
2870
+ count: String(entryPointCount)
2871
+ }),
2872
+ t("doctor.check.forensic.remediation")
2846
2873
  );
2847
2874
  }
2848
2875
  if (!forensic.valid) {
2849
- return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.", "Run `fab install` to regenerate .fabric/forensic.json.");
2876
+ return issueCheck(
2877
+ t("doctor.check.forensic.name"),
2878
+ "error",
2879
+ "manual_error",
2880
+ "forensic_invalid",
2881
+ forensic.error ?? t("doctor.check.forensic.message.invalid-default"),
2882
+ t("doctor.check.forensic.remediation")
2883
+ );
2850
2884
  }
2851
- return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
2885
+ return okCheck(
2886
+ t("doctor.check.forensic.name"),
2887
+ t("doctor.check.forensic.ok", { frameworkKind: forensic.report?.framework.kind ?? "unknown" })
2888
+ );
2852
2889
  }
2853
- function createMetaCheck(meta) {
2890
+ function createMetaCheck(t, meta) {
2854
2891
  if (!meta.present) {
2855
- return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/knowledge/.");
2892
+ return issueCheck(
2893
+ t("doctor.check.agents_meta.name"),
2894
+ "error",
2895
+ "fixable_error",
2896
+ "agents_meta_missing",
2897
+ t("doctor.check.agents_meta.message.missing"),
2898
+ t("doctor.check.agents_meta.remediation.missing")
2899
+ );
2856
2900
  }
2857
2901
  if (!meta.valid) {
2858
- return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
2902
+ return issueCheck(
2903
+ t("doctor.check.agents_meta.name"),
2904
+ "error",
2905
+ "manual_error",
2906
+ "agents_meta_invalid",
2907
+ meta.readError ?? t("doctor.check.agents_meta.message.invalid-default"),
2908
+ t("doctor.check.agents_meta.remediation.invalid")
2909
+ );
2859
2910
  }
2860
2911
  if (meta.stale) {
2861
2912
  return issueCheck(
2862
- "Agents metadata",
2913
+ t("doctor.check.agents_meta.name"),
2863
2914
  "warn",
2864
2915
  "warning",
2865
2916
  "agents_meta_stale",
2866
- `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2867
- "Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
2917
+ t("doctor.check.agents_meta.message.stale", {
2918
+ revision: meta.revision,
2919
+ computedRevision: meta.computedRevision ?? "<unknown>"
2920
+ }),
2921
+ t("doctor.check.agents_meta.remediation.stale")
2868
2922
  );
2869
2923
  }
2870
- return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
2924
+ return okCheck(
2925
+ t("doctor.check.agents_meta.name"),
2926
+ t("doctor.check.agents_meta.ok", { revision: meta.revision })
2927
+ );
2871
2928
  }
2872
- function createRuleContentRefCheck(meta) {
2929
+ function createRuleContentRefCheck(t, meta) {
2873
2930
  if (!meta.valid) {
2874
- return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.", "Fix agents.meta.json first: run `fab doctor --fix`.");
2931
+ return issueCheck(
2932
+ t("doctor.check.rule_content_refs.name"),
2933
+ "error",
2934
+ "manual_error",
2935
+ "content_refs_unavailable",
2936
+ t("doctor.check.rule_content_refs.message.unavailable"),
2937
+ t("doctor.check.rule_content_refs.remediation.unavailable")
2938
+ );
2875
2939
  }
2876
2940
  if (meta.invalidContentRefs.length > 0) {
2941
+ const count = meta.invalidContentRefs.length;
2877
2942
  return issueCheck(
2878
- "Rule content refs",
2943
+ t("doctor.check.rule_content_refs.name"),
2879
2944
  "error",
2880
2945
  "manual_error",
2881
2946
  "content_ref_outside_rules",
2882
- `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/knowledge.`,
2883
- "Edit agents.meta.json to ensure all content_ref values point inside .fabric/knowledge/{type}/ (team) or ~/.fabric/knowledge/{type}/ (personal)."
2947
+ t(`doctor.check.rule_content_refs.message.outside.${count === 1 ? "singular" : "plural"}`, {
2948
+ count: String(count)
2949
+ }),
2950
+ t("doctor.check.rule_content_refs.remediation.outside")
2884
2951
  );
2885
2952
  }
2886
2953
  if (meta.missingContentRefs.length > 0) {
2954
+ const count = meta.missingContentRefs.length;
2887
2955
  return issueCheck(
2888
- "Rule content refs",
2956
+ t("doctor.check.rule_content_refs.name"),
2889
2957
  "error",
2890
2958
  "fixable_error",
2891
2959
  "content_ref_missing",
2892
- `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
2893
- "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/knowledge/."
2960
+ t(`doctor.check.rule_content_refs.message.missing.${count === 1 ? "singular" : "plural"}`, {
2961
+ count: String(count)
2962
+ }),
2963
+ t("doctor.check.rule_content_refs.remediation.missing")
2894
2964
  );
2895
2965
  }
2896
- return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
2966
+ return okCheck(t("doctor.check.rule_content_refs.name"), t("doctor.check.rule_content_refs.ok"));
2897
2967
  }
2898
- function createKnowledgeTestIndexCheck(index) {
2968
+ function createKnowledgeTestIndexCheck(t, index) {
2899
2969
  if (!index.present) {
2900
- return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/.cache/knowledge-test.index.json.");
2970
+ return issueCheck(
2971
+ t("doctor.check.knowledge_test_index.name"),
2972
+ "error",
2973
+ "fixable_error",
2974
+ "knowledge_test_index_missing",
2975
+ index.error,
2976
+ t("doctor.check.knowledge_test_index.remediation.missing")
2977
+ );
2901
2978
  }
2902
2979
  if (!index.valid) {
2903
- return issueCheck("Knowledge-test index", "error", "manual_error", "knowledge_test_index_invalid", index.error, "Delete .fabric/.cache/knowledge-test.index.json and run `fab doctor --fix` to regenerate it.");
2980
+ return issueCheck(
2981
+ t("doctor.check.knowledge_test_index.name"),
2982
+ "error",
2983
+ "manual_error",
2984
+ "knowledge_test_index_invalid",
2985
+ index.error,
2986
+ t("doctor.check.knowledge_test_index.remediation.invalid")
2987
+ );
2904
2988
  }
2905
2989
  if (index.stale) {
2906
- return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_stale", ".fabric/.cache/knowledge-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the knowledge-test index.");
2990
+ return issueCheck(
2991
+ t("doctor.check.knowledge_test_index.name"),
2992
+ "error",
2993
+ "fixable_error",
2994
+ "knowledge_test_index_stale",
2995
+ t("doctor.check.knowledge_test_index.message.stale"),
2996
+ t("doctor.check.knowledge_test_index.remediation.stale")
2997
+ );
2907
2998
  }
2908
- return okCheck("Knowledge-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
2999
+ return okCheck(
3000
+ t("doctor.check.knowledge_test_index.name"),
3001
+ t(
3002
+ `doctor.check.knowledge_test_index.ok.${index.linkCount === 1 ? "link_singular" : "link_plural"}.${index.orphanCount === 1 ? "orphan_singular" : "orphan_plural"}`,
3003
+ { linkCount: String(index.linkCount), orphanCount: String(index.orphanCount) }
3004
+ )
3005
+ );
2909
3006
  }
2910
- function createEventLedgerCheck(ledger) {
3007
+ function createEventLedgerCheck(t, ledger) {
2911
3008
  if (!ledger.exists) {
2912
- return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.", "Run `fab doctor --fix` to create .fabric/events.jsonl.");
3009
+ return issueCheck(
3010
+ t("doctor.check.event_ledger.name"),
3011
+ "error",
3012
+ "fixable_error",
3013
+ "event_ledger_missing",
3014
+ t("doctor.check.event_ledger.message.missing"),
3015
+ t("doctor.check.event_ledger.remediation.missing")
3016
+ );
2913
3017
  }
2914
3018
  if (!ledger.writable) {
2915
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.", "Check file permissions on .fabric/events.jsonl and ensure no other process holds a write lock.");
3019
+ return issueCheck(
3020
+ t("doctor.check.event_ledger.name"),
3021
+ "error",
3022
+ "manual_error",
3023
+ "event_ledger_not_writable",
3024
+ ledger.error ?? t("doctor.check.event_ledger.message.not_writable-default"),
3025
+ t("doctor.check.event_ledger.remediation.not_writable")
3026
+ );
2916
3027
  }
2917
3028
  if (!ledger.parseable) {
2918
- return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.", "Delete .fabric/events.jsonl and run `fab doctor --fix` to recreate it.");
3029
+ return issueCheck(
3030
+ t("doctor.check.event_ledger.name"),
3031
+ "error",
3032
+ "manual_error",
3033
+ "event_ledger_invalid",
3034
+ ledger.error ?? t("doctor.check.event_ledger.message.invalid-default"),
3035
+ t("doctor.check.event_ledger.remediation.invalid")
3036
+ );
2919
3037
  }
2920
- return okCheck("Event ledger", ".fabric/events.jsonl exists, is writable, and is parseable.");
3038
+ return okCheck(t("doctor.check.event_ledger.name"), t("doctor.check.event_ledger.ok"));
2921
3039
  }
2922
- function createMcpConfigInWrongFileCheck(inspection) {
3040
+ function createMcpConfigInWrongFileCheck(t, inspection) {
2923
3041
  if (inspection.hasWrongEntry) {
2924
3042
  return issueCheck(
2925
- "Claude MCP config location",
3043
+ t("doctor.check.mcp_config_in_wrong_file.name"),
2926
3044
  "error",
2927
3045
  "fixable_error",
2928
3046
  "mcp_config_in_wrong_file",
2929
- `.claude/settings.json contains mcpServers.fabric \u2014 this file is for hooks/permissions only. Run --fix to remove it, then re-run fab install to write .mcp.json.`,
2930
- "Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab install` to write .mcp.json."
3047
+ t("doctor.check.mcp_config_in_wrong_file.message"),
3048
+ t("doctor.check.mcp_config_in_wrong_file.remediation")
2931
3049
  );
2932
3050
  }
2933
- return okCheck("Claude MCP config location", "mcpServers.fabric is not in .claude/settings.json.");
3051
+ return okCheck(
3052
+ t("doctor.check.mcp_config_in_wrong_file.name"),
3053
+ t("doctor.check.mcp_config_in_wrong_file.ok")
3054
+ );
2934
3055
  }
2935
- function createEventLedgerPartialWriteCheck(ledger) {
3056
+ function createEventLedgerPartialWriteCheck(t, ledger) {
2936
3057
  if (!ledger.exists || !ledger.writable) {
2937
- return okCheck("Event ledger partial write", "No partial-write check needed (ledger missing or not writable).");
3058
+ return okCheck(
3059
+ t("doctor.check.event_ledger_partial_write.name"),
3060
+ t("doctor.check.event_ledger_partial_write.ok.skipped")
3061
+ );
2938
3062
  }
2939
3063
  if (ledger.hasPartialWrite) {
2940
3064
  return issueCheck(
2941
- "Event ledger partial write",
3065
+ t("doctor.check.event_ledger_partial_write.name"),
2942
3066
  "error",
2943
3067
  "fixable_error",
2944
3068
  "event_ledger_partial_write",
2945
- `events.jsonl has a partial write at byte offset ${ledger.partialWriteByteOffset} (${ledger.partialWriteByteLength} corrupted bytes). Run --fix to truncate and preserve corrupted bytes.`,
2946
- "Run `fab doctor --fix` to truncate the partial write and restore events.jsonl to a valid state."
3069
+ t("doctor.check.event_ledger_partial_write.message", {
3070
+ byteOffset: String(ledger.partialWriteByteOffset),
3071
+ byteLength: String(ledger.partialWriteByteLength)
3072
+ }),
3073
+ t("doctor.check.event_ledger_partial_write.remediation")
2947
3074
  );
2948
3075
  }
2949
- return okCheck("Event ledger partial write", "events.jsonl has no partial trailing write.");
3076
+ return okCheck(
3077
+ t("doctor.check.event_ledger_partial_write.name"),
3078
+ t("doctor.check.event_ledger_partial_write.ok.clean")
3079
+ );
2950
3080
  }
2951
3081
  function okCheck(name, message) {
2952
3082
  return { name, status: "ok", message };
@@ -2966,7 +3096,8 @@ function collectIssues(checks, kind) {
2966
3096
  return checks.filter((check) => check.kind === kind).map((check) => ({
2967
3097
  code: check.code ?? check.name,
2968
3098
  name: check.name,
2969
- message: check.message
3099
+ message: check.message,
3100
+ actionHint: check.actionHint
2970
3101
  }));
2971
3102
  }
2972
3103
  function findIssue(issues, code) {
@@ -3053,18 +3184,24 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
3053
3184
  }
3054
3185
  }
3055
3186
  }
3056
- function createKnowledgeDirUnindexedCheck(inspection) {
3187
+ function createKnowledgeDirUnindexedCheck(t, inspection) {
3057
3188
  if (inspection.unindexedFiles.length > 0) {
3189
+ const count = inspection.unindexedFiles.length;
3058
3190
  return issueCheck(
3059
- "Knowledge dir unindexed",
3191
+ t("doctor.check.knowledge_dir_unindexed.name"),
3060
3192
  "error",
3061
3193
  "fixable_error",
3062
3194
  "knowledge_dir_unindexed",
3063
- `${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/knowledge/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing knowledge files.`,
3064
- "Run `fab doctor --fix` to index the missing knowledge files."
3195
+ t(`doctor.check.knowledge_dir_unindexed.message.${count === 1 ? "singular" : "plural"}`, {
3196
+ count: String(count)
3197
+ }),
3198
+ t("doctor.check.knowledge_dir_unindexed.remediation")
3065
3199
  );
3066
3200
  }
3067
- return okCheck("Knowledge dir unindexed", "All .fabric/knowledge/ .md files are indexed in agents.meta.json.");
3201
+ return okCheck(
3202
+ t("doctor.check.knowledge_dir_unindexed.name"),
3203
+ t("doctor.check.knowledge_dir_unindexed.ok")
3204
+ );
3068
3205
  }
3069
3206
  async function inspectStableIdCollisions(projectRoot) {
3070
3207
  const found = [];
@@ -3147,7 +3284,7 @@ function inspectCounterDesync(meta) {
3147
3284
  ["guideline", "GLD"],
3148
3285
  ["pitfall", "PIT"],
3149
3286
  ["process", "PRO"]
3150
- ].find(([t2]) => t2 === parsed.type)?.[1];
3287
+ ].find(([t]) => t === parsed.type)?.[1];
3151
3288
  if (typeCode === void 0) {
3152
3289
  continue;
3153
3290
  }
@@ -3175,61 +3312,84 @@ function inspectCounterDesync(meta) {
3175
3312
  correctedCounters: desyncs.length === 0 ? null : corrected
3176
3313
  };
3177
3314
  }
3178
- function createCounterDesyncCheck(inspection) {
3315
+ function createCounterDesyncCheck(t, inspection) {
3179
3316
  if (inspection.desyncs.length > 0) {
3180
3317
  const first = inspection.desyncs[0];
3181
- const detail = `counters.${first.layer}.${first.type} = ${first.current} but observed K${first.layer === "KP" ? "P" : "T"}-${first.type}-${String(first.observed).padStart(4, "0")}`;
3318
+ const observedId = `K${first.layer === "KP" ? "P" : "T"}-${first.type}-${String(first.observed).padStart(4, "0")}`;
3319
+ const count = inspection.desyncs.length;
3182
3320
  return issueCheck(
3183
- "Knowledge counter desync",
3321
+ t("doctor.check.counter_desync.name"),
3184
3322
  "error",
3185
3323
  "fixable_error",
3186
3324
  "counter_desync",
3187
- `${inspection.desyncs.length} knowledge counter${inspection.desyncs.length === 1 ? "" : "s"} desynced from observed stable_ids. ${detail}. Run \`fab doctor --fix\` to bump counters.`,
3188
- "Run `fab doctor --fix` to bump agents.meta.json counters to the maximum observed counter value."
3325
+ t(`doctor.check.counter_desync.message.${count === 1 ? "singular" : "plural"}`, {
3326
+ count: String(count),
3327
+ counterPath: `counters.${first.layer}.${first.type}`,
3328
+ current: String(first.current),
3329
+ observedId
3330
+ }),
3331
+ t("doctor.check.counter_desync.remediation")
3189
3332
  );
3190
3333
  }
3191
- return okCheck("Knowledge counter desync", "agents.meta.json counters envelope is consistent with observed stable_ids.");
3334
+ return okCheck(t("doctor.check.counter_desync.name"), t("doctor.check.counter_desync.ok"));
3192
3335
  }
3193
- function createStableIdCollisionCheck(inspection) {
3336
+ function createStableIdCollisionCheck(t, inspection) {
3194
3337
  if (inspection.collisions.length > 0) {
3195
3338
  const first = inspection.collisions[0];
3196
- const detail = inspection.collisions.length === 1 ? `stable_id "${first.stable_id}" is declared in ${first.files.length} files: ${first.files.join(", ")}.` : `${inspection.collisions.length} stable_id collision${inspection.collisions.length === 1 ? "" : "s"} detected. First: "${first.stable_id}" in ${first.files.join(", ")}.`;
3339
+ const count = inspection.collisions.length;
3197
3340
  return issueCheck(
3198
- "Stable ID collision",
3341
+ t("doctor.check.stable_id_collision.name"),
3199
3342
  "warn",
3200
3343
  "warning",
3201
3344
  "stable_id_collision",
3202
- `${detail} Edit one of the knowledge files to use a unique stable_id.`,
3203
- "Edit one of the colliding knowledge files to declare a different `id: K[PT]-XXX-NNNN` frontmatter value."
3345
+ t(`doctor.check.stable_id_collision.message.${count === 1 ? "singular" : "plural"}`, {
3346
+ count: String(count),
3347
+ stableId: first.stable_id,
3348
+ fileCount: String(first.files.length),
3349
+ files: first.files.join(", ")
3350
+ }),
3351
+ t("doctor.check.stable_id_collision.remediation")
3204
3352
  );
3205
3353
  }
3206
- return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/knowledge/.");
3354
+ return okCheck(t("doctor.check.stable_id_collision.name"), t("doctor.check.stable_id_collision.ok"));
3207
3355
  }
3208
- function createMetaManuallyDivergedCheck(inspection) {
3356
+ function createMetaManuallyDivergedCheck(t, inspection) {
3209
3357
  if (!inspection.readable) {
3210
- return okCheck("Meta manual divergence", "agents.meta.json not readable; skipping divergence check.");
3358
+ return okCheck(
3359
+ t("doctor.check.meta_manually_diverged.name"),
3360
+ t("doctor.check.meta_manually_diverged.ok.unreadable")
3361
+ );
3211
3362
  }
3212
3363
  if (inspection.extraMetaEntries.length > 0) {
3364
+ const count = inspection.extraMetaEntries.length;
3213
3365
  return issueCheck(
3214
- "Meta manual divergence",
3366
+ t("doctor.check.meta_manually_diverged.name"),
3215
3367
  "warn",
3216
3368
  "warning",
3217
3369
  "meta_manually_diverged",
3218
- `agents.meta.json has ${inspection.extraMetaEntries.length} entr${inspection.extraMetaEntries.length === 1 ? "y" : "ies"} with no backing file on disk. Run --fix to reconcile.`,
3219
- "Run `fab doctor --fix` to reconcile agents.meta.json with the rule files currently on disk."
3370
+ t(`doctor.check.meta_manually_diverged.message.extra.${count === 1 ? "singular" : "plural"}`, {
3371
+ count: String(count)
3372
+ }),
3373
+ t("doctor.check.meta_manually_diverged.remediation.extra")
3220
3374
  );
3221
3375
  }
3222
3376
  if (inspection.hashMismatchEntries.length > 0) {
3377
+ const count = inspection.hashMismatchEntries.length;
3223
3378
  return issueCheck(
3224
- "Meta manual divergence",
3379
+ t("doctor.check.meta_manually_diverged.name"),
3225
3380
  "warn",
3226
3381
  "warning",
3227
3382
  "meta_manually_diverged",
3228
- `agents.meta.json has ${inspection.hashMismatchEntries.length} entr${inspection.hashMismatchEntries.length === 1 ? "y" : "ies"} whose hash does not match the file on disk. Run --fix to reconcile.`,
3229
- "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule file contents."
3383
+ t(`doctor.check.meta_manually_diverged.message.hash.${count === 1 ? "singular" : "plural"}`, {
3384
+ count: String(count)
3385
+ }),
3386
+ t("doctor.check.meta_manually_diverged.remediation.hash")
3230
3387
  );
3231
3388
  }
3232
- return okCheck("Meta manual divergence", "agents.meta.json is consistent with rule files on disk.");
3389
+ return okCheck(
3390
+ t("doctor.check.meta_manually_diverged.name"),
3391
+ t("doctor.check.meta_manually_diverged.ok.consistent")
3392
+ );
3233
3393
  }
3234
3394
  function inspectPreexistingRootFiles(projectRoot) {
3235
3395
  const candidates = ["CLAUDE.md", "AGENTS.md"];
@@ -3295,36 +3455,44 @@ async function inspectFilesystemEditFallback(projectRoot) {
3295
3455
  }
3296
3456
  return { synthesized: orphanIds.length, synthesizedStableIds: orphanIds };
3297
3457
  }
3298
- function createFilesystemEditFallbackCheck(inspection) {
3458
+ function createFilesystemEditFallbackCheck(t, inspection) {
3299
3459
  if (inspection.synthesized === 0) {
3300
3460
  return okCheck(
3301
- "Filesystem-edit fallback",
3302
- "No orphan canonical knowledge entries detected; events.jsonl promotion trail is complete."
3461
+ t("doctor.check.filesystem_edit_fallback.name"),
3462
+ t("doctor.check.filesystem_edit_fallback.ok")
3303
3463
  );
3304
3464
  }
3305
3465
  const sample = inspection.synthesizedStableIds.slice(0, 3).join(", ");
3306
3466
  return {
3307
- name: "Filesystem-edit fallback",
3467
+ name: t("doctor.check.filesystem_edit_fallback.name"),
3308
3468
  status: "ok",
3309
3469
  kind: "info",
3310
3470
  code: "knowledge_promoted_synthesized",
3311
3471
  fixable: false,
3312
- message: `Synthesized ${inspection.synthesized} knowledge_promoted event${inspection.synthesized === 1 ? "" : "s"} for orphan canonical entries (${sample}${inspection.synthesizedStableIds.length > 3 ? ", ..." : ""}). Reason='${SYNTHESIZED_PROMOTED_REASON}'.`,
3313
- actionHint: "These entries were moved into .fabric/knowledge/<type>/ outside fab_review.approve. The synthesized events restore audit-trail completeness."
3472
+ message: t(
3473
+ `doctor.check.filesystem_edit_fallback.message.synthesized.${inspection.synthesized === 1 ? "singular" : "plural"}`,
3474
+ {
3475
+ count: String(inspection.synthesized),
3476
+ sample,
3477
+ suffix: inspection.synthesizedStableIds.length > 3 ? ", ..." : "",
3478
+ reason: SYNTHESIZED_PROMOTED_REASON
3479
+ }
3480
+ ),
3481
+ actionHint: t("doctor.check.filesystem_edit_fallback.remediation.synthesized")
3314
3482
  };
3315
3483
  }
3316
- function createPreexistingRootFilesCheck(inspection) {
3484
+ function createPreexistingRootFilesCheck(t, inspection) {
3317
3485
  if (inspection.detected.length === 0) {
3318
- return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
3486
+ return okCheck(t("doctor.check.preexisting_root_files.name"), t("doctor.check.preexisting_root_files.ok"));
3319
3487
  }
3320
3488
  return {
3321
- name: "Preexisting root markdown",
3489
+ name: t("doctor.check.preexisting_root_files.name"),
3322
3490
  status: "ok",
3323
3491
  kind: "info",
3324
3492
  code: "preexisting_root_claude_md",
3325
3493
  fixable: false,
3326
- message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
3327
- actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
3494
+ message: t("doctor.check.preexisting_root_files.message", { files: inspection.detected.join(", ") }),
3495
+ actionHint: t("doctor.check.preexisting_root_files.remediation")
3328
3496
  };
3329
3497
  }
3330
3498
  async function buildLastConsumedIndex(projectRoot) {
@@ -3822,114 +3990,156 @@ function readUnderseedThresholdFromConfig(projectRoot) {
3822
3990
  }
3823
3991
  return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3824
3992
  }
3825
- function createOrphanDemoteCheck(inspection) {
3993
+ function createOrphanDemoteCheck(t, inspection) {
3826
3994
  if (inspection.candidates.length === 0) {
3827
3995
  return okCheck(
3828
- "Knowledge orphan demote",
3829
- "No canonical knowledge entries exceed their maturity-keyed inactivity threshold."
3996
+ t("doctor.check.orphan_demote.name"),
3997
+ t("doctor.check.orphan_demote.ok")
3830
3998
  );
3831
3999
  }
3832
4000
  const first = inspection.candidates[0];
3833
4001
  const detail = `${first.stable_id} (${first.maturity}, ${first.age_days}d inactive at ${first.path})`;
4002
+ const count = inspection.candidates.length;
3834
4003
  return issueCheck(
3835
- "Knowledge orphan demote",
4004
+ t("doctor.check.orphan_demote.name"),
3836
4005
  "warn",
3837
4006
  "warning",
3838
4007
  "knowledge_orphan_demote_required",
3839
- `${inspection.candidates.length} canonical knowledge entr${inspection.candidates.length === 1 ? "y exceeds" : "ies exceed"} their maturity-keyed inactivity threshold (stable=${ORPHAN_DEMOTE_THRESHOLD_DAYS.stable}d / endorsed=${ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed}d / draft=${ORPHAN_DEMOTE_THRESHOLD_DAYS.draft}d). First: ${detail}.`,
3840
- "Run `fab doctor --apply-lint` (rc.4 TASK-003) to demote orphan entries one maturity tier."
4008
+ t(`doctor.check.orphan_demote.message.${count === 1 ? "singular" : "plural"}`, {
4009
+ count: String(count),
4010
+ stableDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.stable),
4011
+ endorsedDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed),
4012
+ draftDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.draft),
4013
+ detail
4014
+ }),
4015
+ t("doctor.check.orphan_demote.remediation")
3841
4016
  );
3842
4017
  }
3843
- function createStaleArchiveCheck(inspection) {
4018
+ function createStaleArchiveCheck(t, inspection) {
3844
4019
  if (inspection.candidates.length === 0) {
3845
4020
  return okCheck(
3846
- "Knowledge stale archive",
3847
- "No draft knowledge entries exceed the additional stale-archive quiet window."
4021
+ t("doctor.check.stale_archive.name"),
4022
+ t("doctor.check.stale_archive.ok")
3848
4023
  );
3849
4024
  }
3850
4025
  const first = inspection.candidates[0];
3851
4026
  const detail = `${first.stable_id} (${first.age_days}d inactive at ${first.path}) \u2192 ${first.archive_path}`;
4027
+ const count = inspection.candidates.length;
3852
4028
  return issueCheck(
3853
- "Knowledge stale archive",
4029
+ t("doctor.check.stale_archive.name"),
3854
4030
  "warn",
3855
4031
  "warning",
3856
4032
  "knowledge_stale_archive_required",
3857
- `${inspection.candidates.length} draft knowledge entr${inspection.candidates.length === 1 ? "y is" : "ies are"} stale beyond the demote+${STALE_ARCHIVE_ADDITIONAL_DAYS}d additional quiet window. First: ${detail}.`,
3858
- "Run `fab doctor --apply-lint` (rc.4 TASK-003) to move stale entries into `.fabric/.archive/<type>/`."
4033
+ t(`doctor.check.stale_archive.message.${count === 1 ? "singular" : "plural"}`, {
4034
+ count: String(count),
4035
+ additionalDays: String(STALE_ARCHIVE_ADDITIONAL_DAYS),
4036
+ detail
4037
+ }),
4038
+ t("doctor.check.stale_archive.remediation")
3859
4039
  );
3860
4040
  }
3861
- function createPendingOverdueCheck(inspection) {
4041
+ function createPendingOverdueCheck(t, inspection) {
3862
4042
  if (inspection.candidates.length === 0) {
3863
4043
  return okCheck(
3864
- "Knowledge pending overdue",
3865
- "No pending knowledge entries exceed the 14-day review threshold."
4044
+ t("doctor.check.pending_overdue.name"),
4045
+ t("doctor.check.pending_overdue.ok")
3866
4046
  );
3867
4047
  }
3868
4048
  const first = inspection.candidates[0];
3869
4049
  const detail = `${first.path} (${first.age_days}d old)`;
4050
+ const count = inspection.candidates.length;
3870
4051
  return issueCheck(
3871
- "Knowledge pending overdue",
4052
+ t("doctor.check.pending_overdue.name"),
3872
4053
  "warn",
3873
4054
  "warning",
3874
4055
  "knowledge_pending_overdue",
3875
- `${inspection.candidates.length} pending knowledge entr${inspection.candidates.length === 1 ? "y has" : "ies have"} been awaiting review for more than ${PENDING_OVERDUE_THRESHOLD_DAYS} days. First: ${detail}.`,
3876
- "Review pending entries via the fabric-review Skill (`/fabric-review`) and approve, reject, defer, or modify."
4056
+ t(`doctor.check.pending_overdue.message.${count === 1 ? "singular" : "plural"}`, {
4057
+ count: String(count),
4058
+ thresholdDays: String(PENDING_OVERDUE_THRESHOLD_DAYS),
4059
+ detail
4060
+ }),
4061
+ t("doctor.check.pending_overdue.remediation")
3877
4062
  );
3878
4063
  }
3879
- function createUnderseededCheck(inspection) {
4064
+ function createUnderseededCheck(t, inspection) {
3880
4065
  if (!inspection.underseeded) {
3881
4066
  return okCheck(
3882
- "Knowledge underseeded",
3883
- `Knowledge corpus has ${inspection.node_count} canonical entries (>= ${inspection.threshold}).`
4067
+ t("doctor.check.underseeded.name"),
4068
+ t("doctor.check.underseeded.ok", {
4069
+ count: String(inspection.node_count),
4070
+ threshold: String(inspection.threshold)
4071
+ })
3884
4072
  );
3885
4073
  }
3886
4074
  return issueCheck(
3887
- "Knowledge underseeded",
4075
+ t("doctor.check.underseeded.name"),
3888
4076
  "ok",
3889
4077
  "info",
3890
4078
  "knowledge_underseeded",
3891
- `Knowledge corpus has only ${inspection.node_count} canonical entr${inspection.node_count === 1 ? "y" : "ies"} (< ${inspection.threshold} threshold). The plan_context retrieval surface is below its useful floor.`,
3892
- "Run the fabric-import Skill (`/fabric-import`) to backfill knowledge from git history and existing docs."
4079
+ t(`doctor.check.underseeded.message.${inspection.node_count === 1 ? "singular" : "plural"}`, {
4080
+ count: String(inspection.node_count),
4081
+ threshold: String(inspection.threshold)
4082
+ }),
4083
+ t("doctor.check.underseeded.remediation")
3893
4084
  );
3894
4085
  }
3895
- function createSessionHintsStaleCheck(inspection) {
4086
+ function createSessionHintsStaleCheck(t, inspection) {
3896
4087
  if (inspection.candidates.length === 0) {
3897
4088
  return okCheck(
3898
- "Knowledge session-hints stale",
3899
- `No session-hints cache files older than ${SESSION_HINTS_STALE_DAYS} days under .fabric/.cache/.`
4089
+ t("doctor.check.session_hints_stale.name"),
4090
+ t("doctor.check.session_hints_stale.ok", {
4091
+ days: String(SESSION_HINTS_STALE_DAYS)
4092
+ })
3900
4093
  );
3901
4094
  }
3902
4095
  const first = inspection.candidates[0];
3903
4096
  const detail = `${first.path} (${first.age_days}d old)`;
4097
+ const count = inspection.candidates.length;
3904
4098
  return issueCheck(
3905
- "Knowledge session-hints stale",
4099
+ t("doctor.check.session_hints_stale.name"),
3906
4100
  "ok",
3907
4101
  "info",
3908
4102
  "knowledge_session_hints_stale",
3909
- `${inspection.candidates.length} session-hints cache file${inspection.candidates.length === 1 ? "" : "s"} under .fabric/.cache/ ${inspection.candidates.length === 1 ? "is" : "are"} older than ${SESSION_HINTS_STALE_DAYS} days. First: ${detail}.`,
3910
- "Run `fab doctor --apply-lint` to delete stale session-hints cache files."
4103
+ t(`doctor.check.session_hints_stale.message.${count === 1 ? "singular" : "plural"}`, {
4104
+ count: String(count),
4105
+ days: String(SESSION_HINTS_STALE_DAYS),
4106
+ detail
4107
+ }),
4108
+ t("doctor.check.session_hints_stale.remediation")
3911
4109
  );
3912
4110
  }
3913
- function createStaleServeLockCheck(inspection) {
4111
+ function createStaleServeLockCheck(t, inspection) {
3914
4112
  if (!inspection.present) {
3915
- return okCheck("Serve lock", "No .fabric/.serve.lock present.");
4113
+ return okCheck(
4114
+ t("doctor.check.stale_serve_lock.name"),
4115
+ t("doctor.check.stale_serve_lock.ok.no_lock")
4116
+ );
3916
4117
  }
3917
4118
  if (inspection.pidAlive) {
3918
4119
  return okCheck(
3919
- "Serve lock",
3920
- `.fabric/.serve.lock held by live PID ${inspection.pid}.`
4120
+ t("doctor.check.stale_serve_lock.name"),
4121
+ t("doctor.check.stale_serve_lock.ok.live_pid", {
4122
+ pid: String(inspection.pid)
4123
+ })
3921
4124
  );
3922
4125
  }
3923
4126
  const days = Math.floor(inspection.ageMs / MS_PER_DAY);
3924
4127
  const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
3925
- const acquiredAgo = days >= 1 ? `${days} day${days === 1 ? "" : "s"} ago` : `${hours} hour${hours === 1 ? "" : "s"} ago`;
4128
+ const acquiredAgo = days >= 1 ? t(`doctor.check.stale_serve_lock.age.day.${days === 1 ? "singular" : "plural"}`, {
4129
+ count: String(days)
4130
+ }) : t(`doctor.check.stale_serve_lock.age.hour.${hours === 1 ? "singular" : "plural"}`, {
4131
+ count: String(hours)
4132
+ });
3926
4133
  return issueCheck(
3927
- "Serve lock",
4134
+ t("doctor.check.stale_serve_lock.name"),
3928
4135
  "ok",
3929
4136
  "info",
3930
4137
  "stale_serve_lock",
3931
- `[advisory] .fabric/.serve.lock holds dead PID ${inspection.pid} (acquired ${acquiredAgo}). Run \`fab doctor --fix\` to remove.`,
3932
- "Run `fab doctor --fix` to remove the stale .fabric/.serve.lock."
4138
+ t("doctor.check.stale_serve_lock.message.dead_pid", {
4139
+ pid: String(inspection.pid),
4140
+ acquiredAgo
4141
+ }),
4142
+ t("doctor.check.stale_serve_lock.remediation.dead_pid")
3933
4143
  );
3934
4144
  }
3935
4145
  function extractKnowledgeFrontmatterRelevanceScope(source) {
@@ -4128,64 +4338,81 @@ function readRecentGitTouchedPaths(projectRoot, windowDays) {
4128
4338
  }
4129
4339
  return Array.from(set);
4130
4340
  }
4131
- function createNarrowNoPathsCheck(inspection) {
4341
+ function createNarrowNoPathsCheck(t, inspection) {
4132
4342
  if (inspection.candidates.length === 0) {
4133
4343
  return okCheck(
4134
- "Knowledge narrow without paths",
4135
- "No narrow-scope canonical entries have an empty relevance_paths array."
4344
+ t("doctor.check.narrow_no_paths.name"),
4345
+ t("doctor.check.narrow_no_paths.ok")
4136
4346
  );
4137
4347
  }
4138
4348
  const first = inspection.candidates[0];
4139
4349
  const detail = `${first.stable_id} (${first.path})`;
4350
+ const count = inspection.candidates.length;
4140
4351
  return issueCheck(
4141
- "Knowledge narrow without paths",
4352
+ t("doctor.check.narrow_no_paths.name"),
4142
4353
  "warn",
4143
4354
  "warning",
4144
4355
  "knowledge_narrow_no_paths",
4145
- `${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} an empty relevance_paths array (silent recall risk \u2014 narrow without anchors can never match a target path). First: ${detail}.`,
4146
- "Either add path anchors to relevance_paths or widen the entry's relevance_scope to broad."
4356
+ t(`doctor.check.narrow_no_paths.message.${count === 1 ? "singular" : "plural"}`, {
4357
+ count: String(count),
4358
+ detail
4359
+ }),
4360
+ t("doctor.check.narrow_no_paths.remediation")
4147
4361
  );
4148
4362
  }
4149
- function createRelevancePathsDanglingCheck(inspection) {
4363
+ function createRelevancePathsDanglingCheck(t, inspection) {
4150
4364
  if (inspection.entries.length === 0) {
4151
4365
  return okCheck(
4152
- "Knowledge relevance_paths dangling",
4153
- "All relevance_paths globs resolve to at least one file under the workspace root."
4366
+ t("doctor.check.relevance_paths_dangling.name"),
4367
+ t("doctor.check.relevance_paths_dangling.ok")
4154
4368
  );
4155
4369
  }
4156
4370
  const first = inspection.entries[0];
4157
4371
  const detail = `${first.stable_id} at ${first.path} \u2192 \`${first.dangling_glob}\` (0 matches)`;
4372
+ const count = inspection.entries.length;
4158
4373
  return issueCheck(
4159
- "Knowledge relevance_paths dangling",
4374
+ t("doctor.check.relevance_paths_dangling.name"),
4160
4375
  "warn",
4161
4376
  "warning",
4162
4377
  "knowledge_relevance_paths_dangling",
4163
- `${inspection.entries.length} relevance_paths glob${inspection.entries.length === 1 ? " resolves" : "s resolve"} to zero files in the current workspace. First: ${detail}.`,
4164
- "Update the entry's relevance_paths to remove globs that no longer match any files, or use `fab_review.modify` to rewrite the anchor set."
4378
+ t(`doctor.check.relevance_paths_dangling.message.${count === 1 ? "singular" : "plural"}`, {
4379
+ count: String(count),
4380
+ detail
4381
+ }),
4382
+ t("doctor.check.relevance_paths_dangling.remediation")
4165
4383
  );
4166
4384
  }
4167
- function createRelevancePathsDriftCheck(inspection) {
4385
+ function createRelevancePathsDriftCheck(t, inspection) {
4168
4386
  if (!inspection.git_available) {
4169
4387
  return okCheck(
4170
- "Knowledge relevance_paths drift",
4171
- `Skipped (git history unavailable; cannot evaluate ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d drift window).`
4388
+ t("doctor.check.relevance_paths_drift.name"),
4389
+ t("doctor.check.relevance_paths_drift.ok.skipped", {
4390
+ windowDays: String(RELEVANCE_PATHS_DRIFT_WINDOW_DAYS)
4391
+ })
4172
4392
  );
4173
4393
  }
4174
4394
  if (inspection.candidates.length === 0) {
4175
4395
  return okCheck(
4176
- "Knowledge relevance_paths drift",
4177
- `All narrow-scope canonical entries have at least one relevance_path touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d.`
4396
+ t("doctor.check.relevance_paths_drift.name"),
4397
+ t("doctor.check.relevance_paths_drift.ok.fresh", {
4398
+ windowDays: String(RELEVANCE_PATHS_DRIFT_WINDOW_DAYS)
4399
+ })
4178
4400
  );
4179
4401
  }
4180
4402
  const first = inspection.candidates[0];
4181
4403
  const detail = `${first.stable_id} at ${first.path} (globs: ${first.globs.join(", ")})`;
4404
+ const count = inspection.candidates.length;
4182
4405
  return issueCheck(
4183
- "Knowledge relevance_paths drift",
4406
+ t("doctor.check.relevance_paths_drift.name"),
4184
4407
  "ok",
4185
4408
  "info",
4186
4409
  "knowledge_relevance_paths_drift",
4187
- `${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} relevance_paths whose globs match no file touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d of git history. First: ${detail}.`,
4188
- "Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
4410
+ t(`doctor.check.relevance_paths_drift.message.${count === 1 ? "singular" : "plural"}`, {
4411
+ count: String(count),
4412
+ windowDays: String(RELEVANCE_PATHS_DRIFT_WINDOW_DAYS),
4413
+ detail
4414
+ }),
4415
+ t("doctor.check.relevance_paths_drift.remediation")
4189
4416
  );
4190
4417
  }
4191
4418
  function inspectRelevanceFieldsMissing(projectRoot) {
@@ -4328,11 +4555,11 @@ async function applyRelevanceFieldsMissing(candidate) {
4328
4555
  };
4329
4556
  }
4330
4557
  }
4331
- function createRelevanceFieldsMissingCheck(inspection) {
4558
+ function createRelevanceFieldsMissingCheck(t, inspection) {
4332
4559
  if (inspection.candidates.length === 0) {
4333
4560
  return okCheck(
4334
- "Knowledge relevance fields missing",
4335
- "All pending entries declare both relevance_scope and relevance_paths."
4561
+ t("doctor.check.relevance_fields_missing.name"),
4562
+ t("doctor.check.relevance_fields_missing.ok")
4336
4563
  );
4337
4564
  }
4338
4565
  const first = inspection.candidates[0];
@@ -4340,13 +4567,17 @@ function createRelevanceFieldsMissingCheck(inspection) {
4340
4567
  if (first.missing_scope) missingParts.push("relevance_scope");
4341
4568
  if (first.missing_paths) missingParts.push("relevance_paths");
4342
4569
  const detail = `${first.pending_path} (missing: ${missingParts.join(", ")})`;
4570
+ const count = inspection.candidates.length;
4343
4571
  return issueCheck(
4344
- "Knowledge relevance fields missing",
4572
+ t("doctor.check.relevance_fields_missing.name"),
4345
4573
  "ok",
4346
4574
  "info",
4347
4575
  "knowledge_relevance_fields_missing",
4348
- `${inspection.candidates.length} pending entr${inspection.candidates.length === 1 ? "y is" : "ies are"} missing relevance_scope and/or relevance_paths in frontmatter. First: ${detail}.`,
4349
- "Run `fab doctor --apply-lint` to back-fill the schema defaults (relevance_scope: broad, relevance_paths: [])."
4576
+ t(`doctor.check.relevance_fields_missing.message.${count === 1 ? "singular" : "plural"}`, {
4577
+ count: String(count),
4578
+ detail
4579
+ }),
4580
+ t("doctor.check.relevance_fields_missing.remediation")
4350
4581
  );
4351
4582
  }
4352
4583
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
@@ -4417,23 +4648,26 @@ function extractSkillFrontmatterLines(raw) {
4417
4648
  }
4418
4649
  return null;
4419
4650
  }
4420
- function createSkillMdYamlInvalidCheck(inspection) {
4651
+ function createSkillMdYamlInvalidCheck(t, inspection) {
4421
4652
  if (inspection.candidates.length === 0) {
4422
4653
  return okCheck(
4423
- "Skill markdown YAML",
4424
- "All .claude/.codex SKILL.md frontmatter values parse as strict YAML."
4654
+ t("doctor.check.skill_md_yaml_invalid.name"),
4655
+ t("doctor.check.skill_md_yaml_invalid.ok")
4425
4656
  );
4426
4657
  }
4427
4658
  const first = inspection.candidates[0];
4428
4659
  const detail = `${first.path}:${first.line} (key \`${first.key}\` value contains an unquoted ': ' \u2014 preview: \`${first.preview}\`)`;
4429
4660
  const plural = inspection.candidates.length === 1;
4430
4661
  return issueCheck(
4431
- "Skill markdown YAML",
4662
+ t("doctor.check.skill_md_yaml_invalid.name"),
4432
4663
  "warn",
4433
4664
  "warning",
4434
4665
  "skill_md_yaml_invalid",
4435
- `${inspection.candidates.length} SKILL.md frontmatter ${plural ? "value contains" : "values contain"} an unquoted ': ' that strict YAML parsers reject (Claude Code tolerates it; Codex CLI drops the skill at load). First: ${detail}.`,
4436
- 'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
4666
+ t(`doctor.check.skill_md_yaml_invalid.message.${plural ? "singular" : "plural"}`, {
4667
+ count: String(inspection.candidates.length),
4668
+ detail
4669
+ }),
4670
+ t("doctor.check.skill_md_yaml_invalid.remediation")
4437
4671
  );
4438
4672
  }
4439
4673
  var KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD = [
@@ -4528,55 +4762,83 @@ function readFrontmatterScalar(content, key) {
4528
4762
  }
4529
4763
  return void 0;
4530
4764
  }
4531
- function createOnboardCoverageCheck(inspection) {
4765
+ function createOnboardCoverageCheck(t, inspection) {
4532
4766
  const filledCount = ONBOARD_SLOT_NAMES.filter(
4533
4767
  (slot) => inspection.filled[slot].length > 0
4534
4768
  ).length;
4535
4769
  if (inspection.missing.length === 0) {
4536
4770
  return okCheck(
4537
- "Onboard coverage",
4538
- `Onboard coverage: ${filledCount}/${ONBOARD_SLOT_TOTAL} \u2713 (opted-out: ${inspection.opted_out.length}).`
4771
+ t("doctor.check.onboard_coverage.name"),
4772
+ t("doctor.check.onboard_coverage.ok.complete", {
4773
+ filledCount: String(filledCount),
4774
+ total: String(ONBOARD_SLOT_TOTAL),
4775
+ optedOutCount: String(inspection.opted_out.length)
4776
+ })
4539
4777
  );
4540
4778
  }
4541
4779
  return issueCheck(
4542
- "Onboard coverage",
4780
+ t("doctor.check.onboard_coverage.name"),
4543
4781
  "ok",
4544
4782
  "info",
4545
4783
  "onboard_coverage_incomplete",
4546
- `Onboard slots not yet covered: [${inspection.missing.join(", ")}]. ${filledCount}/${ONBOARD_SLOT_TOTAL} filled; ${inspection.opted_out.length} opted-out.`,
4547
- "Run /fabric-archive to onboard \u2014 the Skill's first-run phase will tour the project and propose pending entries for each unclaimed slot."
4784
+ t("doctor.check.onboard_coverage.message.incomplete", {
4785
+ missingSlots: inspection.missing.join(", "),
4786
+ filledCount: String(filledCount),
4787
+ total: String(ONBOARD_SLOT_TOTAL),
4788
+ optedOutCount: String(inspection.opted_out.length)
4789
+ }),
4790
+ t("doctor.check.onboard_coverage.remediation.incomplete")
4548
4791
  );
4549
4792
  }
4550
- function createNarrowTooFewCheck(inspection) {
4793
+ function createNarrowTooFewCheck(t, inspection) {
4551
4794
  const { structural_flagged, telemetry_flagged } = inspection;
4552
4795
  if (!structural_flagged && !telemetry_flagged) {
4553
4796
  const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
4554
- const teleNote = inspection.telemetry_skipped ? "telemetry skipped (no edit-counter fires in window)" : `silence rate ${(inspection.silence_rate * 100).toFixed(0)}% over ${SILENCE_WINDOW_DAYS}d`;
4797
+ const teleNote = inspection.telemetry_skipped ? t("doctor.check.narrow_too_few.message.telemetry_skipped") : t("doctor.check.narrow_too_few.message.telemetry_window", {
4798
+ silencePct: (inspection.silence_rate * 100).toFixed(0),
4799
+ windowDays: String(SILENCE_WINDOW_DAYS)
4800
+ });
4555
4801
  return okCheck(
4556
- "Knowledge narrow too few",
4557
- `Narrow-with-paths ratio ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}); ${teleNote}.`
4802
+ t("doctor.check.narrow_too_few.name"),
4803
+ t("doctor.check.narrow_too_few.ok", {
4804
+ ratioPct,
4805
+ narrowCount: String(inspection.narrow_with_paths_count),
4806
+ totalCount: String(inspection.total_canonical_entries),
4807
+ teleNote
4808
+ })
4558
4809
  );
4559
4810
  }
4560
4811
  const parts = [];
4561
4812
  if (structural_flagged) {
4562
4813
  const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
4563
4814
  parts.push(
4564
- `narrow-with-paths share ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}) below ${(NARROW_RATIO_THRESHOLD * 100).toFixed(0)}% threshold`
4815
+ t("doctor.check.narrow_too_few.message.structural", {
4816
+ ratioPct,
4817
+ narrowCount: String(inspection.narrow_with_paths_count),
4818
+ totalCount: String(inspection.total_canonical_entries),
4819
+ thresholdPct: (NARROW_RATIO_THRESHOLD * 100).toFixed(0)
4820
+ })
4565
4821
  );
4566
4822
  }
4567
4823
  if (telemetry_flagged) {
4568
4824
  const silencePct = (inspection.silence_rate * 100).toFixed(0);
4569
4825
  parts.push(
4570
- `narrow-hook silence rate ${silencePct}% (${inspection.silence_fires_in_window}/${inspection.total_edit_fires_in_window}) over ${SILENCE_WINDOW_DAYS}d above ${(SILENCE_RATE_THRESHOLD * 100).toFixed(0)}% threshold`
4826
+ t("doctor.check.narrow_too_few.message.telemetry", {
4827
+ silencePct,
4828
+ silenceFires: String(inspection.silence_fires_in_window),
4829
+ totalFires: String(inspection.total_edit_fires_in_window),
4830
+ windowDays: String(SILENCE_WINDOW_DAYS),
4831
+ thresholdPct: (SILENCE_RATE_THRESHOLD * 100).toFixed(0)
4832
+ })
4571
4833
  );
4572
4834
  }
4573
4835
  return issueCheck(
4574
- "Knowledge narrow too few",
4836
+ t("doctor.check.narrow_too_few.name"),
4575
4837
  "ok",
4576
4838
  "info",
4577
4839
  "knowledge_narrow_too_few",
4578
- `Narrow-scope KB coverage is below the useful floor: ${parts.join("; ")}.`,
4579
- "Run the fabric-import Skill (`/fabric-import`) to re-seed narrow anchors against the current codebase."
4840
+ t("doctor.check.narrow_too_few.message.summary", { parts: parts.join("; ") }),
4841
+ t("doctor.check.narrow_too_few.remediation")
4580
4842
  );
4581
4843
  }
4582
4844
  function resolvePersonalKnowledgeRoot() {
@@ -4713,58 +4975,70 @@ function inspectIndexDrift(projectRoot, meta) {
4713
4975
  );
4714
4976
  return { drifts };
4715
4977
  }
4716
- function createStableIdDuplicateCheck(inspection) {
4978
+ function createStableIdDuplicateCheck(t, inspection) {
4717
4979
  if (inspection.duplicates.length === 0) {
4718
4980
  return okCheck(
4719
- "Knowledge stable_id duplicate",
4720
- "No canonical knowledge files share a stable_id across team / personal trees."
4981
+ t("doctor.check.stable_id_duplicate.name"),
4982
+ t("doctor.check.stable_id_duplicate.ok")
4721
4983
  );
4722
4984
  }
4723
4985
  const first = inspection.duplicates[0];
4724
4986
  const detail = `${first.stable_id} appears in ${first.paths.length} files: ${first.paths.join(", ")}`;
4987
+ const count = inspection.duplicates.length;
4725
4988
  return issueCheck(
4726
- "Knowledge stable_id duplicate",
4989
+ t("doctor.check.stable_id_duplicate.name"),
4727
4990
  "error",
4728
4991
  "manual_error",
4729
4992
  "knowledge_stable_id_duplicate",
4730
- `${inspection.duplicates.length} stable_id${inspection.duplicates.length === 1 ? "" : "s"} duplicated across canonical knowledge files (path-decoupled identity invariant). First: ${detail}.`,
4731
- "Manually rename one of the colliding files to a fresh `<prefix>-<type>-<counter>--<slug>.md` allocated via the canonical id allocator; do not edit by hand."
4993
+ t(`doctor.check.stable_id_duplicate.message.${count === 1 ? "singular" : "plural"}`, {
4994
+ count: String(count),
4995
+ detail
4996
+ }),
4997
+ t("doctor.check.stable_id_duplicate.remediation")
4732
4998
  );
4733
4999
  }
4734
- function createLayerMismatchCheck(inspection) {
5000
+ function createLayerMismatchCheck(t, inspection) {
4735
5001
  if (inspection.mismatches.length === 0) {
4736
5002
  return okCheck(
4737
- "Knowledge layer mismatch",
4738
- "All canonical knowledge files are physically located under the layer their stable_id prefix declares."
5003
+ t("doctor.check.layer_mismatch.name"),
5004
+ t("doctor.check.layer_mismatch.ok")
4739
5005
  );
4740
5006
  }
4741
5007
  const first = inspection.mismatches[0];
4742
5008
  const detail = `${first.stable_id} at ${first.path} (located in ${first.located_in}, expected ${first.expected_layer})`;
5009
+ const count = inspection.mismatches.length;
4743
5010
  return issueCheck(
4744
- "Knowledge layer mismatch",
5011
+ t("doctor.check.layer_mismatch.name"),
4745
5012
  "error",
4746
5013
  "manual_error",
4747
5014
  "knowledge_layer_mismatch",
4748
- `${inspection.mismatches.length} canonical knowledge file${inspection.mismatches.length === 1 ? "" : "s"} are physically misaligned with their stable_id layer prefix (KT-* must live under team/, KP-* under personal/). First: ${detail}.`,
4749
- "Move the file to the correct layer root, or use the fabric-review modify flow to flip its layer (which renames the stable_id prefix accordingly)."
5015
+ t(`doctor.check.layer_mismatch.message.${count === 1 ? "singular" : "plural"}`, {
5016
+ count: String(count),
5017
+ detail
5018
+ }),
5019
+ t("doctor.check.layer_mismatch.remediation")
4750
5020
  );
4751
5021
  }
4752
- function createIndexDriftCheck(inspection) {
5022
+ function createIndexDriftCheck(t, inspection) {
4753
5023
  if (inspection.drifts.length === 0) {
4754
5024
  return okCheck(
4755
- "Knowledge index drift",
4756
- "agents.meta.json counters envelope is at or above the highest existing canonical counter for every (layer, type) pair."
5025
+ t("doctor.check.index_drift.name"),
5026
+ t("doctor.check.index_drift.ok")
4757
5027
  );
4758
5028
  }
4759
5029
  const first = inspection.drifts[0];
4760
5030
  const detail = `${first.layer}.${first.type} counter=${first.counter} but max_observed=${first.max_observed} (would propose counters.${first.layer}.${first.type}=${first.proposed_after})`;
5031
+ const count = inspection.drifts.length;
4761
5032
  return issueCheck(
4762
- "Knowledge index drift",
5033
+ t("doctor.check.index_drift.name"),
4763
5034
  "error",
4764
5035
  "fixable_error",
4765
5036
  "knowledge_index_drift",
4766
- `${inspection.drifts.length} (layer, type) counter slot${inspection.drifts.length === 1 ? "" : "s"} have drifted below the observed canonical maximum (next allocate would collide). First: ${detail}.`,
4767
- "Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
5037
+ t(`doctor.check.index_drift.message.${count === 1 ? "singular" : "plural"}`, {
5038
+ count: String(count),
5039
+ detail
5040
+ }),
5041
+ t("doctor.check.index_drift.remediation")
4768
5042
  );
4769
5043
  }
4770
5044
  async function migrateBootstrapMarkers(projectRoot) {
@@ -5137,7 +5411,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5137
5411
  break;
5138
5412
  }
5139
5413
  }
5140
- const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t2) => t2.client === options.client);
5414
+ const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
5141
5415
  let clientSessionIds = null;
5142
5416
  if (options.client !== "all") {
5143
5417
  clientSessionIds = /* @__PURE__ */ new Set();