@fenglimg/fabric-cli 2.0.0-rc.36 → 2.0.0-rc.38

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{chunk-XVS4F3P6.js → chunk-D25XJ4BC.js} +49 -5
  3. package/dist/{chunk-G2CIOLD4.js → chunk-WWNXR34K.js} +1 -16
  4. package/dist/{doctor-2FCRAWDZ.js → doctor-EJDSEJSS.js} +119 -16
  5. package/dist/index.js +8 -7
  6. package/dist/{install-XSUIX6AD.js → install-E6OEB3V2.js} +53 -22
  7. package/dist/metrics-ACEQFPDU.js +122 -0
  8. package/dist/{plan-context-hint-UQLRKGBZ.js → plan-context-hint-FC6P3WFE.js} +7 -28
  9. package/dist/{uninstall-BIJ5GLEU.js → uninstall-MH7ZIB6M.js} +6 -18
  10. package/package.json +30 -4
  11. package/templates/hooks/cite-policy-evict.cjs +80 -91
  12. package/templates/hooks/configs/README.md +19 -0
  13. package/templates/hooks/configs/codex-hooks.json +3 -0
  14. package/templates/hooks/configs/cursor-hooks.json +2 -1
  15. package/templates/hooks/fabric-hint.cjs +146 -8
  16. package/templates/hooks/knowledge-hint-broad.cjs +65 -104
  17. package/templates/hooks/knowledge-hint-narrow.cjs +140 -7
  18. package/templates/hooks/lib/cite-line-parser.cjs +7 -1
  19. package/templates/hooks/lib/client-adapter.cjs +106 -0
  20. package/templates/hooks/lib/config-cache.cjs +107 -0
  21. package/templates/hooks/lib/state-store.cjs +84 -0
  22. package/templates/skills/fabric-archive/SKILL.md +29 -7
  23. package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
  24. package/templates/skills/fabric-archive/ref/i18n-policy.md +6 -0
  25. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +1 -1
  26. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +2 -0
  27. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +25 -11
  28. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +43 -15
  29. package/templates/skills/fabric-import/SKILL.md +3 -3
  30. package/templates/skills/fabric-import/ref/i18n-policy.md +6 -0
  31. package/templates/skills/fabric-import/ref/phase-2-mining.md +2 -2
  32. package/templates/skills/fabric-review/SKILL.md +31 -25
  33. package/templates/skills/fabric-review/ref/i18n-policy.md +6 -0
  34. package/templates/skills/fabric-review/ref/modify-flow.md +9 -1
  35. package/templates/skills/fabric-review/ref/per-mode-flows.md +1 -1
  36. package/templates/skills/lib/shared-policy.md +69 -0
  37. package/dist/serve-43JTEM3U.js +0 -142
