@fenglimg/fabric-cli 2.0.0-rc.22 → 2.0.0-rc.25

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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  3. 在目标项目运行 `fabric install`,完成一站式安装。
10
10
  4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_knowledge_sections`。
11
11
 
12
- `fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `install`、`doctor`、`serve`、`uninstall`、`config`(rc.15 `fab scan` 已折叠到 `fab doctor --rescan`)。
12
+ `fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `install`、`doctor`、`serve`、`uninstall`、`config`(rc.23 起移除了 baseline scan 机制,知识库唯一合法来源是 Skill 路径:`fabric-archive` / `fabric-import` / `fabric-review`)。
13
13
 
14
14
  ## 常用命令
15
15
 
@@ -18,7 +18,6 @@
18
18
  - `fabric doctor --json`
19
19
  - `fabric doctor --strict`
20
20
  - `fabric doctor --fix`
21
- - `fabric doctor --rescan`(替代旧的 `fabric scan`)
22
21
  - `fabric serve`
23
22
  - `fabric uninstall`
24
23
  - `fabric config`(rc.16 起将提供配置面板;当前为占位提示)
@@ -109,14 +109,14 @@ var HOOK_CONFIG_ARRAY_PATHS = {
109
109
  };
110
110
  var FABRIC_HOOK_COMMAND_PATHS = {
111
111
  claudeCode: {
112
- fabricHint: ".claude/hooks/fabric-hint.cjs",
113
- knowledgeHintBroad: ".claude/hooks/knowledge-hint-broad.cjs",
114
- knowledgeHintNarrow: ".claude/hooks/knowledge-hint-narrow.cjs"
112
+ fabricHint: "${CLAUDE_PROJECT_DIR}/.claude/hooks/fabric-hint.cjs",
113
+ knowledgeHintBroad: "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-broad.cjs",
114
+ knowledgeHintNarrow: "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-narrow.cjs"
115
115
  },
116
116
  codex: {
117
- fabricHint: ".codex/hooks/fabric-hint.cjs",
118
- knowledgeHintBroad: ".codex/hooks/knowledge-hint-broad.cjs",
119
- knowledgeHintNarrow: ".codex/hooks/knowledge-hint-narrow.cjs"
117
+ fabricHint: '"$(git rev-parse --show-toplevel)/.codex/hooks/fabric-hint.cjs"',
118
+ knowledgeHintBroad: '"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-broad.cjs"',
119
+ knowledgeHintNarrow: '"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-narrow.cjs"'
120
120
  },
121
121
  cursor: {
122
122
  fabricHint: ".cursor/hooks/fabric-hint.cjs",
@@ -3,17 +3,6 @@
3
3
  // src/dev-mode.ts
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { isAbsolute, join, resolve } from "path";
6
- function readFabricConfig(workspaceRoot = process.cwd()) {
7
- const configPath = join(workspaceRoot, "fabric.config.json");
8
- if (!existsSync(configPath)) {
9
- return {};
10
- }
11
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
12
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
13
- throw new Error(`Expected object in ${configPath}`);
14
- }
15
- return parsed;
16
- }
17
6
  function resolveDevMode(cliTarget, workspaceRoot = process.cwd()) {
18
7
  const envTarget = normalizeTarget(process.env.EXTERNAL_FIXTURE_PATH, workspaceRoot);
19
8
  const directTarget = normalizeTarget(cliTarget, workspaceRoot);
@@ -51,7 +40,6 @@ function formatResolutionStep(source, value) {
51
40
  }
52
41
 
53
42
  export {
54
- readFabricConfig,
55
43
  resolveDevMode,
56
44
  createDebugLogger
57
45
  };
@@ -12,7 +12,10 @@ import { readFile } from "fs/promises";
12
12
  import { join, resolve } from "path";
13
13
  import { fileURLToPath } from "url";
14
14
  import { cancel, intro, isCancel, log, outro, select, text } from "@clack/prompts";
15
- import { getPanelFields } from "@fenglimg/fabric-shared";
15
+ import {
16
+ getPanelFields,
17
+ ONBOARD_SLOT_NAMES
18
+ } from "@fenglimg/fabric-shared";
16
19
  import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
17
20
  import { defineCommand } from "citty";
18
21
  async function loadFabricConfig(workspaceRoot) {
@@ -33,6 +36,131 @@ function resolveServerPath(override) {
33
36
  }
34
37
  var PANEL_CONFIG_RELATIVE_PATH = [".fabric", "fabric-config.json"];
35
38
  var EXIT_CHOICE = "__exit__";
39
+ async function readOnboardSlotsList(configPath) {
40
+ const raw = await readFile(configPath, "utf8");
41
+ const parsed = JSON.parse(raw);
42
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
43
+ throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
44
+ }
45
+ const obj = parsed;
46
+ const list = obj.onboard_slots_opted_out;
47
+ const optedOut = Array.isArray(list) ? list.filter((v) => typeof v === "string") : [];
48
+ return { config: obj, optedOut };
49
+ }
50
+ function ensureUninitGate(workspaceRoot) {
51
+ const configPath = join(workspaceRoot, ...PANEL_CONFIG_RELATIVE_PATH);
52
+ const fabricDir = join(workspaceRoot, ".fabric");
53
+ const fabricDirOk = existsSync(fabricDir) && statSync(fabricDir).isDirectory();
54
+ const configOk = fabricDirOk && existsSync(configPath);
55
+ if (!configOk) {
56
+ console.error(t("cli.config.errors.uninit-workspace.message"));
57
+ return null;
58
+ }
59
+ return configPath;
60
+ }
61
+ function validateSlotArg(slot) {
62
+ if (slot === void 0 || slot.length === 0) {
63
+ console.error(`Missing required <slot> argument. Valid slots: ${ONBOARD_SLOT_NAMES.join(", ")}.`);
64
+ return null;
65
+ }
66
+ if (!ONBOARD_SLOT_NAMES.includes(slot)) {
67
+ console.error(`Unknown slot "${slot}". Valid slots: ${ONBOARD_SLOT_NAMES.join(", ")}.`);
68
+ return null;
69
+ }
70
+ return slot;
71
+ }
72
+ var dismissSlotCmd = defineCommand({
73
+ meta: {
74
+ name: "dismiss-slot",
75
+ description: "Add an S5 onboard slot to the opted-out list (fabric-archive Skill onboard phase invokes this).",
76
+ hidden: true
77
+ },
78
+ args: {
79
+ slot: {
80
+ type: "positional",
81
+ description: "Slot name to dismiss (one of the locked S5 set).",
82
+ required: true
83
+ },
84
+ target: {
85
+ type: "string",
86
+ description: "Override the project root (defaults to cwd)."
87
+ }
88
+ },
89
+ async run({ args }) {
90
+ const slot = validateSlotArg(args.slot);
91
+ if (slot === null) {
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ const workspaceRoot = resolve(args.target ?? process.cwd());
96
+ const configPath = ensureUninitGate(workspaceRoot);
97
+ if (configPath === null) {
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ try {
102
+ const { config, optedOut } = await readOnboardSlotsList(configPath);
103
+ if (optedOut.includes(slot)) {
104
+ console.log(`Slot "${slot}" already opted out; no-op.`);
105
+ return;
106
+ }
107
+ const next = [...optedOut, slot];
108
+ const merged = { ...config, onboard_slots_opted_out: next };
109
+ await atomicWriteJson(configPath, merged);
110
+ console.log(`Dismissed onboard slot "${slot}". Run \`fab config onboard-reset ${slot}\` to re-open.`);
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ console.error(`dismiss-slot failed: ${message}`);
114
+ process.exitCode = 1;
115
+ }
116
+ }
117
+ });
118
+ var onboardResetCmd = defineCommand({
119
+ meta: {
120
+ name: "onboard-reset",
121
+ description: "Remove an S5 onboard slot from the opted-out list \u2014 re-opens the slot for future fabric-archive onboard prompts.",
122
+ hidden: true
123
+ },
124
+ args: {
125
+ slot: {
126
+ type: "positional",
127
+ description: "Slot name to reset (one of the locked S5 set).",
128
+ required: true
129
+ },
130
+ target: {
131
+ type: "string",
132
+ description: "Override the project root (defaults to cwd)."
133
+ }
134
+ },
135
+ async run({ args }) {
136
+ const slot = validateSlotArg(args.slot);
137
+ if (slot === null) {
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ const workspaceRoot = resolve(args.target ?? process.cwd());
142
+ const configPath = ensureUninitGate(workspaceRoot);
143
+ if (configPath === null) {
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ try {
148
+ const { config, optedOut } = await readOnboardSlotsList(configPath);
149
+ if (!optedOut.includes(slot)) {
150
+ console.log(`Slot "${slot}" not opted out; no-op.`);
151
+ return;
152
+ }
153
+ const next = optedOut.filter((s) => s !== slot);
154
+ const merged = { ...config, onboard_slots_opted_out: next };
155
+ await atomicWriteJson(configPath, merged);
156
+ console.log(`Reset onboard slot "${slot}"; it will appear in \`fab onboard-coverage\` as missing again.`);
157
+ } catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ console.error(`onboard-reset failed: ${message}`);
160
+ process.exitCode = 1;
161
+ }
162
+ }
163
+ });
36
164
  var configCmd = defineCommand({
37
165
  meta: {
38
166
  name: "config",
@@ -45,7 +173,15 @@ var configCmd = defineCommand({
45
173
  valueHint: "path"
46
174
  }
47
175
  },
176
+ subCommands: {
177
+ "dismiss-slot": dismissSlotCmd,
178
+ "onboard-reset": onboardResetCmd
179
+ },
48
180
  async run({ args }) {
181
+ const argvSub = process.argv[3];
182
+ if (argvSub === "dismiss-slot" || argvSub === "onboard-reset") {
183
+ return;
184
+ }
49
185
  const workspaceRoot = resolve(args.target ?? process.cwd());
50
186
  const configPath = join(workspaceRoot, ...PANEL_CONFIG_RELATIVE_PATH);
51
187
  const fabricDir = join(workspaceRoot, ".fabric");
@@ -3,7 +3,7 @@ import {
3
3
  configCmd,
4
4
  config_default,
5
5
  installMcpClients
6
- } from "./chunk-KZ2YITOS.js";
6
+ } from "./chunk-STLR2GHP.js";
7
7
  import "./chunk-MF3OTILQ.js";
8
8
  import "./chunk-6ICJICVU.js";
9
9
  export {
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- runInitScan
4
- } from "./chunk-PSVKSMRO.js";
5
2
  import {
6
3
  hasActionHint,
7
4
  paint,
@@ -13,7 +10,7 @@ import {
13
10
  } from "./chunk-6ICJICVU.js";
14
11
  import {
15
12
  resolveDevMode
16
- } from "./chunk-ZSESMG6L.js";
13
+ } from "./chunk-COI5VDFU.js";
17
14
 
18
15
  // src/commands/doctor.ts
19
16
  import { confirm, isCancel } from "@clack/prompts";
@@ -21,7 +18,9 @@ import { defineCommand } from "citty";
21
18
  import {
22
19
  appendEventLedgerEvent,
23
20
  checkLockOrThrow,
21
+ enrichDescriptions,
24
22
  runDoctorApplyLint as runDoctorFixKnowledge,
23
+ runDoctorArchiveHistory,
25
24
  runDoctorCiteCoverage,
26
25
  runDoctorFix,
27
26
  runDoctorReport
@@ -59,11 +58,6 @@ var doctorCommand = defineCommand({
59
58
  description: t("cli.doctor.args.json.description"),
60
59
  default: false
61
60
  },
62
- rescan: {
63
- type: "boolean",
64
- description: t("cli.doctor.args.rescan.description"),
65
- default: false
66
- },
67
61
  strict: {
68
62
  type: "boolean",
69
63
  description: t("cli.doctor.args.strict.description"),
@@ -95,6 +89,41 @@ var doctorCommand = defineCommand({
95
89
  description: t("cli.doctor.args.client.description"),
96
90
  default: "all",
97
91
  valueHint: "cc|codex|cursor|all"
92
+ },
93
+ // v2.0.0-rc.24 TASK-10: --layer filter for the cite contract audit. Pairs
94
+ // with --cite-coverage. Validated against {'team','personal','all'} at
95
+ // command entry; rejects 'both' (rc.20 plan-context vocabulary) explicitly.
96
+ layer: {
97
+ type: "string",
98
+ description: t("cli.doctor.args.layer.description"),
99
+ default: "all",
100
+ valueHint: "team|personal|all"
101
+ },
102
+ // rc.23 TASK-007 (a-C2): description-grade back-fill flag set. Read-side
103
+ // by default; `--auto` flips the writer arm on. Mutually exclusive with
104
+ // --fix / --fix-knowledge / --cite-coverage (different mutation surfaces).
105
+ "enrich-descriptions": {
106
+ type: "boolean",
107
+ description: t("cli.doctor.args.enrich-descriptions.description"),
108
+ default: false
109
+ },
110
+ auto: {
111
+ type: "boolean",
112
+ description: t("cli.doctor.args.auto.description"),
113
+ default: false
114
+ },
115
+ "dry-run": {
116
+ type: "boolean",
117
+ description: t("cli.doctor.args.dry-run.description"),
118
+ default: false
119
+ },
120
+ // v2.0.0-rc.25 TASK-10: --archive-history flag (parallel to rc.20
121
+ // --cite-coverage). Read-only; reads session_archive_attempted events
122
+ // and renders a per-session table. Pairs with the shared `--since` flag.
123
+ "archive-history": {
124
+ type: "boolean",
125
+ description: t("cli.doctor.args.archive-history.description"),
126
+ default: false
98
127
  }
99
128
  },
100
129
  async run({ args }) {
@@ -111,8 +140,53 @@ var doctorCommand = defineCommand({
111
140
  }
112
141
  const fixKnowledge = args["fix-knowledge"] === true;
113
142
  const fix = args.fix === true;
114
- const rescan = args.rescan === true;
115
143
  const citeCoverage = args["cite-coverage"] === true;
144
+ const enrichDesc = args["enrich-descriptions"] === true;
145
+ const archiveHistory = args["archive-history"] === true;
146
+ if (archiveHistory) {
147
+ if (fix || fixKnowledge || citeCoverage || enrichDesc) {
148
+ writeStderr(t("cli.doctor.errors.archive-history-mutex"));
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+ const sinceInput = args.since ?? "7d";
153
+ let sinceMs;
154
+ try {
155
+ sinceMs = parseSinceDuration(sinceInput);
156
+ } catch {
157
+ writeStderr(t("cli.doctor.errors.invalid-since", { input: sinceInput }));
158
+ process.exitCode = 1;
159
+ return;
160
+ }
161
+ const report2 = await runDoctorArchiveHistory(resolution.target, {
162
+ since: sinceMs
163
+ });
164
+ if (args.json === true) {
165
+ writeStdout(JSON.stringify(report2, null, 2));
166
+ } else {
167
+ renderArchiveHistoryReport(report2, sinceInput);
168
+ }
169
+ return;
170
+ }
171
+ if (enrichDesc) {
172
+ if (fix || fixKnowledge || citeCoverage) {
173
+ writeStderr(t("cli.doctor.errors.enrich-descriptions-mutex"));
174
+ process.exitCode = 1;
175
+ return;
176
+ }
177
+ const autoFlag = args.auto === true;
178
+ const dryRun = args["dry-run"] === true;
179
+ const report2 = await enrichDescriptions(resolution.target, {
180
+ auto: autoFlag,
181
+ dryRun
182
+ });
183
+ if (args.json === true) {
184
+ writeStdout(JSON.stringify(report2, null, 2));
185
+ } else {
186
+ renderEnrichDescriptionsReport(report2);
187
+ }
188
+ return;
189
+ }
116
190
  if (citeCoverage) {
117
191
  if (fix || fixKnowledge) {
118
192
  writeStderr(t("cli.doctor.errors.cite-coverage-mutex"));
@@ -133,9 +207,16 @@ var doctorCommand = defineCommand({
133
207
  process.exitCode = 1;
134
208
  return;
135
209
  }
210
+ const layerFilter = args.layer ?? "all";
211
+ if (!isValidLayerFilter(layerFilter)) {
212
+ writeStderr(t("cli.doctor.errors.invalid-layer", { input: layerFilter }));
213
+ process.exitCode = 1;
214
+ return;
215
+ }
136
216
  const report2 = await runDoctorCiteCoverage(resolution.target, {
137
217
  since: sinceMs,
138
- client: clientFilter
218
+ client: clientFilter,
219
+ layer: layerFilter
139
220
  });
140
221
  renderCiteCoverageReport(report2, args.json === true);
141
222
  return;
@@ -145,9 +226,6 @@ var doctorCommand = defineCommand({
145
226
  process.exitCode = 1;
146
227
  return;
147
228
  }
148
- if (rescan) {
149
- await runInitScan(resolution.target, { source: "doctor-rescan" });
150
- }
151
229
  let fixKnowledgeReport = null;
152
230
  let fixReport = null;
153
231
  let report;
@@ -350,6 +428,14 @@ var CITE_COVERAGE_CLIENT_FILTERS = /* @__PURE__ */ new Set([
350
428
  function isValidClientFilter(input) {
351
429
  return CITE_COVERAGE_CLIENT_FILTERS.has(input);
352
430
  }
431
+ var CITE_COVERAGE_LAYER_FILTERS = /* @__PURE__ */ new Set([
432
+ "team",
433
+ "personal",
434
+ "all"
435
+ ]);
436
+ function isValidLayerFilter(input) {
437
+ return CITE_COVERAGE_LAYER_FILTERS.has(input);
438
+ }
353
439
  function renderCiteCoverageReport(report, jsonMode) {
354
440
  if (jsonMode) {
355
441
  writeStdout(JSON.stringify(report, null, 2));
@@ -392,8 +478,115 @@ function renderCiteCoverageReport(report, jsonMode) {
392
478
  lines.push(` ${label}: ${count}`);
393
479
  }
394
480
  }
481
+ if (report.none_reason_histogram !== void 0 && Object.keys(report.none_reason_histogram).length > 0) {
482
+ lines.push("");
483
+ lines.push(`### ${t("doctor.cite.section.noneReasons")}`);
484
+ for (const [reason, count] of Object.entries(report.none_reason_histogram)) {
485
+ const label = t(`doctor.cite.none.${reason}`);
486
+ lines.push(` ${label}: ${count}`);
487
+ }
488
+ }
489
+ appendContractSection(lines, report);
395
490
  writeStdout(lines.join("\n"));
396
491
  }
492
+ function appendContractSection(lines, report) {
493
+ const status = report.contract_metrics_status;
494
+ if (status === void 0) {
495
+ return;
496
+ }
497
+ const metrics = report.contract_metrics;
498
+ const perLayerType = report.per_layer_type;
499
+ 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;
500
+ if (status === "awaiting_marker" && allCountsZero) {
501
+ return;
502
+ }
503
+ lines.push("");
504
+ lines.push(`### ${t("cite-coverage.contract.header")}`);
505
+ if (status === "skipped:bootstrap_drift") {
506
+ lines.push(` ${t("cite-coverage.contract.status.skipped_bootstrap_drift")}`);
507
+ return;
508
+ }
509
+ const statusKey = status === "ok" ? "cite-coverage.contract.status.ok" : "cite-coverage.contract.status.awaiting_marker";
510
+ lines.push(` status: ${t(statusKey)}`);
511
+ if (typeof report.contract_marker_ts === "number" && report.contract_marker_ts > 0) {
512
+ lines.push(` since: ${new Date(report.contract_marker_ts).toISOString()}`);
513
+ }
514
+ if (report.layer_filter !== void 0) {
515
+ lines.push(` layer filter: ${report.layer_filter}`);
516
+ }
517
+ if (metrics !== void 0) {
518
+ lines.push(` ${t("cite-coverage.contract.decisions_cited")}: ${metrics.decisions_cited}`);
519
+ lines.push(` ${t("cite-coverage.contract.pitfalls_cited")}: ${metrics.pitfalls_cited}`);
520
+ lines.push(` ${t("cite-coverage.contract.with")}: ${metrics.contract_with}`);
521
+ lines.push(` ${t("cite-coverage.contract.missing")}: ${metrics.contract_missing}`);
522
+ if (metrics.hard_violated > 0) {
523
+ const layerSuffix = report.layer_filter === "personal" ? t("cite-coverage.layer.personal_fyi") : t("cite-coverage.layer.team_review");
524
+ lines.push(
525
+ ` ${t("cite-coverage.contract.hard_violated")} ${layerSuffix}: ${metrics.hard_violated}`
526
+ );
527
+ }
528
+ }
529
+ if (perLayerType !== void 0) {
530
+ const teamKeys = Object.keys(perLayerType.team).filter(
531
+ (k) => perLayerType.team[k] > 0
532
+ );
533
+ const personalKeys = Object.keys(perLayerType.personal).filter(
534
+ (k) => perLayerType.personal[k] > 0
535
+ );
536
+ if (teamKeys.length > 0 || personalKeys.length > 0) {
537
+ lines.push("");
538
+ lines.push(`#### ${t("cite-coverage.layer.team")} \xD7 ${t("cite-coverage.layer.personal")}`);
539
+ for (const key of teamKeys) {
540
+ const label = t(`cite-coverage.contract.type.${key}`);
541
+ lines.push(` ${t("cite-coverage.layer.team")} \u2014 ${label}: ${perLayerType.team[key]}`);
542
+ }
543
+ for (const key of personalKeys) {
544
+ const label = t(`cite-coverage.contract.type.${key}`);
545
+ lines.push(
546
+ ` ${t("cite-coverage.layer.personal")} \u2014 ${label}: ${perLayerType.personal[key]}`
547
+ );
548
+ }
549
+ }
550
+ }
551
+ if (metrics !== void 0 && Object.keys(metrics.skip_count).length > 0) {
552
+ lines.push("");
553
+ lines.push(`#### ${t("cite-coverage.contract.skip_count")}`);
554
+ for (const [reason, count] of Object.entries(metrics.skip_count)) {
555
+ const label = t(`cite-coverage.skip.${reason}`);
556
+ lines.push(` ${label}: ${count}`);
557
+ }
558
+ }
559
+ if (metrics !== void 0 && metrics.cite_id_unresolved > 0) {
560
+ lines.push("");
561
+ lines.push(
562
+ `${symbol.warn} ${t("cite-coverage.contract.cite_id_unresolved")}: ${metrics.cite_id_unresolved}`
563
+ );
564
+ }
565
+ }
566
+ function renderEnrichDescriptionsReport(report) {
567
+ const header = `${symbol.ok} ${paint.ai("fab doctor --enrich-descriptions")} mode=${report.mode}${report.dryRun ? " (dry-run)" : ""} scanned=${report.scanned} modified=${report.modified} skipped=${report.skipped}`;
568
+ writeStdout(header);
569
+ if (report.candidates.length === 0) {
570
+ writeStdout(t("doctor.enrich.allComplete"));
571
+ return;
572
+ }
573
+ writeStdout("");
574
+ for (const candidate of report.candidates) {
575
+ if (candidate.error !== void 0) {
576
+ writeStdout(`${symbol.error} ${candidate.path} \u2014 ${candidate.error}`);
577
+ continue;
578
+ }
579
+ const missing = candidate.missing.join(", ");
580
+ if (candidate.modified) {
581
+ const added = candidate.added_fields.join(", ");
582
+ writeStdout(
583
+ `${symbol.ok} ${candidate.path} \u2014 missing: ${missing} \u2192 added: ${added}`
584
+ );
585
+ } else {
586
+ writeStdout(`${symbol.warn} ${candidate.path} \u2014 missing: ${missing}`);
587
+ }
588
+ }
589
+ }
397
590
  function parseSinceDuration(input) {
398
591
  const trimmed = input.trim();
399
592
  if (trimmed.length === 0) {
@@ -418,6 +611,44 @@ function parseSinceDuration(input) {
418
611
  }
419
612
  throw new Error(`invalid --since value: ${input}`);
420
613
  }
614
+ function renderArchiveHistoryReport(report, sinceLabel) {
615
+ if (report.entries.length === 0) {
616
+ writeStdout(t("doctor.archive-history.empty", { sinceLabel }));
617
+ return;
618
+ }
619
+ const lines = [];
620
+ lines.push(
621
+ t("doctor.archive-history.header", {
622
+ sinceLabel,
623
+ count: String(report.total),
624
+ plural: report.total === 1 ? "" : "s"
625
+ })
626
+ );
627
+ lines.push("");
628
+ lines.push(
629
+ `| ${t("doctor.archive-history.table.session")} | ${t(
630
+ "doctor.archive-history.table.lastAttempt"
631
+ )} | ${t("doctor.archive-history.table.outcome")} | ${t(
632
+ "doctor.archive-history.table.candidates"
633
+ )} | ${t("doctor.archive-history.table.coveredGap")} |`
634
+ );
635
+ lines.push("| ------- | ---------------- | -------- | ---------- | ----------- |");
636
+ for (const entry of report.entries) {
637
+ const lastAttempt = formatTimestampForTable(entry.last_attempted_at);
638
+ lines.push(
639
+ `| ${entry.session_id_short} | ${lastAttempt} | ${entry.outcome} | ${entry.candidates_proposed} | ${entry.age_since_covered_hours}h |`
640
+ );
641
+ }
642
+ writeStdout(lines.join("\n"));
643
+ }
644
+ function formatTimestampForTable(iso) {
645
+ const d = new Date(iso);
646
+ if (Number.isNaN(d.getTime())) return iso;
647
+ const pad = (n) => n < 10 ? `0${n}` : `${n}`;
648
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(
649
+ d.getUTCHours()
650
+ )}:${pad(d.getUTCMinutes())}`;
651
+ }
421
652
  export {
422
653
  doctor_default as default,
423
654
  doctorCommand,
package/dist/index.js CHANGED
@@ -11,19 +11,22 @@ import { defineCommand, runMain } from "citty";
11
11
 
12
12
  // src/commands/index.ts
13
13
  var allCommands = {
14
- install: () => import("./install-WJZQZM7D.js").then((module) => module.default),
15
- doctor: () => import("./doctor-HIX2FFEP.js").then((module) => module.default),
16
- serve: () => import("./serve-6PPQX7AW.js").then((module) => module.default),
17
- uninstall: () => import("./uninstall-L2HEEOU3.js").then((module) => module.default),
18
- config: () => import("./config-AYP5F72E.js").then((module) => module.default),
19
- "plan-context-hint": () => import("./plan-context-hint-RYVSMULL.js").then((module) => module.default)
14
+ install: () => import("./install-S2J76N2B.js").then((module) => module.default),
15
+ doctor: () => import("./doctor-DXKPYPRC.js").then((module) => module.default),
16
+ serve: () => import("./serve-NPCI342P.js").then((module) => module.default),
17
+ uninstall: () => import("./uninstall-MQM6NUFM.js").then((module) => module.default),
18
+ config: () => import("./config-XGUUAYX6.js").then((module) => module.default),
19
+ "plan-context-hint": () => import("./plan-context-hint-KPGOW3QC.js").then((module) => module.default),
20
+ // v2.0.0-rc.23 TASK-014 (F8c): S5 onboard-slot coverage. Used by the
21
+ // fabric-archive Skill's first-run phase to detect unclaimed slots.
22
+ "onboard-coverage": () => import("./onboard-coverage-JJ5NGU7I.js").then((module) => module.default)
20
23
  };
21
24
 
22
25
  // src/index.ts
23
26
  var main = defineCommand({
24
27
  meta: {
25
28
  name: "fabric",
26
- version: "2.0.0-rc.22",
29
+ version: "2.0.0-rc.25",
27
30
  description: t("cli.main.description")
28
31
  },
29
32
  subCommands: allCommands