@fenglimg/fabric-cli 2.0.0 → 2.0.1

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -5
  3. package/dist/chunk-BATF4PEJ.js +361 -0
  4. package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
  5. package/dist/chunk-D25XJ4BC.js +880 -0
  6. package/dist/chunk-MF3OTILQ.js +544 -0
  7. package/dist/chunk-PWLW3B57.js +18 -0
  8. package/dist/config-XJIPZNUP.js +13 -0
  9. package/dist/doctor-EJDSEJSS.js +810 -0
  10. package/dist/index.js +15 -8
  11. package/dist/{init-BIRSIOXO.js → install-EKWMFLUU.js} +622 -711
  12. package/dist/metrics-ACEQFPDU.js +122 -0
  13. package/dist/onboard-coverage-MFCAEBDO.js +220 -0
  14. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
  15. package/dist/uninstall-MH7ZIB6M.js +1064 -0
  16. package/package.json +30 -5
  17. package/templates/hooks/cite-policy-evict.cjs +231 -0
  18. package/templates/hooks/configs/README.md +29 -6
  19. package/templates/hooks/configs/claude-code.json +14 -3
  20. package/templates/hooks/configs/codex-hooks.json +6 -3
  21. package/templates/hooks/configs/cursor-hooks.json +8 -10
  22. package/templates/hooks/fabric-hint.cjs +833 -105
  23. package/templates/hooks/knowledge-hint-broad.cjs +509 -135
  24. package/templates/hooks/knowledge-hint-narrow.cjs +791 -26
  25. package/templates/hooks/lib/banner-i18n.cjs +309 -0
  26. package/templates/hooks/lib/cite-contract-reminder.cjs +173 -0
  27. package/templates/hooks/lib/cite-line-parser.cjs +158 -0
  28. package/templates/hooks/lib/client-adapter.cjs +106 -0
  29. package/templates/hooks/lib/config-cache.cjs +107 -0
  30. package/templates/hooks/lib/state-store.cjs +84 -0
  31. package/templates/hooks/lib/summary-fallback.cjs +210 -0
  32. package/templates/skills/fabric-archive/SKILL.md +93 -419
  33. package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
  34. package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
  35. package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
  36. package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
  37. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
  38. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
  39. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
  40. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
  41. package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
  42. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
  43. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
  44. package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
  45. package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
  46. package/templates/skills/fabric-import/SKILL.md +75 -516
  47. package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
  48. package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
  49. package/templates/skills/fabric-import/ref/output-contract.md +61 -0
  50. package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
  51. package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
  52. package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
  53. package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
  54. package/templates/skills/fabric-review/SKILL.md +86 -284
  55. package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
  56. package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
  57. package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
  58. package/templates/skills/fabric-review/ref/output-contract.md +58 -0
  59. package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
  60. package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
  61. package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
  62. package/templates/skills/lib/shared-policy.md +69 -0
  63. package/dist/chunk-6ICJICVU.js +0 -10
  64. package/dist/chunk-74SZWYPH.js +0 -658
  65. package/dist/chunk-EYIDD2YS.js +0 -1000
  66. package/dist/doctor-T7JWODKG.js +0 -282
  67. package/dist/hooks-Y74Y5LQS.js +0 -12
  68. package/dist/scan-LMK3UCWL.js +0 -22
  69. package/dist/serve-H554BHLG.js +0 -124
  70. package/templates/agents-md/AGENTS.md.template +0 -59
  71. package/templates/bootstrap/CLAUDE.md +0 -8
  72. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  73. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -0,0 +1,810 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ paint,