@@ -0,0 +1,107 @@
1
+ /**
2
+ * v2.0.0-rc.37 NEW-19: shared fabric-config reader for hook scripts.
3
+ *
4
+ * Before this lib, every hook re-implemented the same defensive
5
+ * `readFileSync(.fabric/fabric-config.json) → JSON.parse → validate one key →
6
+ * fall back to default` boilerplate, once PER KEY (knowledge-hint-broad alone
7
+ * read the file 5× per SessionStart fire: cooldown / top_k / underseed /
8
+ * summary_max_len / reminder_to_context). This module centralises the read +
9
+ * mtime-keyed memoisation so a single hook fire parses the config once.
10
+ *
11
+ * Provides:
12
+ * - readConfig(projectRoot) → object
13
+ * Parsed fabric-config.json (memoised on path+mtime). Returns `{}` on
14
+ * any failure (missing file / parse error / non-object). Never throws.
15
+ * mtime-keyed so a config rewrite mid-process (test harness) invalidates
16
+ * the cached value automatically — production hooks are single-shot so
17
+ * the common case is one stat + one parse.
18
+ * - readConfigNumber(root, key, fallback, { min, max, integer }) → number
19
+ * - readConfigBoolean(root, key, fallback) → boolean
20
+ * - readConfigString(root, key, fallback) → string
21
+ * Typed getters with inline range/shape validation; any miss → fallback.
22
+ * - configPathFor(projectRoot) → absolute config path
23
+ * - clearConfigCache() → void (test helper)
24
+ *
25
+ * Never-throw contract: every export degrades to its fallback rather than
26
+ * throwing, preserving the reminder-layer hook invariant (KT-DEC-0007: hooks
27
+ * never block on their own malfunction).
28
+ */
29
+
30
+ const { existsSync, readFileSync, statSync } = require("node:fs");
31
+ const { join } = require("node:path");
32
+
33
+ const FABRIC_DIR_REL = ".fabric";
34
+ const FABRIC_CONFIG_FILE = "fabric-config.json";
35
+
36
+ // path → { mtime, value }. Per-process; mtime-keyed for test-mutation safety.
37
+ const _cache = new Map();
38
+
39
+ function configPathFor(projectRoot) {
40
+ return join(projectRoot, FABRIC_DIR_REL, FABRIC_CONFIG_FILE);
41
+ }
42
+
43
+ function readConfig(projectRoot) {
44
+ const path = configPathFor(projectRoot);
45
+ let mtime;
46
+ try {
47
+ if (!existsSync(path)) {
48
+ _cache.delete(path);
49
+ return {};
50
+ }
51
+ mtime = statSync(path).mtimeMs;
52
+ } catch {
53
+ return {};
54
+ }
55
+ const cached = _cache.get(path);
56
+ if (cached && cached.mtime === mtime) return cached.value;
57
+ let value = {};
58
+ try {
59
+ const raw = JSON.parse(readFileSync(path, "utf8"));
60
+ if (raw && typeof raw === "object") value = raw;
61
+ } catch {
62
+ value = {};
63
+ }
64
+ _cache.set(path, { mtime, value });
65
+ return value;
66
+ }
67
+
68
+ function clearConfigCache() {
69
+ _cache.clear();
70
+ }
71
+
72
+ // opts:
73
+ // min / max — inclusive range; out-of-range → fallback
74
+ // integer — require Number.isInteger; non-integer → fallback (strict)
75
+ // floor — accept any in-range number, return Math.floor(v) (lenient)
76
+ // `integer` and `floor` are independent: `integer` rejects fractional values,
77
+ // `floor` truncates them. Pick whichever matches the caller's legacy contract.
78
+ function readConfigNumber(projectRoot, key, fallback, opts) {
79
+ const { min, max, integer, floor } = opts || {};
80
+ const v = readConfig(projectRoot)[key];
81
+ if (typeof v === "number" && Number.isFinite(v)) {
82
+ if (integer && !Number.isInteger(v)) return fallback;
83
+ if (typeof min === "number" && v < min) return fallback;
84
+ if (typeof max === "number" && v > max) return fallback;
85
+ return floor ? Math.floor(v) : v;
86
+ }
87
+ return fallback;
88
+ }
89
+
90
+ function readConfigBoolean(projectRoot, key, fallback) {
91
+ const v = readConfig(projectRoot)[key];
92
+ return typeof v === "boolean" ? v : fallback;
93
+ }
94
+
95
+ function readConfigString(projectRoot, key, fallback) {
96
+ const v = readConfig(projectRoot)[key];
97
+ return typeof v === "string" && v.length > 0 ? v : fallback;
98
+ }
99
+
100
+ module.exports = {
101
+ readConfig,
102
+ clearConfigCache,
103
+ readConfigNumber,
104
+ readConfigBoolean,
105
+ readConfigString,
106
+ configPathFor,
107
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * v2.0.0-rc.37 NEW-19: shared `.fabric/.cache/` sidecar I/O for hook scripts.
3
+ *
4
+ * Hooks persist tiny per-session state (turn counters, last-emit timestamps,
5
+ * shown-hint sets) under `.fabric/.cache/`. Each hook had its own copy of the
6
+ * read-JSON-or-null / write-JSON-best-effort / read-text / write-text helpers
7
+ * (cite-policy-evict's readEvictState/writeEvictState, broad's
8
+ * readBroadLastEmit/writeBroadLastEmit, fabric-hint's shown-cache + edit-counter
9
+ * + maintenance-last-emit). This module is the single canonical implementation.
10
+ *
11
+ * Provides (all keyed on a bare `fileName` resolved under .fabric/.cache/):
12
+ * - cachePath(projectRoot, fileName) → absolute path
13
+ * - readJsonState(root, fileName, validate?) → parsed | null
14
+ * null on missing / parse error / validate() === false. Never throws.
15
+ * - writeJsonState(root, fileName, value) → boolean
16
+ * mkdir -p + write; false on failure. Never throws.
17
+ * - readTextState(root, fileName) → trimmed string | null
18
+ * - writeTextState(root, fileName, text) → boolean
19
+ *
20
+ * Never-throw contract: write failures return false (counter loss is
21
+ * acceptable — the hook never blocks user flow on sidecar I/O, KT-DEC-0007).
22
+ */
23
+
24
+ const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("node:fs");
25
+ const { dirname, join } = require("node:path");
26
+
27
+ const CACHE_DIR_REL = join(".fabric", ".cache");
28
+
29
+ function cachePath(projectRoot, fileName) {
30
+ return join(projectRoot, CACHE_DIR_REL, fileName);
31
+ }
32
+
33
+ function readJsonState(projectRoot, fileName, validate) {
34
+ const path = cachePath(projectRoot, fileName);
35
+ if (!existsSync(path)) return null;
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
38
+ if (typeof validate === "function" && !validate(parsed)) return null;
39
+ return parsed;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function writeJsonState(projectRoot, fileName, value) {
46
+ const path = cachePath(projectRoot, fileName);
47
+ try {
48
+ mkdirSync(dirname(path), { recursive: true });
49
+ writeFileSync(path, JSON.stringify(value));
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function readTextState(projectRoot, fileName) {
57
+ const path = cachePath(projectRoot, fileName);
58
+ if (!existsSync(path)) return null;
59
+ try {
60
+ return readFileSync(path, "utf8").trim();
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function writeTextState(projectRoot, fileName, text) {
67
+ const path = cachePath(projectRoot, fileName);
68
+ try {
69
+ mkdirSync(dirname(path), { recursive: true });
70
+ writeFileSync(path, String(text));
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ cachePath,
79
+ readJsonState,
80
+ writeJsonState,
81
+ readTextState,
82
+ writeTextState,
83
+ CACHE_DIR_REL,
84
+ };
@@ -21,11 +21,17 @@ If none hold, stop the skill and tell the user (UX i18n Policy class 2):
21
21
 
22
22
  Render per `fabric_language` resolved in Phase 0.5.
23
23
 
24
- This skill is `Check-not-Ask`, not a preference interview. Phase 2 proactively gathers evidence; Phase 2.5 viability gate aborts if no archive signal exists; Phase 3 classifies / layers / slugs + batch-review; Phase 4 persists via `fab_extract_knowledge` (one call per candidate).
24
+ This skill runs automatically it does not interview the user for preferences. It gathers evidence, aborts if no archive signal exists, then classifies + persists.
25
25
 
26
26
  ## 执行流程 (1 User Review Round)
27
27
 
28
- Phase chain: `00.5 1 [1.5] 2 → 2.5 → 3 3.5 4 4.5`. Each phase below is a navigator stub full procedure, decision tables, and worked examples live in `ref/`.
28
+ v2.0.0-rc.37 NEW-9 collapsed the flow to **3 macro-phases**; the legacy fine-grained phases survive as labelled sub-steps (back-compat for `ref/` navigation + existing traces):
29
+
30
+ - **GATHER** = Phase 0 (range) → 0.5 (config) → 1 (ledger scan via `fab_archive_scan`) → [1.5 onboard] → 2 (candidates). Resolve scope, load config, deterministically scan the ledger for in-scope sessions, collect candidate observations.
31
+ - **REVIEW** = Phase 2.5 (viability gate — abort guard) → 3 (classify / layer / slug + batch review) → 3.5 (scope + relevance_paths). The single user review round lives here.
32
+ - **PERSIST** = Phase 4 (`fab_extract_knowledge`, one call per candidate) → 4.5 (archive-attempt ledger).
33
+
34
+ Sub-step chain: `0 → 0.5 → 1 → [1.5] → 2 → 2.5 → 3 → 3.5 → 4 → 4.5`. Each below is a navigator stub — full procedure, decision tables, and worked examples live in `ref/`.
29
35
 
30
36
  ### Phase 0 — Range Resolution
31
37
 
@@ -43,11 +49,19 @@ Read `fabric_language` (`zh-CN` / `en` / `zh-CN-hybrid` / `match-existing`); emi
43
49
 
44
50
  `Read ref/i18n-policy.md` for the full 5-class taxonomy + edge cases.
45
51
 
46
- ### Phase 1 — Collect Cross-Session Digests
52
+ ### Phase 1 — Collect Cross-Session Digests (server-side ledger scan, rc.37 NEW-9)
53
+
54
+ The deterministic ledger scan now runs **server-side** — call `fab_archive_scan({ range, session_id })` (range = Phase 0's `session_id[]` or `"all"`/omitted). It returns:
55
+
56
+ - `anchor_ts` — ts of the last `knowledge_proposed` (the lower bound).
57
+ - `session_ids[]` — distinct in-scope sessions since the anchor, ALREADY filtered through the outcome-ledger state machine (drops `user_dismissed`, sessions inside the 12h anti-loop cooldown, and watermarked sessions with no new high-value signal). First-seen order.
58
+ - `dropped[]` — `{session_id, reason}` for transparency.
59
+ - `covered_through_ts` — max ts examined (becomes the next watermark).
60
+ - `already_proposed_keys[]` — idempotency keys already proposed but not yet reviewed; drop matching candidates in Phase 3 (cross-session pending dedupe).
47
61
 
48
- Stitch context from every session accumulated since the last `knowledge_proposed` event. Tail-scan `.fabric/events.jsonl` → find anchor → forward-collect distinct `session_id`s load per-session digests from `.fabric/.cache/session-digests/<session_id>.md` concatenate into `### Cross-session digest` block, populating `source_sessions[]` + `session_context` for Phase 4. Cap at `archive_digest_max_sessions`.
62
+ Then (LLM side, Boundary B): for each returned `session_id`, load `.fabric/.cache/session-digests/<session_id>.md`, concatenate into a `### Cross-session digest` block, and populate `source_sessions[]` + `session_context` for Phase 4. Cap at `archive_digest_max_sessions`. Missing digest files degrade silently.
49
63
 
50
- `Read ref/phase-1-cross-session.md` for the Step 4.5 ledger filter state machine (rules a-f), `ANTI_LOOP_HOURS` / `HIGH_VALUE_EVENT_TYPES` / `NORMATIVE_KEYWORDS` constants, and 3 worked examples (user_dismissed / cooldown / re-scan-with-signal).
64
+ `Read ref/phase-1-cross-session.md` for the (now server-side) filter state machine rules + the digest-stitch + graceful-degradation notes. The hand-rolled `tail -n 200` events.jsonl scan is retired — `fab_archive_scan` is the source of truth for which sessions to load.
51
65
 
52
66
  Graceful degradation: missing digest cache → single-session fallback. Missing `session_archive_attempted` events (pre-rc.25) → legacy "scan everything since anchor" behaviour.
53
67
 
@@ -63,7 +77,15 @@ Gather raw evidence: tail `.fabric/events.jsonl` since last `knowledge_proposed`
63
77
 
64
78
  ### Phase 2.5 — Viability Gate (Anti-Archive Guard)
65
79
 
66
- Coarse viability check. **PASS conditions**: user_explicit_invoke OR ≥1 archive signal hit (normative language, wrong-turn-and-revert, long diagnostic loop, new dependency, new pattern, decision confirmation, dismissal-with-reason, process formalization). **FAIL branch by entry_point**: E1/E3/E5 silent-skip (emit Phase 4.5 event `outcome='skipped_no_signal'`); E2/E4 render gate-FAIL message (emit `outcome='viability_failed'`).
80
+ Coarse viability check. **PASS conditions**: user_explicit_invoke OR ≥1 archive signal hit. v2.0.0-rc.37 NEW-4 simplifies the legacy 8-signal list into **3 major categories** (each maps to multiple legacy signals for back-compat scoring):
81
+
82
+ 1. **User-driven knowledge expression** — user message contains normative language (`always`/`never`/`from now on`/`下次注意`/`记一下`/`以后`/`永远不要`), OR user weighed ≥2 alternatives and gave rationale (decision confirmation), OR user explicitly dismissed an approach AND stated why (dismissal-with-reason). Legacy signals #1 + #6 + #7.
83
+ 2. **Reflective discovery** — AI tried path X, reflected, then took path Y (wrong-turn-and-revert); OR a long diagnostic loop (>15 min / >10 turns) surfaced a non-obvious cause; OR a reusable pattern was named in the session ("the X phase", "the Y pattern"). Legacy signals #2 + #3 + #5.
84
+ 3. **Concrete artifact change** — a new dependency was added (package.json/pyproject.toml/Cargo.toml diff), OR a load-bearing multi-step procedure was formalized in a specific order. Legacy signals #4 + #8.
85
+
86
+ Pre-PASS MUST step (rc.37 NEW-4): for each candidate, run a quick `Glob` over `.fabric/knowledge/**/*.md` keyed on slug-stem to check for duplicate canonical entry. If duplicate found → drop candidate (treat as anti-signal #4 'duplicate of existing canonical'). This is a HARD gate, not advisory — silently writing a near-duplicate is the highest-noise failure mode.
87
+
88
+ **FAIL → branch by entry_point**: E1/E3/E5 silent-skip (emit Phase 4.5 event `outcome='skipped_no_signal'`); E2/E4 render gate-FAIL message (emit `outcome='viability_failed'`). Gate-FAIL message for E2/E4 MUST include the "to force-archive, explicitly invoke fabric-archive" remediation pointer so the user has an unambiguous escape hatch when the gate misclassifies (zh-CN: `如需强制归档,请显式调用 fabric-archive` / en: `To force-archive, explicitly invoke fabric-archive`).
67
89
 
68
90
  `Read ref/phase-2-5-viability.md` for verbose signal definitions, zh-CN/en gate-FAIL message bodies, anti-archive signals (typo / refactor / rename / duplicate), and the events.jsonl 4KB POSIX atomicity constraint.
69
91
 
@@ -126,7 +148,7 @@ MANDATORY closing step on EVERY invocation (Phase 4 success path + every early-e
126
148
  - NEVER classify a candidate as `personal` when a 强 team signal applies. Default to team on ambiguity.
127
149
  - NEVER emit a non-empty `relevance_paths` when `relevance_scope=broad` — broad MUST always carry `relevance_paths=[]`.
128
150
  - NEVER emit a non-empty `relevance_paths` when `layer=personal` — personal forces `relevance_scope=broad` + `relevance_paths=[]`.
129
- - NEVER use multi-signal sources for relevance_paths in rc.5 `edit_paths` is the SOLE source. `read_paths`, body regex, and symbol extraction are reserved for rc.7+.
151
+ - v2.0.0-rc.37 NEW-7 widened Phase 3.5: `edit_paths` `user_mentioned_paths` drives `relevance_paths`; `read_paths` flows separately to `evidence_paths` (structured frontmatter, not body markdown). NEVER lift body regex / symbol extraction into `relevance_paths` — those remain reserved for v2.1+.
130
152
  - NEVER batch multiple candidates into a single fab_extract_knowledge call; one call per candidate.
131
153
  - NEVER paraphrase the verbatim layer heuristic block above — the Chinese text is contract-locked.
132
154
  - MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `.fabric/knowledge/pending/`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`.
@@ -1,6 +1,6 @@
1
1
  # Dry-run Scope (unified)
2
2
 
3
- `dry_run = true` (per Phase 4.5 detection rule — substring match on `--dry-run` | `dry-run` | `dry_run` | `预览` token) suspends ALL side-effecting writes below; read-side machinery (Phase 1 digest collection, Phase 2.5 viability gate evaluation, Phase 3 candidate render) executes normally so the user can preview what WOULD happen.
3
+ `dry_run = true` (per Phase 4.5 detection rule — invocation MUST carry the verbatim token `--dry-run`; v2.0.0-rc.37 NEW-10 dropped the legacy substring fallback on bare `dry-run` / `dry_run` / `预览` to eliminate false positives on incidental mentions of those words mid-prompt) suspends ALL side-effecting writes below; read-side machinery (Phase 1 digest collection, Phase 2.5 viability gate evaluation, Phase 3 candidate render) executes normally so the user can preview what WOULD happen.
4
4
 
5
5
  | Write operation | Normal mode | Dry-run mode |
6
6
  |---|---|---|
@@ -1,5 +1,11 @@
1
1
  # UX i18n Policy — full reference
2
2
 
3
+ > **Shared core (rc.37 NEW-13):** the cross-skill invariants — protected-token
4
+ > NEVER-translate list, AskUserQuestion routing-key rule, layer heuristic, and
5
+ > events-emit convention — live once in `../../lib/shared-policy.md`. This file
6
+ > keeps only the fabric-archive-specific 5-class examples. Read the shared lib
7
+ > for the common rules; do not fork them here.
8
+
3
9
  > **Loaded on demand.** Only consult when rendering bilingual output AND you're unsure which class a string belongs to. SKILL.md gives the operative rule: read `.fabric/fabric-config.json` → `fabric_language`, emit prose in resolved variant, never translate protected tokens. The 5-class taxonomy below disambiguates edge cases.
4
10
 
5
11
  ## UX i18n Policy (5-class bilingualization)
@@ -23,7 +23,7 @@ is the canonical taxonomy for this gate.
23
23
  |-------|--------|-------------------------------------------------------|
24
24
  | **E1** | `hook_passive` | stdout JSON `{decision:'block', ...}` from `archive-hint.cjs` detected at skill entry (the Stop-hook reminder path). |
25
25
  | **E2** | `explicit_user_invoke` | User prompt is a direct invocation: `fabric archive` / `/fabric-archive` / `archive what we just did` / `归档一下` / similar imperative. |
26
- | **E3** | `ai_self_trigger` | AI internal marker `self-archive policy triggered by signal: <X>` present (substring match on the verbatim prefix `self-archive policy triggered by signal`; `<X>` is one of the 4 self-trigger signals from AGENTS.md E3 section: `Normative` / `Wrong-turn-and-revert` / `Decision confirmation` / `Explicit dismissal`). |
26
+ | **E3** | `ai_self_trigger` | AI internal marker `self-archive policy triggered by signal: <X>` present (substring match on the verbatim prefix `self-archive policy triggered by signal` per AGENTS.md self-archive policy section; `<X>` is the signal name. v2.0.0-rc.37 NEW-2 simplified the AGENTS.md taxonomy to 2 categories: `User-driven normative` / `Wrong-turn-and-revert`. Back-compat: legacy 4-state names (`Normative` / `Decision confirmation` / `Explicit dismissal`) still route correctly because the substring gate only matches the verbatim prefix and treats any text after `signal:` as the signal label.) |
27
27
  | **E4** | `user_range_rollback` | Prompt contains a **range hint** (parsed in Phase 0 — e.g. `今日` / `上周` / `rc.20`) AND the user is invoking. Sub-mode of E2. |
28
28
  | **E5** | `cron` | Prompt contains literal `今日复盘` / `daily recap` / `daily-archive` AND no human is present (running under `/loop`, OS cron, or scheduled trigger). |
29
29
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  > **Loaded on demand.** SKILL.md hot path retains Phase 1's purpose statement + 5-step summary + graceful-degradation note. This file holds Steps 1-5 detailed implementation (events.jsonl tail-scan, anchor-walk, digest load, rc.25 TASK-05 ledger filter algorithm + constants + worked examples, cross-session context build).
4
4
 
5
+ > **v2.0.0-rc.37 NEW-9 — Steps 1-4.5 moved server-side.** The deterministic part of this algorithm (events.jsonl tail-scan, anchor-find, session forward-collect, and the Step 4.5 outcome-ledger filter state machine) now runs in the server and is exposed as the `fab_archive_scan` MCP tool — call it instead of hand-running `tail`/grep. The tool returns the already-filtered `session_ids[]` + `anchor_ts` + `covered_through_ts` + `already_proposed_keys[]`. Steps 1-4.5 below remain as the AUTHORITATIVE SPEC of what the server computes (and the contract tests pin it); the Skill no longer executes them by hand. Step 5 (digest load + cross-session context stitch) stays LLM-side per Boundary B.
6
+
5
7
  ## Step 1 — Read events.jsonl tail
6
8
 
7
9
  Use `Bash` with `tail -n 200 .fabric/events.jsonl` (tolerate ENOENT — empty ledger is a normal first-run state).
@@ -2,18 +2,32 @@
2
2
 
3
3
  > **Loaded on demand.** SKILL.md hot path retains the signal lists, gate decision pseudocode, and entry-point branching summary. This file holds the verbose explanations for each signal, gate-FAIL message variants (zh-CN/en), the events.jsonl 4KB atomicity constraint note, and rationale.
4
4
 
5
- ## Archive signals — verbose explanation
5
+ ## Archive signals — verbose explanation (rc.37 NEW-4 simplified 8 → 3)
6
6
 
7
- Scan `user_messages_summary` + `recent_paths` + the events tail collected in Phase 2. Each signal listed in SKILL.md hot path is explained below:
7
+ Scan `user_messages_summary` + `recent_paths` + the events tail collected in Phase 2. The legacy 8-signal list was simplified in v2.0.0-rc.37 NEW-4 to **3 major categories**, mirroring the AGENTS.md Self-archive policy rc.37 NEW-2 simplification. Legacy signal names remain valid scoring inputs for back-compat (any path that fires a legacy signal also fires the corresponding category):
8
8
 
9
- 1. **Explicit normative language** user said `always` / `never` / `from now on` / `下次注意` / `记一下` / `以后` / `永远不要`. Strongest single signal — even one normative cue is sufficient.
10
- 2. **Wrong-turn-and-revert** — a path was edited, then reverted (or partially undone) after diagnosis. Indicates a pitfall worth recording (the why-not lives in the revert).
11
- 3. **Long diagnostic loop** an issue took > 15 minutes (or > ~10 tool turns) of debugging before resolution. Implies a non-obvious cause worth capturing.
12
- 4. **New dependency adoption** — a new package / library / external tool was introduced (e.g. `package.json` / `pyproject.toml` / `Cargo.toml` diff adds a dep).
13
- 5. **New pattern emergence** — a reusable abstraction or naming convention was named ("the X phase", "the Y pattern", "let's call this Z").
14
- 6. **Decision confirmation** — ≥ 2 alternatives were weighed AND a rationale was given before settling. The rationale is the archivable knowledge.
15
- 7. **Explicit dismissal-with-reason** — user rejected an approach AND stated why. The why is the archivable knowledge, not the dismissal itself.
16
- 8. **Process formalization** — a multi-step procedure was executed in a specific order AND the order was identified as load-bearing.
9
+ ### Category 1User-driven knowledge expression
10
+
11
+ Covers legacy signals #1 (normative language) + #6 (decision confirmation) + #7 (dismissal-with-reason). All three are "user message contains structured knowledge worth keeping":
12
+
13
+ - **Normative language** — user said `always` / `never` / `from now on` / `下次注意` / `记一下` / `以后` / `永远不要`. Strongest single cue.
14
+ - **Decision confirmation** — ≥ 2 alternatives were weighed AND a rationale was given before settling. The rationale is the archivable knowledge.
15
+ - **Dismissal-with-reason** — user rejected an approach AND stated why. The why is archivable, not the dismissal itself.
16
+
17
+ ### Category 2 — Reflective discovery
18
+
19
+ Covers legacy signals #2 (wrong-turn-and-revert) + #3 (long diagnostic loop) + #5 (new pattern emergence). All three are "AI execution surfaced a non-obvious insight":
20
+
21
+ - **Wrong-turn-and-revert** — a path was edited, then reverted (or partially undone) after diagnosis. The why-not lives in the revert.
22
+ - **Long diagnostic loop** — an issue took > 15 minutes (or > ~10 tool turns) of debugging before resolution. Non-obvious cause worth capturing.
23
+ - **New pattern emergence** — a reusable abstraction or naming convention was named in-session ("the X phase", "the Y pattern", "let's call this Z").
24
+
25
+ ### Category 3 — Concrete artifact change
26
+
27
+ Covers legacy signals #4 (new dependency) + #8 (process formalization). Both surface in tangible workspace artifacts:
28
+
29
+ - **New dependency adoption** — a new package / library / external tool was introduced (`package.json` / `pyproject.toml` / `Cargo.toml` diff adds a dep).
30
+ - **Process formalization** — a multi-step procedure was executed in a specific order AND the order was identified as load-bearing.
17
31
 
18
32
  ## Anti-archive signals — verbose explanation
19
33
 
@@ -22,7 +36,7 @@ These force the gate to FAIL **unless** an archive signal also fires (i.e. anti-
22
36
  1. **Typo-only edits** — the entire session is whitespace / spelling / formatting changes. No semantic content to archive.
23
37
  2. **Pure refactor** — rename / move / extract with no behavior change AND no naming convention being established.
24
38
  3. **Narrow rename request** — user asked to rename one symbol / file with no rationale. Zero generalization potential.
25
- 4. **Duplicate of existing canonical** — the observation is already covered by an existing entry under `.fabric/knowledge/<type>/`. Do a quick Glob before deciding.
39
+ 4. **Duplicate of existing canonical** — v2.0.0-rc.37 NEW-4: this check is now **mandatory** (was "do a quick Glob before deciding"). Pre-PASS MUST step: for each candidate, `Glob('.fabric/knowledge/**/*.md')` keyed on slug-stem. If duplicate found → drop candidate. Silently writing a near-duplicate is the highest-noise failure mode and dwarfs the cost of one extra glob per candidate.
26
40
 
27
41
  ## Gate-FAIL user messages (E2 / E4 only)
28
42
 
@@ -1,45 +1,73 @@
1
1
  # Phase 3.5 — Scope Decision + relevance_paths Derivation (ref)
2
2
 
3
- > **Loaded on demand.** SKILL.md hot path retains the scope decision pseudocode, personal-layer-forces-broad rule, and brief examples. This file holds the rc.5 single-signal `edit_paths` derivation algorithm (Steps 1-6) + worked generalization example + inline-edit re-derivation rules.
3
+ > **Loaded on demand.** SKILL.md hot path retains the scope decision pseudocode, personal-layer-forces-broad rule, and brief examples. This file holds the rc.37 multi-signal derivation algorithm (edit_paths + read_paths + user_mentioned_paths), Steps 1-6 + worked generalization example + inline-edit re-derivation rules + the frontmatter `evidence_paths` upgrade.
4
4
 
5
- ## relevance_paths derivation algorithm (rc.5 single-signal: edit_paths only)
5
+ ## relevance_paths derivation algorithm (rc.37 multi-signal NEW-7)
6
6
 
7
- rc.5 uses ONLY the `edit_paths` signal — list of paths modified by `Edit` / `Write` / `MultiEdit` tool calls in the current session. Multi-signal (read_paths + body regex + symbols) is explicitly deferred to rc.7 per design decision.
7
+ rc.37 NEW-7 widens Step 1 from the rc.5 single-signal (`edit_paths` only) to three sources:
8
+
9
+ 1. **`edit_paths`** — files modified by `Edit` / `Write` / `MultiEdit` tool calls. The primary activation signal: if the agent CHANGED a file, the knowledge derived in this session most likely applies there.
10
+ 2. **`read_paths`** — files inspected via `Read` / `Grep` / `Glob` without modification. Secondary signal: read-only inspection often anchors the applicability surface even when no write happened (e.g. discovering that a pitfall surfaces in a getter that the agent only READ).
11
+ 3. **`user_mentioned_paths`** — paths the user typed verbatim in messages (`packages/server/src/foo.ts`, `\`packages/cli/**/*.ts\`` etc.). Strongest signal of all: an explicit user-named path is ground-truth applicability surface, independent of what the agent did.
8
12
 
9
13
  ```
10
- Step 1: COLLECT
14
+ Step 1: COLLECT (rc.37 NEW-7 — three sources)
11
15
  edit_paths = []
16
+ read_paths = []
17
+ user_mentioned_paths = []
18
+
19
+ // 1a — edit signal (rc.5 primary)
12
20
  Scan session transcript for tool_use entries where
13
21
  tool_use.name ∈ {Edit, Write, MultiEdit}
14
22
  Extract the file_path argument from each, push into edit_paths.
15
23
 
16
- Step 2: DEDUPE
17
- edit_paths = unique(edit_paths)
18
-
19
- Step 3: BLACKLIST FILTER
24
+ // 1b — read signal (rc.37 NEW-7 secondary)
25
+ Scan session transcript for tool_use entries where
26
+ tool_use.name ∈ {Read, Grep, Glob}
27
+ Extract the file_path / path / glob argument from each, push into read_paths.
28
+
29
+ // 1c — user-mentioned signal (rc.37 NEW-7 ground truth)
30
+ Scan user messages for token sequences matching workspace-relative
31
+ path patterns: `<segment>/<segment>/...<ext>` or `<segment>/**` or
32
+ ``<path>`` (backtick-quoted). De-dupe and push into user_mentioned_paths.
33
+
34
+ Step 2: DEDUPE + CLASSIFY
35
+ // Union all three sources for the relevance_paths candidate set.
36
+ candidate_paths = unique(edit_paths ∪ user_mentioned_paths)
37
+ // read_paths stay separate — they become evidence_paths (Step 6) rather
38
+ // than activation triggers. A path that appears in BOTH edit_paths and
39
+ // read_paths goes to candidate_paths (writes dominate reads).
40
+ evidence_candidate_paths = unique(read_paths \ edit_paths)
41
+
42
+ Step 3: BLACKLIST FILTER (applies to BOTH candidate sets)
20
43
  Drop paths matching any of:
21
44
  - **/*.<ext> where <ext> is a single trivial extension on a single file
22
45
  (i.e. avoid emitting bare **/*.md as a relevance pattern)
23
46
  - Repo-root single files: README.md, package.json, package-lock.json,
24
47
  pnpm-lock.yaml, tsconfig.json, .gitignore, LICENSE, CHANGELOG.md
25
- - Read-only paths (never modified) — those go to ## Evidence, not relevance_paths
26
48
 
27
49
  Step 4: PUBLIC-PREFIX GENERALIZE (depth ≤ 2, minGroupSize = 2)
28
- Group remaining paths by common prefix.
50
+ Group remaining candidate_paths by common prefix.
29
51
  For each group of ≥ 2 sibling paths sharing a prefix:
30
52
  - Compute longest common directory prefix
31
53
  - Limit generalization depth: at most 2 levels below the common prefix
32
54
  - Emit glob: <common-prefix>/**/*.<ext> (or <common-prefix>/**/<filename>)
33
55
  Singleton paths (group size = 1) are kept as-is (literal path, no glob).
56
+ (Evidence paths are NOT generalized — they stay literal so plan-context
57
+ retrieval can do exact-match recall lookups.)
34
58
 
35
59
  Step 5: SCOPE GATE
36
- IF relevance_scope == broad → relevance_paths = [] (force empty regardless of edit_paths)
60
+ IF relevance_scope == broad → relevance_paths = [] (force empty regardless of candidate_paths)
37
61
  IF relevance_scope == narrow → relevance_paths = result of Step 4
38
62
 
39
- Step 6: ATTACH READ-ONLY EVIDENCE
40
- Read-only paths (filtered in Step 3) are emitted as a ## Evidence markdown
41
- block in the pending entry body — NOT in relevance_paths. They document
42
- what the agent consulted without making them part of the activation gate.
63
+ Step 6: ATTACH evidence_paths to FRONTMATTER (rc.37 NEW-7 upgrade)
64
+ Pass evidence_candidate_paths (from Step 2, post-blacklist Step 3) to
65
+ fab_extract_knowledge as the `evidence_paths` input field. Server writes
66
+ them to frontmatter `evidence_paths: [...]` (NOT to body `## Evidence`).
67
+ This makes evidence consumable by plan-context retrieval as structured
68
+ data instead of forcing markdown re-parsing every recall. The legacy
69
+ body `## Evidence` block stays for back-compat readers but is no longer
70
+ the source of truth.
43
71
  ```
44
72
 
45
73
  ## Worked generalization example
@@ -65,7 +65,7 @@ Classify each candidate into 5 types (decisions/pitfalls/guidelines/models/proce
65
65
 
66
66
  #### Mandatory Scope Rule — broad + empty paths (NON-NEGOTIABLE)
67
67
 
68
- Every call MUST `relevance_scope="broad"` AND `relevance_paths=[]`. No exceptions. Why: import is LLM-driven (not session-driven); LLM-inferred narrow false-narrow that silently hides knowledge. Narrowing deferred to `fab_review.modify` post-import. Full rationale + prohibitions + doctor lint #23 → `Read .../ref/phase-2-mining.md`.
68
+ Every call MUST `relevance_scope="broad"` AND `relevance_paths=[]`. No exceptions. Why: import is LLM-driven (not session-driven); LLM-inferred narrow lies about applicability. Post-rc.37 A1 the server returns every selectable entry regardless of scope, so false-narrow no longer hides knowledge — but it still poisons doctor lint accounting + downstream consumers that read `relevance_paths` literally. Narrowing deferred to `fab_review.modify` post-import when the user has the real applicability surface. Full rationale + prohibitions + doctor lint #23 → `Read .../ref/phase-2-mining.md`.
69
69
 
70
70
  #### Step 2.1 — Git Mining
71
71
 
@@ -85,7 +85,7 @@ Skip if: cosmetic-only / metadata-only / in baseline / not classifiable / slug n
85
85
 
86
86
  #### Dry-Run
87
87
 
88
- Keyword `dry-run` / `预览` / `--dry-run` → skip MCP, render bilingual preview table (every Scope row `broad+[]`). State NOT written. P3 skipped.
88
+ Explicit token `--dry-run` in invocation → skip MCP, render bilingual preview table (every Scope row `broad+[]`). State NOT written. P3 skipped. v2.0.0-rc.37 NEW-10 dropped legacy substring fallback on bare `dry-run` / `预览` (false-positive on incidental mentions).
89
89
 
90
90
  Full MCP call shape, Step 2.1.5 table, dry-run templates, T5 idempotency → `Read .../ref/phase-2-mining.md`.
91
91
 
@@ -144,4 +144,4 @@ Roll-up sections (per `fabric_language`): `Phase 2 — Mining` | `Phase 3 — De
144
144
  - State corruption: P0 detects → rename `.json.corrupt-<ISO>` → restart P1.
145
145
  - MCP unreachable: halt + `MCP 工具未注册;请检查 fabric server 是否运行` / `MCP tool not registered; please check that the fabric server is running` → exit without state write.
146
146
 
147
- Check-not-Ask: inspect state, resume deterministically.
147
+ Resume policy: inspect existing state and continue from the last completed phase — do not prompt the user mid-flow.
@@ -1,5 +1,11 @@
1
1
  # UX i18n Policy — fabric-import full reference
2
2
 
3
+ > **Shared core (rc.37 NEW-13):** the cross-skill invariants — protected-token
4
+ > NEVER-translate list, AskUserQuestion routing-key rule, layer heuristic, and
5
+ > events-emit convention — live once in `../../lib/shared-policy.md`. This file
6
+ > keeps only the fabric-import-specific 5-class examples. Read the shared lib
7
+ > for the common rules; do not fork them here.
8
+
3
9
  > **Loaded on demand.** Only consult when you need to disambiguate which of the 5 classes a given string belongs to. SKILL.md gives the operative rule.
4
10
 
5
11
  ## UX i18n Policy (5-class bilingualization)
@@ -15,7 +15,7 @@ This is non-negotiable and applies to BOTH Step 2.1 (git mining) AND Step 2.2 (d
15
15
 
16
16
  1. `fabric-import` is LLM-driven (mines git log + docs), not session-driven (no live `edit_paths` signal).
17
17
  2. `git diff --stat` lists files touched by a commit, but those files are the commit's **effect surface**, not the **applicability surface** of the underlying observation. A pitfall surfaced by a fix in `packages/server/src/retry.ts` may apply to every retry call-site in the repo, not just that one file.
18
- 3. LLM-inferred `relevance_paths` from historical commit metadata produces false-narrow bindings — entries get filtered out for paths they actually govern. False-narrow is worse than broad because it silently hides knowledge during plan-context filtering.
18
+ 3. LLM-inferred `relevance_paths` from historical commit metadata produces false-narrow bindings — `relevance_paths` becomes a lie about applicability. Post-rc.37 A1 the server no longer filters by `relevance_scope`, so false-narrow does NOT hide knowledge from AI recall (every selectable entry is surfaced regardless of scope). The damage is now downstream: doctor lint accounting, future-AI judgment, and any consumer that reads `relevance_paths` literally treats the wrong globs as ground truth. Broad+[] keeps the metadata honest until the user has the real applicability surface in hand to declare narrow.
19
19
  4. Doc-mined observations are usually architectural / cross-cutting (a `docs/architecture.md` "Why a monolith?" decision applies to the whole codebase, not just to `docs/`).
20
20
 
21
21
  **Strict prohibitions — DO NOT attempt any of the following:**
@@ -174,7 +174,7 @@ After Step 2.2 completes (or hits the cap), update `.fabric/.import-state.json`:
174
174
 
175
175
  ## Dry-Run Mode
176
176
 
177
- When the user invocation includes `dry-run` / `预览` / `--dry-run` keywords, Phase 2 runs WITHOUT calling `fab_extract_knowledge`. Instead it prints a table. UX i18n Policy class 4 — header + column titles bilingualized; row content (slug / commit sha / doc path) NOT translated. Protected tokens `broad`, `relevance_scope`, `relevance_paths` appear verbatim:
177
+ When the user invocation carries the verbatim token `--dry-run`, Phase 2 runs WITHOUT calling `fab_extract_knowledge`. Instead it prints a table. v2.0.0-rc.37 NEW-10 dropped the legacy substring fallback on bare `dry-run` / `预览` because those caused false positives on incidental mentions ("preview the table" / "do a dry run later"). UX i18n Policy class 4 — header + column titles bilingualized; row content (slug / commit sha / doc path) NOT translated. Protected tokens `broad`, `relevance_scope`, `relevance_paths` appear verbatim:
178
178
 
179
179
  ### zh-CN variant (`fabric_language === "zh-CN"`)
180
180
 
@@ -48,46 +48,46 @@ Read `fabric_language` (`zh-CN` / `en` / `zh-CN-hybrid` / `match-existing`); emi
48
48
 
49
49
  ## Mode Inference (System Infers — NEVER Ask)
50
50
 
51
- > Verbatim from rc.3 locked decisions:
52
- > "review 永远走 fabric-review skill,**模式从上下文推断**(4 种 mode:pending queue / by topic / health overview / revisit existing)"
51
+ > Locked decision (KT-DEC-0006): "Review mode inferred from context, not solicited via AskUserQuestion."
53
52
  > "**AskUserQuestion 仅在真有选择时用**——'何种 mode' 不是真选择(系统能推断),'approve/reject/modify 单条' 是真选择"
54
53
 
55
- The skill MUST infer one of {`pending`, `topic`, `health`, `revisit`} BEFORE any user-facing output.
54
+ The skill MUST infer one of **2 modes** BEFORE any user-facing output (v2.0.0-rc.37 NEW-12 simplified 4 → 2):
56
55
 
57
- ### 3-Step Inference Algorithm
56
+ - **`pending`** — triage the write-side backlog (`.fabric/knowledge/pending/`): approve / reject / modify / defer per item. The dominant entry point.
57
+ - **`maintain`** — sustain the EXISTING canonical KB: browse by topic (search), survey staleness/health, or revisit a specific entry. Merges the legacy `topic` + `health` + `revisit` modes — they are all "operate on already-canonical knowledge", distinct from triaging new drafts.
58
58
 
59
- **Step 1 — Recent user message keyword scan.** Match against priority order:
59
+ ### 2-Step Inference Algorithm
60
+
61
+ **Step 1 — Recent user message keyword scan:**
60
62
 
61
63
  | Keywords (zh-CN + en) | Inferred mode |
62
64
  |---|---|
63
65
  | "approve", "review pending", "promote", "what's queued", "审核 pending", "通过" | `pending` |
64
- | "search for X about Y", "find entries about <topic>", "关于…的知识", "找一下 <topic>" | `topic` |
65
- | "what's stale", "demote old", "health check", "过期的", "陈旧的", "整理一下" | `health` |
66
- | "look at <id>", "revisit KT-…", "show <slug>", "再看下 <id>", "回顾" | `revisit` |
67
-
68
- Exactly one row matches → lock mode, skip to Step 3.
69
-
70
- **Step 2 — events.jsonl tail scan.** If Step 1 yielded 0 or >1 matches, tail (last 200 lines) `.fabric/events.jsonl`:
66
+ | "search/find about <topic>", "what's stale", "demote old", "health check", "look at <id>", "revisit KT-…", "关于…的知识", "过期的", "陈旧的", "整理一下", "再看下 <id>", "回顾" | `maintain` |
71
67
 
72
- - `>5` recent `knowledge_proposed` since last `knowledge_promoted` `pending` (write side piled up).
73
- - `≥1` `knowledge_demoted` or `lint` events in 24h → `health` (corpus quality signal).
74
- - Recent `knowledge_layer_changed` for the entry user referenced → `revisit`.
68
+ A `maintain`-row match lock `maintain`. A `pending`-row match (or 0/ambiguous) fall to Step 2.
75
69
 
76
- **Step 3Pending count default.** Still ambiguous → glob `.fabric/knowledge/pending/**/*.md`:
70
+ **Step 2Backlog default.** Glob `.fabric/knowledge/pending/**/*.md`:
77
71
 
78
72
  - Count ≥ `review_hint_pending_count` (default 10) OR oldest mtime > `review_hint_pending_age_days` (default 7) → `pending` (overflow, same threshold as Stop-hook).
79
73
  - Otherwise → default `pending` (most common review entry point).
80
74
 
81
- `Read ref/per-mode-flows.md` for 5 inference examples and anti-pattern restatement.
75
+ > Back-compat: the legacy 4-mode names (`topic` / `health` / `revisit`) still resolve — they all map to `maintain`. Old session traces / muscle memory keep working.
76
+
77
+ `Read ref/per-mode-flows.md` for inference examples and anti-pattern restatement.
82
78
 
83
79
  ## Per-Mode Flow
84
80
 
85
81
  Each mode produces user-facing output, then routes per-item or per-batch decisions through `fab_review` actions. Display body = zh-CN summaries (M3 style); section headings = EN.
86
82
 
87
- - **`pending`** — list pending entries → run Semantic Check (see `ref/semantic-check.md`) → per-item AskUserQuestion `{approve, reject, modify, defer, skip}` → route per choice (modify path = `ref/modify-flow.md`).
88
- - **`topic`** — extract keywords → `fab_review action="search"` render top-N (cap `review_topic_result_cap`) only AskUserQuestion when user signals action verb.
89
- - **`health`** — `fab_review action="list"` + tail events compute stale render dashboard per-stale AskUserQuestion `{defer, demote, skip}`.
90
- - **`revisit`** — user referenced specific id/slug → `Read` canonical file directly OR `fab_review action="list"` with narrow filters display body + history AskUserQuestion only if pending.
83
+ - **`pending`** — list pending entries → run Semantic Check (see `ref/semantic-check.md`) → per-item AskUserQuestion `{approve, reject, modify, defer, skip}` → route per choice. The modify branch chooses between two explicit actions (rc.37 NEW-12):
84
+ - `fab_review action="modify-content"` edit scalars (title/summary/maturity/tags/relevance_*); NEVER flips layer.
85
+ - `fab_review action="modify-layer"` the dedicated layer-flip path (`changes.layer` required); may reallocate the stable_id + emit an id-redirect.
86
+ - (Legacy `action="modify"` still works it routes by whether `changes.layer` is present.) See `ref/modify-flow.md`.
87
+ - **`maintain`** — sub-flow inferred from the same keywords:
88
+ - *browse-by-topic*: extract keywords → `fab_review action="search"` → render top-N (cap `review_topic_result_cap`) → AskUserQuestion only on an action verb.
89
+ - *health/staleness*: `fab_review action="list"` + tail events → compute stale → render dashboard → per-stale AskUserQuestion `{defer, demote, skip}`.
90
+ - *revisit*: user referenced a specific id/slug → `Read` canonical file directly OR `fab_review action="list"` with narrow filters → display body + history → AskUserQuestion only if actionable.
91
91
 
92
92
  `Read ref/per-mode-flows.md` for full step-by-step procedures, bilingual rendering blocks (en + zh-CN per-item display, AskUserQuestion templates, health dashboard format), and the rc.7 T6 `proposed_reason` + `## Why proposed` + `## Session context` rendering contract.
93
93
 
@@ -97,11 +97,17 @@ Each mode produces user-facing output, then routes per-item or per-batch decisio
97
97
 
98
98
  Semantic check is the LLM's job — the MCP tool does NOT compare meaning. Run during `pending` mode (and on demand during `topic`): for each pending entry, `fab_review action="search"` scoped by `filters.type` → LLM judges semantically against returned canonical entries → surface one of three flags as informational:
99
99
 
100
- - `⚠ Possible duplicate of <stable_id>` — same essential claim
101
- - `⚠ Contradicts <stable_id>` — opposing claims, same scope
102
- - `⚠ Subsumed by <stable_id>; consider modify-to-merge` — fully covered
100
+ - `⚠ Possible duplicate of <stable_id> (overlap: high)` — same essential claim
101
+ - `⚠ Contradicts <stable_id> (overlap: high)` — opposing claims, same scope
102
+ - `⚠ Subsumed by <stable_id> (overlap: medium+); consider modify-to-merge` — fully covered
103
+
104
+ **Quantified overlap band (rc.37 NEW-12).** The LLM still judges meaning (no embedding %), but MUST tag each flag with a 3-level band so the signal is comparable across entries and the user can triage at a glance:
105
+
106
+ - `high` — ≥ ~80% of the candidate's essential claim is restated; near-certain dup/contradiction. Recommend reject-as-duplicate or modify-to-merge.
107
+ - `medium` — substantial conceptual overlap but the new entry adds a distinct facet (different path scope, added caveat). Recommend modify-to-harmonize.
108
+ - `low` — adjacent topic, not a real overlap. Do NOT raise a flag at `low` — it is below the surfacing threshold (suppresses noise).
103
109
 
104
- Thresholds intentionally NOT quantified (no similarity %). User decides: still-approve (flag informational), modify-to-harmonize, or reject-as-duplicate (reason MUST cite existing stable_id).
110
+ Only `medium`+ flags are surfaced. User decides: still-approve (flag informational), modify-to-harmonize, or reject-as-duplicate (reason MUST cite existing stable_id).
105
111
 
106
112
  DO NOT AskUserQuestion "is this a duplicate?" — LLM already judged. User only chooses approve/reject/modify.
107
113