4
+ symbol
5
+ } from "./chunk-WWNXR34K.js";
6
+ import {
7
+ resolveDevMode
8
+ } from "./chunk-COI5VDFU.js";
9
+ import {
10
+ getDoctorTranslator,
11
+ t
12
+ } from "./chunk-PWLW3B57.js";
13
+
14
+ // src/commands/doctor.ts
15
+ import { confirm, isCancel } from "@clack/prompts";
16
+ import { defineCommand } from "citty";
17
+ import {
18
+ appendEventLedgerEvent,
19
+ enrichDescriptions,
20
+ runDoctorApplyLint as runDoctorFixKnowledge,
21
+ runDoctorArchiveHistory,
22
+ runDoctorCiteCoverage,
23
+ runDoctorFix,
24
+ runDoctorHistoryAll,
25
+ runDoctorReport
26
+ } from "@fenglimg/fabric-server";
27
+ var FIX_KNOWLEDGE_CODE_LABELS = {
28
+ knowledge_orphan_demote_required: "demote (maturity)",
29
+ knowledge_stale_archive_required: "archive (git mv)",
30
+ knowledge_pending_auto_archive: "archive (git mv, pending)",
31
+ knowledge_index_drift: "counter bump (agents.meta)",
32
+ knowledge_session_hints_stale: "cache delete"
33
+ };
34
+ var PLAN_PREVIEW_LIMIT = 12;
35
+ var doctorCommand = defineCommand({
36
+ meta: {
37
+ name: "doctor",
38
+ description: t("cli.doctor.description")
39
+ },
40
+ args: {
41
+ target: {
42
+ type: "string",
43
+ description: t("cli.doctor.args.target.description")
44
+ },
45
+ fix: {
46
+ type: "boolean",
47
+ description: t("cli.doctor.args.fix.description"),
48
+ default: false
49
+ },
50
+ "fix-knowledge": {
51
+ type: "boolean",
52
+ description: t("cli.doctor.args.fix-knowledge.description"),
53
+ default: false
54
+ },
55
+ json: {
56
+ type: "boolean",
57
+ description: t("cli.doctor.args.json.description"),
58
+ default: false
59
+ },
60
+ strict: {
61
+ type: "boolean",
62
+ description: t("cli.doctor.args.strict.description"),
63
+ default: false
64
+ },
65
+ // rc.7 T11: skip the safety confirm before mutations. Required for any
66
+ // non-tty invocation that wants to run --fix-knowledge without setting
67
+ // FABRIC_NONINTERACTIVE=1 in the environment.
68
+ yes: {
69
+ type: "boolean",
70
+ description: t("cli.doctor.args.yes.description"),
71
+ default: false
72
+ },
73
+ // rc.35 TASK-12 (P0-11): expose maintainer-audience actionHints. By
74
+ // default the renderer folds remediation strings that target Fabric
75
+ // contributors (edit `packages/cli/templates/...`, interpret G1-G5
76
+ // cite-goodhart codes, etc.) since npm end users have no actionable
77
+ // lever for them. --verbose shows them.
78
+ verbose: {
79
+ type: "boolean",
80
+ description: t("cli.doctor.args.verbose.description"),
81
+ default: false
82
+ },
83
+ // rc.20 TASK-05: cite policy adherence report (read-only). Skips standard
84
+ // inspections entirely — different output surface. Mutually exclusive
85
+ // with --fix / --fix-knowledge (enforced in run()).
86
+ "cite-coverage": {
87
+ type: "boolean",
88
+ description: t("cli.doctor.args.cite-coverage.description"),
89
+ default: false
90
+ },
91
+ since: {
92
+ type: "string",
93
+ description: t("cli.doctor.args.since.description"),
94
+ default: "7d"
95
+ },
96
+ client: {
97
+ type: "string",
98
+ description: t("cli.doctor.args.client.description"),
99
+ default: "all",
100
+ valueHint: "cc|codex|cursor|all"
101
+ },
102
+ // v2.0.0-rc.24 TASK-10: --layer filter for the cite contract audit. Pairs
103
+ // with --cite-coverage. Validated against {'team','personal','all'} at
104
+ // command entry; rejects 'both' (rc.20 plan-context vocabulary) explicitly.
105
+ layer: {
106
+ type: "string",
107
+ description: t("cli.doctor.args.layer.description"),
108
+ default: "all",
109
+ valueHint: "team|personal|all"
110
+ },
111
+ // rc.23 TASK-007 (a-C2): description-grade back-fill flag set. Read-side
112
+ // by default; `--auto` flips the writer arm on. Mutually exclusive with
113
+ // --fix / --fix-knowledge / --cite-coverage (different mutation surfaces).
114
+ "enrich-descriptions": {
115
+ type: "boolean",
116
+ description: t("cli.doctor.args.enrich-descriptions.description"),
117
+ default: false
118
+ },
119
+ auto: {
120
+ type: "boolean",
121
+ description: t("cli.doctor.args.auto.description"),
122
+ default: false
123
+ },
124
+ "dry-run": {
125
+ type: "boolean",
126
+ description: t("cli.doctor.args.dry-run.description"),
127
+ default: false
128
+ },
129
+ // v2.0.0-rc.25 TASK-10: --archive-history flag (parallel to rc.20
130
+ // --cite-coverage). Read-only; reads session_archive_attempted events
131
+ // and renders a per-session table. Pairs with the shared `--since` flag.
132
+ "archive-history": {
133
+ type: "boolean",
134
+ description: t("cli.doctor.args.archive-history.description"),
135
+ default: false
136
+ },
137
+ // rc.37 NEW-33: unified history view across archive / fix / all surfaces.
138
+ // Mode = `archive | fix | all` (the `archive` mode delegates to the
139
+ // existing runDoctorArchiveHistory; `fix` aggregates doctor_run events;
140
+ // `all` rolls up both into a per-day count table). Read-only; mutex
141
+ // with the mutation arms.
142
+ history: {
143
+ type: "string",
144
+ description: t("cli.doctor.args.history.description"),
145
+ valueHint: "archive|fix|all"
146
+ }
147
+ },
148
+ async run({ args }) {
149
+ const workspaceRoot = process.cwd();
150
+ const resolution = resolveDevMode(args.target, workspaceRoot);
151
+ const dt = getDoctorTranslator(resolution.target);
152
+ const fixKnowledge = args["fix-knowledge"] === true;
153
+ const fix = args.fix === true;
154
+ const citeCoverage = args["cite-coverage"] === true;
155
+ const enrichDesc = args["enrich-descriptions"] === true;
156
+ const archiveHistory = args["archive-history"] === true;
157
+ if (args.since !== void 0) {
158
+ try {
159
+ parseSinceDuration(args.since);
160
+ } catch {
161
+ writeStderr(dt("cli.doctor.errors.invalid-since", { input: args.since }));
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+ }
166
+ const historyMode = args.history;
167
+ if (typeof historyMode === "string" && historyMode.length > 0) {
168
+ if (fix || fixKnowledge || citeCoverage || enrichDesc || archiveHistory) {
169
+ writeStderr(dt("cli.doctor.errors.history-mutex"));
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ if (historyMode !== "archive" && historyMode !== "fix" && historyMode !== "all") {
174
+ writeStderr(dt("cli.doctor.errors.invalid-history-mode", { input: historyMode }));
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+ const sinceInput = args.since ?? "7d";
179
+ let sinceMs;
180
+ try {
181
+ sinceMs = parseSinceDuration(sinceInput);
182
+ } catch {
183
+ writeStderr(dt("cli.doctor.errors.invalid-since", { input: sinceInput }));
184
+ process.exitCode = 1;
185
+ return;
186
+ }
187
+ if (historyMode === "archive") {
188
+ const report3 = await runDoctorArchiveHistory(resolution.target, { since: sinceMs });
189
+ if (args.json === true) {
190
+ writeStdout(JSON.stringify(report3, null, 2));
191
+ } else {
192
+ renderArchiveHistoryReport(report3, sinceInput, dt);
193
+ }
194
+ return;
195
+ }
196
+ const report2 = await runDoctorHistoryAll(resolution.target, { since: sinceMs });
197
+ if (args.json === true) {
198
+ writeStdout(JSON.stringify(report2, null, 2));
199
+ } else {
200
+ renderHistoryAllReport(report2, sinceInput, historyMode, dt);
201
+ }
202
+ return;
203
+ }
204
+ if (archiveHistory) {
205
+ if (fix || fixKnowledge || citeCoverage || enrichDesc) {
206
+ writeStderr(dt("cli.doctor.errors.archive-history-mutex"));
207
+ process.exitCode = 1;
208
+ return;
209
+ }
210
+ const sinceInput = args.since ?? "7d";
211
+ let sinceMs;
212
+ try {
213
+ sinceMs = parseSinceDuration(sinceInput);
214
+ } catch {
215
+ writeStderr(dt("cli.doctor.errors.invalid-since", { input: sinceInput }));
216
+ process.exitCode = 1;
217
+ return;
218
+ }
219
+ const report2 = await runDoctorArchiveHistory(resolution.target, {
220
+ since: sinceMs
221
+ });
222
+ if (args.json === true) {
223
+ writeStdout(JSON.stringify(report2, null, 2));
224
+ } else {
225
+ renderArchiveHistoryReport(report2, sinceInput, dt);
226
+ }
227
+ return;
228
+ }
229
+ if (enrichDesc) {
230
+ if (fix || fixKnowledge || citeCoverage) {
231
+ writeStderr(dt("cli.doctor.errors.enrich-descriptions-mutex"));
232
+ process.exitCode = 1;
233
+ return;
234
+ }
235
+ const autoFlag = args.auto === true;
236
+ const dryRun = args["dry-run"] === true;
237
+ const report2 = await enrichDescriptions(resolution.target, {
238
+ auto: autoFlag,
239
+ dryRun
240
+ });
241
+ if (args.json === true) {
242
+ writeStdout(JSON.stringify(report2, null, 2));
243
+ } else {
244
+ renderEnrichDescriptionsReport(report2, dt);
245
+ }
246
+ return;
247
+ }
248
+ if (citeCoverage) {
249
+ if (fix || fixKnowledge) {
250
+ writeStderr(dt("cli.doctor.errors.cite-coverage-mutex"));
251
+ process.exitCode = 1;
252
+ return;
253
+ }
254
+ let sinceMs;
255
+ try {
256
+ sinceMs = parseSinceDuration(args.since ?? "7d");
257
+ } catch {
258
+ writeStderr(dt("cli.doctor.errors.invalid-since", { input: args.since ?? "7d" }));
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+ const clientFilter = args.client ?? "all";
263
+ if (!isValidClientFilter(clientFilter)) {
264
+ writeStderr(dt("cli.doctor.errors.invalid-client", { input: clientFilter }));
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+ const layerFilter = args.layer ?? "all";
269
+ if (!isValidLayerFilter(layerFilter)) {
270
+ writeStderr(dt("cli.doctor.errors.invalid-layer", { input: layerFilter }));
271
+ process.exitCode = 1;
272
+ return;
273
+ }
274
+ const report2 = await runDoctorCiteCoverage(resolution.target, {
275
+ since: sinceMs,
276
+ client: clientFilter,
277
+ layer: layerFilter
278
+ });
279
+ renderCiteCoverageReport(report2, args.json === true, dt);
280
+ return;
281
+ }
282
+ if (fixKnowledge && fix) {
283
+ writeStderr(dt("cli.doctor.errors.fix-knowledge-fix-mutually-exclusive"));
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+ let fixKnowledgeReport = null;
288
+ let fixReport = null;
289
+ let report;
290
+ if (fixKnowledge) {
291
+ const preReport = await runDoctorReport(resolution.target);
292
+ const plan = computeFixKnowledgePlan(preReport);
293
+ const yesFlag = args.yes === true;
294
+ const envBypass = process.env.FABRIC_NONINTERACTIVE === "1";
295
+ if (plan.totalCount === 0) {
296
+ } else {
297
+ renderFixKnowledgePlan(plan);
298
+ const decision = await resolveFixKnowledgeConsent({
299
+ yesFlag,
300
+ envBypass,
301
+ plan
302
+ });
303
+ if (decision === "abort") {
304
+ process.exitCode = 1;
305
+ return;
306
+ }
307
+ }
308
+ fixKnowledgeReport = await runDoctorFixKnowledge(resolution.target);
309
+ report = fixKnowledgeReport.report;
310
+ } else if (fix) {
311
+ if (args["dry-run"] === true) {
312
+ report = await runDoctorReport(resolution.target);
313
+ } else {
314
+ fixReport = await runDoctorFix(resolution.target);
315
+ report = fixReport.report;
316
+ }
317
+ } else {
318
+ report = await runDoctorReport(resolution.target);
319
+ }
320
+ if (args.json === true) {
321
+ writeStdout(JSON.stringify(fixKnowledgeReport ?? fixReport ?? report, null, 2));
322
+ } else {
323
+ if (fixKnowledgeReport !== null) {
324
+ writeStdout(fixKnowledgeReport.message);
325
+ if (fixKnowledgeReport.aborted && fixKnowledgeReport.abort_reason !== void 0) {
326
+ writeStderr(fixKnowledgeReport.abort_reason);
327
+ }
328
+ renderFixKnowledgeMutations(fixKnowledgeReport, dt);
329
+ } else if (fixReport !== null) {
330
+ writeStdout(fixReport.message);
331
+ } else if (fix && args["dry-run"] === true) {
332
+ writeStdout(dt("cli.doctor.fix-dry-run-banner"));
333
+ }
334
+ renderHumanReport(report, dt, args.verbose === true);
335
+ }
336
+ await emitDoctorRunEventBestEffort(resolution.target, {
337
+ mode: fixKnowledge ? "fix-knowledge" : "lint",
338
+ issues: report.fixable_errors.length + report.manual_errors.length + report.warnings.length,
339
+ mutations: fixKnowledgeReport !== null ? fixKnowledgeReport.mutations.filter((m) => m.applied).length : void 0
340
+ });
341
+ if (fixKnowledgeReport !== null) {
342
+ if (fixKnowledgeReport.aborted) {
343
+ process.exitCode = 1;
344
+ return;
345
+ }
346
+ if (fixKnowledgeReport.mutations.some((m) => !m.applied)) {
347
+ process.exitCode = 1;
348
+ return;
349
+ }
350
+ }
351
+ if (report.status === "error" || args.strict === true && (report.status === "warn" || report.warnings.length > 0)) {
352
+ process.exitCode = 1;
353
+ }
354
+ }
355
+ });
356
+ var doctor_default = doctorCommand;
357
+ function renderHumanReport(report, dt, verbose) {
358
+ writeStdout(`${renderStatus(report.status)} ${paint.ai("fabric doctor")} ${paint.human(report.summary.target)}`);
359
+ renderTldrHeader(report);
360
+ for (const check of report.checks) {
361
+ writeStdout(`${renderStatus(check.status)} ${check.name}: ${check.message}`);
362
+ }
363
+ const opts = { verbose, dt };
364
+ writeIssueSection(dt("doctor.section.fixable"), report.fixable_errors, opts);
365
+ writeIssueSection(dt("doctor.section.manual"), report.manual_errors, opts);
366
+ writeIssueSection(dt("doctor.section.warnings"), report.warnings, opts);
367
+ renderPayloadLimits(report, dt);
368
+ }
369
+ function renderPayloadLimits(report, dt) {
370
+ const limits = report.summary.payload_limits;
371
+ if (limits === void 0) {
372
+ return;
373
+ }
374
+ writeStdout("");
375
+ writeStdout(dt("doctor.section.payload-limits"));
376
+ writeStdout(
377
+ `- ${dt("doctor.payload-limits.line", {
378
+ warnKb: String(Math.round(limits.warn_bytes / 1024)),
379
+ hardKb: String(Math.round(limits.hard_bytes / 1024)),
380
+ source: limits.source
381
+ })}`
382
+ );
383
+ }
384
+ function renderFixKnowledgeMutations(fixKnowledgeReport, dt) {
385
+ if (fixKnowledgeReport.mutations.length === 0) {
386
+ return;
387
+ }
388
+ writeStdout("");
389
+ writeStdout(dt("doctor.section.fix-knowledge-mutations"));
390
+ for (const mutation of fixKnowledgeReport.mutations) {
391
+ const marker = mutation.applied ? symbol.ok : symbol.error;
392
+ const errSuffix = mutation.applied || mutation.error === void 0 ? "" : ` (${mutation.error})`;
393
+ writeStdout(`${marker} ${mutation.kind}: ${mutation.path} [${mutation.detail}]${errSuffix}`);
394
+ }
395
+ }
396
+ function writeIssueSection(title, issues, options) {
397
+ if (issues.length === 0) {
398
+ return;
399
+ }
400
+ writeStdout("");
401
+ writeStdout(title);
402
+ for (const issue of issues) {
403
+ writeStdout(`- ${issue.code}: ${issue.message}`);
404
+ if (issue.actionHint !== void 0 && issue.actionHint.length > 0) {
405
+ if (issue.audience === "maintainer" && !options.verbose) {
406
+ writeStdout(` \u2192 ${options.dt("doctor.maintainer-hint-folded")}`);
407
+ } else {
408
+ writeStdout(` \u2192 ${issue.actionHint}`);
409
+ }
410
+ }
411
+ }
412
+ }
413
+ function renderTldrHeader(report) {
414
+ const ranked = [];
415
+ for (const issue of report.fixable_errors) {
416
+ ranked.push({ severity: "fixable", code: issue.code, message: issue.message });
417
+ }
418
+ for (const issue of report.manual_errors) {
419
+ ranked.push({ severity: "manual", code: issue.code, message: issue.message });
420
+ }
421
+ for (const issue of report.warnings) {
422
+ ranked.push({ severity: "warn", code: issue.code, message: issue.message });
423
+ }
424
+ if (ranked.length === 0) {
425
+ writeStdout(`${symbol.ok} TL;DR: all 48 checks green \u2014 nothing to fix.`);
426
+ return;
427
+ }
428
+ const top3 = ranked.slice(0, 3);
429
+ writeStdout(
430
+ `TL;DR (top ${top3.length} of ${ranked.length}, severity order: fixable\u2192manual\u2192warn):`
431
+ );
432
+ for (const item of top3) {
433
+ const marker = item.severity === "fixable" ? symbol.error : item.severity === "manual" ? symbol.error : symbol.warn;
434
+ const truncated = item.message.length > 140 ? `${item.message.slice(0, 137)}...` : item.message;
435
+ writeStdout(` ${marker} ${item.code}: ${truncated}`);
436
+ }
437
+ }
438
+ function renderStatus(status) {
439
+ if (status === "ok") {
440
+ return symbol.ok;
441
+ }
442
+ if (status === "warn") {
443
+ return symbol.warn;
444
+ }
445
+ return symbol.error;
446
+ }
447
+ function writeStdout(message) {
448
+ process.stdout.write(`${message}
449
+ `);
450
+ }
451
+ async function emitDoctorRunEventBestEffort(projectRoot, payload) {
452
+ try {
453
+ await appendEventLedgerEvent(projectRoot, {
454
+ event_type: "doctor_run",
455
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
456
+ mode: payload.mode,
457
+ issues: payload.issues,
458
+ ...payload.mutations !== void 0 ? { mutations: payload.mutations } : {}
459
+ });
460
+ } catch {
461
+ }
462
+ }
463
+ function writeStderr(message) {
464
+ process.stderr.write(`${message}
465
+ `);
466
+ }
467
+ function computeFixKnowledgePlan(report) {
468
+ const buckets = {};
469
+ const sources = [
470
+ ...report.fixable_errors,
471
+ ...report.warnings
472
+ ];
473
+ for (const issue of sources) {
474
+ if (FIX_KNOWLEDGE_CODE_LABELS[issue.code] === void 0) continue;
475
+ if (!Array.isArray(buckets[issue.code])) {
476
+ buckets[issue.code] = [];
477
+ }
478
+ buckets[issue.code].push(issue);
479
+ }
480
+ const codes = Object.keys(buckets).sort(
481
+ (a, b) => FIX_KNOWLEDGE_CODE_LABELS[a].localeCompare(FIX_KNOWLEDGE_CODE_LABELS[b])
482
+ );
483
+ const perCodeLines = [];
484
+ let totalCount = 0;
485
+ for (const code of codes) {
486
+ const items = buckets[code];
487
+ totalCount += items.length;
488
+ perCodeLines.push(` - ${FIX_KNOWLEDGE_CODE_LABELS[code]}: ${items.length}`);
489
+ }
490
+ const previewLines = [];
491
+ const flattened = codes.flatMap((c) => buckets[c]);
492
+ for (const item of flattened.slice(0, PLAN_PREVIEW_LIMIT)) {
493
+ const where = item.path !== void 0 && item.path.length > 0 ? `${item.path}` : "(no path)";
494
+ previewLines.push(` \u2022 ${where} \u2014 ${item.message}`);
495
+ }
496
+ if (flattened.length > PLAN_PREVIEW_LIMIT) {
497
+ previewLines.push(` \u2022 ... and ${flattened.length - PLAN_PREVIEW_LIMIT} more`);
498
+ }
499
+ return { totalCount, perCodeLines, previewLines };
500
+ }
501
+ function renderFixKnowledgePlan(plan) {
502
+ writeStdout("");
503
+ writeStdout(`${paint.warn("fix-knowledge mutation plan")} (${plan.totalCount} total)`);
504
+ for (const line of plan.perCodeLines) {
505
+ writeStdout(line);
506
+ }
507
+ if (plan.previewLines.length > 0) {
508
+ writeStdout("");
509
+ writeStdout(" preview:");
510
+ for (const line of plan.previewLines) {
511
+ writeStdout(line);
512
+ }
513
+ }
514
+ }
515
+ async function resolveFixKnowledgeConsent(options) {
516
+ if (options.yesFlag || options.envBypass) {
517
+ return "proceed";
518
+ }
519
+ if (process.stdin.isTTY !== true) {
520
+ writeStderr(
521
+ "doctor --fix-knowledge: stdin is not a TTY and neither --yes nor FABRIC_NONINTERACTIVE=1 is set. Refusing to mutate."
522
+ );
523
+ return "abort";
524
+ }
525
+ const message = `About to apply ${options.plan.totalCount} mutation(s) to knowledge entries (frontmatter writes + git mv + cache deletes). Proceed?`;
526
+ const answer = await confirm({
527
+ message,
528
+ initialValue: false
529
+ });
530
+ if (isCancel(answer) || answer !== true) {
531
+ writeStderr("doctor --fix-knowledge: aborted by user.");
532
+ return "abort";
533
+ }
534
+ return "proceed";
535
+ }
536
+ var CITE_COVERAGE_CLIENT_FILTERS = /* @__PURE__ */ new Set([
537
+ "cc",
538
+ "codex",
539
+ "cursor",
540
+ "all"
541
+ ]);
542
+ function isValidClientFilter(input) {
543
+ return CITE_COVERAGE_CLIENT_FILTERS.has(input);
544
+ }
545
+ var CITE_COVERAGE_LAYER_FILTERS = /* @__PURE__ */ new Set([
546
+ "team",
547
+ "personal",
548
+ "all"
549
+ ]);
550
+ function isValidLayerFilter(input) {
551
+ return CITE_COVERAGE_LAYER_FILTERS.has(input);
552
+ }
553
+ function renderCiteCoverageReport(report, jsonMode, dt) {
554
+ if (jsonMode) {
555
+ writeStdout(JSON.stringify(report, null, 2));
556
+ return;
557
+ }
558
+ if (report.status === "skipped") {
559
+ writeStdout(dt("doctor.cite.status.skipped"));
560
+ return;
561
+ }
562
+ const lines = [];
563
+ lines.push(dt("doctor.section.cite-coverage"));
564
+ lines.push(
565
+ dt("doctor.cite.header", {
566
+ since: new Date(report.since_ts).toISOString(),
567
+ marker: new Date(report.marker_ts).toISOString()
568
+ })
569
+ );
570
+ if (report.marker_emitted_now) {
571
+ lines.push(dt("doctor.cite.warning.justActivated"));
572
+ }
573
+ lines.push("");
574
+ lines.push(` ${dt("doctor.cite.metric.editsTouched")}: ${report.metrics.edits_touched}`);
575
+ lines.push(` ${dt("doctor.cite.metric.qualifyingCites")}: ${report.metrics.qualifying_cites}`);
576
+ lines.push(` ${dt("doctor.cite.metric.recalledUnverified")}: ${report.metrics.recalled_unverified}`);
577
+ lines.push(` ${dt("doctor.cite.metric.expectedButMissed")}: ${report.metrics.expected_but_missed}`);
578
+ lines.push(` ${dt("doctor.cite.metric.totalTurns")}: ${report.metrics.total_turns}`);
579
+ const complianceRate = report.metrics.cite_compliance_rate;
580
+ const complianceStr = complianceRate === null || complianceRate === void 0 ? dt("doctor.cite.metric.complianceNA") : `${(complianceRate * 100).toFixed(1)}% (${report.metrics.compliant_cites ?? 0}/${(report.metrics.compliant_cites ?? 0) + (report.metrics.noncompliant_cites ?? 0)})`;
581
+ lines.push(` ${dt("doctor.cite.metric.complianceRate")}: ${complianceStr}`);
582
+ const uncorrelatable = report.metrics.uncorrelatable_edits ?? 0;
583
+ if (uncorrelatable > 0) {
584
+ lines.push(` ${dt("doctor.cite.metric.uncorrelatableEdits")}: ${uncorrelatable}`);
585
+ }
586
+ if (report.per_client !== void 0 && Object.keys(report.per_client).length > 1) {
587
+ lines.push("");
588
+ lines.push(`### ${dt("doctor.cite.section.perClient")}`);
589
+ for (const [client, metrics] of Object.entries(report.per_client)) {
590
+ const summary = Object.entries(metrics).map(([k, v]) => `${k}=${v}`).join(" / ");
591
+ lines.push(` ${client}: ${summary}`);
592
+ }
593
+ }
594
+ if (report.dismissed_reason_histogram !== void 0 && Object.keys(report.dismissed_reason_histogram).length > 0) {
595
+ lines.push("");
596
+ lines.push(`### ${dt("doctor.cite.section.dismissedReasons")}`);
597
+ for (const [reason, count] of Object.entries(report.dismissed_reason_histogram)) {
598
+ const label = dt(`doctor.cite.dismissed.${reason}`);
599
+ lines.push(` ${label}: ${count}`);
600
+ }
601
+ }
602
+ if (report.none_reason_histogram !== void 0 && Object.keys(report.none_reason_histogram).length > 0) {
603
+ lines.push("");
604
+ lines.push(`### ${dt("doctor.cite.section.noneReasons")}`);
605
+ for (const [reason, count] of Object.entries(report.none_reason_histogram)) {
606
+ const label = dt(`doctor.cite.none.${reason}`);
607
+ lines.push(` ${label}: ${count}`);
608
+ }
609
+ }
610
+ appendContractSection(lines, report, dt);
611
+ writeStdout(lines.join("\n"));
612
+ }
613
+ function appendContractSection(lines, report, dt) {
614
+ const status = report.contract_metrics_status;
615
+ if (status === void 0) {
616
+ return;
617
+ }
618
+ const metrics = report.contract_metrics;
619
+ const perLayerType = report.per_layer_type;
620
+ const allCountsZero = metrics === void 0 || metrics.decisions_cited === 0 && metrics.pitfalls_cited === 0 && metrics.contract_with === 0 && metrics.contract_missing === 0 && metrics.hard_violated === 0 && metrics.cite_id_unresolved === 0 && Object.keys(metrics.skip_count).length === 0;
621
+ if (status === "awaiting_marker" && allCountsZero) {
622
+ return;
623
+ }
624
+ lines.push("");
625
+ lines.push(`### ${dt("cite-coverage.contract.header")}`);
626
+ if (status === "skipped:bootstrap_drift") {
627
+ lines.push(` ${dt("cite-coverage.contract.status.skipped_bootstrap_drift")}`);
628
+ return;
629
+ }
630
+ const statusKey = status === "ok" ? "cite-coverage.contract.status.ok" : "cite-coverage.contract.status.awaiting_marker";
631
+ lines.push(` status: ${dt(statusKey)}`);
632
+ if (typeof report.contract_marker_ts === "number" && report.contract_marker_ts > 0) {
633
+ lines.push(` since: ${new Date(report.contract_marker_ts).toISOString()}`);
634
+ }
635
+ if (report.layer_filter !== void 0) {
636
+ lines.push(` layer filter: ${report.layer_filter}`);
637
+ }
638
+ if (metrics !== void 0) {
639
+ lines.push(` ${dt("cite-coverage.contract.decisions_cited")}: ${metrics.decisions_cited}`);
640
+ lines.push(` ${dt("cite-coverage.contract.pitfalls_cited")}: ${metrics.pitfalls_cited}`);
641
+ lines.push(` ${dt("cite-coverage.contract.with")}: ${metrics.contract_with}`);
642
+ lines.push(` ${dt("cite-coverage.contract.missing")}: ${metrics.contract_missing}`);
643
+ if (metrics.hard_violated > 0) {
644
+ const layerSuffix = report.layer_filter === "personal" ? dt("cite-coverage.layer.personal_fyi") : dt("cite-coverage.layer.team_review");
645
+ lines.push(
646
+ ` ${dt("cite-coverage.contract.hard_violated")} ${layerSuffix}: ${metrics.hard_violated}`
647
+ );
648
+ }
649
+ }
650
+ if (perLayerType !== void 0) {
651
+ const teamKeys = Object.keys(perLayerType.team).filter(
652
+ (k) => perLayerType.team[k] > 0
653
+ );
654
+ const personalKeys = Object.keys(perLayerType.personal).filter(
655
+ (k) => perLayerType.personal[k] > 0
656
+ );
657
+ if (teamKeys.length > 0 || personalKeys.length > 0) {
658
+ lines.push("");
659
+ lines.push(`#### ${dt("cite-coverage.layer.team")} \xD7 ${dt("cite-coverage.layer.personal")}`);
660
+ for (const key of teamKeys) {
661
+ const label = dt(`cite-coverage.contract.type.${key}`);
662
+ lines.push(` ${dt("cite-coverage.layer.team")} \u2014 ${label}: ${perLayerType.team[key]}`);
663
+ }
664
+ for (const key of personalKeys) {
665
+ const label = dt(`cite-coverage.contract.type.${key}`);
666
+ lines.push(
667
+ ` ${dt("cite-coverage.layer.personal")} \u2014 ${label}: ${perLayerType.personal[key]}`
668
+ );
669
+ }
670
+ }
671
+ }
672
+ if (metrics !== void 0 && Object.keys(metrics.skip_count).length > 0) {
673
+ lines.push("");
674
+ lines.push(`#### ${dt("cite-coverage.contract.skip_count")}`);
675
+ for (const [reason, count] of Object.entries(metrics.skip_count)) {
676
+ const label = dt(`cite-coverage.skip.${reason}`);
677
+ lines.push(` ${label}: ${count}`);
678
+ }
679
+ }
680
+ if (metrics !== void 0 && metrics.cite_id_unresolved > 0) {
681
+ lines.push("");
682
+ lines.push(
683
+ `${symbol.warn} ${dt("cite-coverage.contract.cite_id_unresolved")}: ${metrics.cite_id_unresolved}`
684
+ );
685
+ }
686
+ }
687
+ function renderEnrichDescriptionsReport(report, dt) {
688
+ const header = `${symbol.ok} ${paint.ai("fabric doctor --enrich-descriptions")} mode=${report.mode}${report.dryRun ? " (dry-run)" : ""} scanned=${report.scanned} modified=${report.modified} skipped=${report.skipped}`;
689
+ writeStdout(header);
690
+ if (report.candidates.length === 0) {
691
+ writeStdout(dt("doctor.enrich.allComplete"));
692
+ return;
693
+ }
694
+ writeStdout("");
695
+ for (const candidate of report.candidates) {
696
+ if (candidate.error !== void 0) {
697
+ writeStdout(`${symbol.error} ${candidate.path} \u2014 ${candidate.error}`);
698
+ continue;
699
+ }
700
+ const missing = candidate.missing.join(", ");
701
+ if (candidate.modified) {
702
+ const added = candidate.added_fields.join(", ");
703
+ writeStdout(
704
+ `${symbol.ok} ${candidate.path} \u2014 missing: ${missing} \u2192 added: ${added}`
705
+ );
706
+ } else {
707
+ writeStdout(`${symbol.warn} ${candidate.path} \u2014 missing: ${missing}`);
708
+ }
709
+ }
710
+ }
711
+ function parseSinceDuration(input) {
712
+ const trimmed = input.trim();
713
+ if (trimmed.length === 0) {
714
+ throw new Error(`invalid --since value: ${input}`);
715
+ }
716
+ const durationMatch = /^(\d+)([dhm])$/.exec(trimmed);
717
+ if (durationMatch !== null) {
718
+ const value = Number.parseInt(durationMatch[1], 10);
719
+ const unit = durationMatch[2];
720
+ if (!Number.isFinite(value) || value <= 0) {
721
+ throw new Error(`invalid --since value: ${input}`);
722
+ }
723
+ const unitMs = unit === "d" ? 864e5 : unit === "h" ? 36e5 : 6e4;
724
+ return Date.now() - value * unitMs;
725
+ }
726
+ if (/^\d+$/.test(trimmed)) {
727
+ const value = Number.parseInt(trimmed, 10);
728
+ if (!Number.isFinite(value) || value < 0) {
729
+ throw new Error(`invalid --since value: ${input}`);
730
+ }
731
+ return value;
732
+ }
733
+ throw new Error(`invalid --since value: ${input}`);
734
+ }
735
+ function renderArchiveHistoryReport(report, sinceLabel, dt) {
736
+ if (report.entries.length === 0) {
737
+ writeStdout(dt("doctor.archive-history.empty", { sinceLabel }));
738
+ return;
739
+ }
740
+ const lines = [];
741
+ lines.push(
742
+ dt("doctor.archive-history.header", {
743
+ sinceLabel,
744
+ count: String(report.total),
745
+ plural: report.total === 1 ? "" : "s"
746
+ })
747
+ );
748
+ lines.push("");
749
+ lines.push(
750
+ `| ${dt("doctor.archive-history.table.session")} | ${dt(
751
+ "doctor.archive-history.table.lastAttempt"
752
+ )} | ${dt("doctor.archive-history.table.outcome")} | ${dt(
753
+ "doctor.archive-history.table.candidates"
754
+ )} | ${dt("doctor.archive-history.table.coveredGap")} |`
755
+ );
756
+ lines.push("| ------- | ---------------- | -------- | ---------- | ----------- |");
757
+ for (const entry of report.entries) {
758
+ const lastAttempt = formatTimestampForTable(entry.last_attempted_at);
759
+ lines.push(
760
+ `| ${entry.session_id_short} | ${lastAttempt} | ${entry.outcome} | ${entry.candidates_proposed} | ${entry.age_since_covered_hours}h |`
761
+ );
762
+ }
763
+ writeStdout(lines.join("\n"));
764
+ }
765
+ function renderHistoryAllReport(report, sinceLabel, mode, dt) {
766
+ if (report.rows.length === 0) {
767
+ writeStdout(dt("doctor.history.empty", { sinceLabel, mode }));
768
+ return;
769
+ }
770
+ const lines = [];
771
+ lines.push(
772
+ dt("doctor.history.header", {
773
+ sinceLabel,
774
+ mode,
775
+ days: String(report.rows.length)
776
+ })
777
+ );
778
+ lines.push("");
779
+ if (mode === "fix") {
780
+ lines.push("| date | lint | fix | issues | mutations |");
781
+ lines.push("| ---------- | ---- | --- | ------ | --------- |");
782
+ for (const row of report.rows) {
783
+ lines.push(
784
+ `| ${row.date} | ${row.doctor_runs_lint} | ${row.doctor_runs_fix} | ${row.doctor_total_issues} | ${row.doctor_total_mutations} |`
785
+ );
786
+ }
787
+ } else {
788
+ lines.push("| date | lint | fix | issues | mutations | archive | proposed |");
789
+ lines.push("| ---------- | ---- | --- | ------ | --------- | ------- | -------- |");
790
+ for (const row of report.rows) {
791
+ lines.push(
792
+ `| ${row.date} | ${row.doctor_runs_lint} | ${row.doctor_runs_fix} | ${row.doctor_total_issues} | ${row.doctor_total_mutations} | ${row.archive_attempts} | ${row.archive_proposed} |`
793
+ );
794
+ }
795
+ }
796
+ writeStdout(lines.join("\n"));
797
+ }
798
+ function formatTimestampForTable(iso) {
799
+ const d = new Date(iso);
800
+ if (Number.isNaN(d.getTime())) return iso;
801
+ const pad = (n) => n < 10 ? `0${n}` : `${n}`;
802
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(
803
+ d.getUTCHours()
804
+ )}:${pad(d.getUTCMinutes())}`;
805
+ }
806
+ export {
807
+ doctor_default as default,
808
+ doctorCommand,
809
+ parseSinceDuration
810
+ };