@fenglimg/fabric-cli 2.2.0-rc.1 → 2.2.0-rc.4

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 (35) hide show
  1. package/dist/chunk-5JG4QJLO.js +64 -0
  2. package/dist/chunk-5SSNE5GM.js +301 -0
  3. package/dist/chunk-EOT63RDH.js +36 -0
  4. package/dist/{chunk-AOE6AYI7.js → chunk-F6ITRM7T.js} +2 -2
  5. package/dist/{chunk-WU6GAPKH.js → chunk-H3FE6VIK.js} +3 -5
  6. package/dist/chunk-XCBVSGCS.js +25 -0
  7. package/dist/{chunk-2R55HNVD.js → chunk-XHHCRDIR.js} +71 -6
  8. package/dist/{config-XYRBZJDU.js → config-VJMXCLXW.js} +1 -1
  9. package/dist/{doctor-YONYXDX6.js → doctor-U5W4CX5I.js} +118 -7
  10. package/dist/index.js +13 -12
  11. package/dist/{install-74ANPCCP.js → install-7XJ64WSC.js} +252 -246
  12. package/dist/{plan-context-hint-FC6P3WFE.js → plan-context-hint-CHVZGOZ5.js} +21 -8
  13. package/dist/{scope-explain-CDIZESP5.js → scope-explain-BWRWBCCP.js} +14 -4
  14. package/dist/{status-GLQWLWH6.js → status-7UFLWRX7.js} +17 -6
  15. package/dist/store-ZEZMQVG7.js +817 -0
  16. package/dist/{sync-UJ4BBCZJ.js → sync-EA5HZMXM.js} +165 -21
  17. package/dist/{uninstall-C3QXKOO6.js → uninstall-F75MPKQC.js} +27 -1
  18. package/dist/{whoami-2MLO4Y37.js → whoami-3FRWYGML.js} +16 -5
  19. package/package.json +3 -3
  20. package/templates/hooks/cite-policy-evict.cjs +412 -160
  21. package/templates/hooks/configs/claude-code.json +17 -2
  22. package/templates/hooks/configs/codex-hooks.json +14 -2
  23. package/templates/hooks/configs/cursor-hooks.json +14 -2
  24. package/templates/hooks/fabric-hint.cjs +151 -15
  25. package/templates/hooks/knowledge-hint-broad.cjs +12 -1
  26. package/templates/hooks/knowledge-hint-narrow.cjs +54 -1
  27. package/templates/hooks/post-tooluse-mutation.cjs +285 -0
  28. package/templates/hooks/session-end-marker.cjs +140 -0
  29. package/templates/skills/fabric-archive/SKILL.md +7 -1
  30. package/dist/chunk-4R2CYEA4.js +0 -116
  31. package/dist/chunk-L4Q55UC4.js +0 -52
  32. package/dist/chunk-LFIKMVY7.js +0 -27
  33. package/dist/chunk-RYAFBNES.js +0 -33
  34. package/dist/chunk-T5RPGCCM.js +0 -40
  35. package/dist/store-XB3ADT65.js +0 -144
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lifecycle-refactor W2-T3 — PostToolUse mutation marker hook (previously
4
+ * dormant). Closes the mutation env opened by the PreToolUse narrow hint.
5
+ *
6
+ * PostToolUse fires AFTER an Edit/Write/MultiEdit tool call completes. This
7
+ * hook appends one `file_mutated` event per edited path to
8
+ * `.fabric/events.jsonl`, carrying the `tool_call_id` so doctor can pair the
9
+ * Pre (intent) and Post (mutation) halves of a single tool call — the per-call
10
+ * key also guards against parallel-fire races (two concurrent edits never
11
+ * collapse to one ledger key).
12
+ *
13
+ * Design (lifecycle-concept-final.md §1 FROZEN invariants + §5 row7):
14
+ * - LOW compute: extract paths + tool_call_id, append; the hook never reads
15
+ * or aggregates the ledger, never runs `git diff`. ALL mutation_pool /
16
+ * attribution work is doctor-side (offline, §5 row7).
17
+ * - hook = nudge/marker, never a gate (KT-DEC-0007): every error path ends
18
+ * in a silent exit 0; we never throw upward.
19
+ * - Front-stage O(1) per path: advisory-locked append, no traversal.
20
+ * - Per-event session_id: threaded from the REAL payload when present
21
+ * (omitted when the client omits it — the marker is still useful for the
22
+ * tool_call_id pairing even without a session).
23
+ * - Hooks never require() the server package — only co-located lib/*.cjs.
24
+ *
25
+ * Each emitted line matches `fileMutatedEventSchema`
26
+ * (packages/shared/src/schemas/event-ledger.ts):
27
+ * { kind:"fabric-event", id, ts, schema_version:1, session_id?,
28
+ * event_type:"file_mutated", path, tool_call_id, tool_name? }
29
+ *
30
+ * Stdout/stderr are intentionally empty — PostToolUse is observation-only and
31
+ * never blocks the host's tool pipeline.
32
+ */
33
+
34
+ const { randomUUID } = require("node:crypto");
35
+ const { existsSync } = require("node:fs");
36
+ const { isAbsolute, join, relative } = require("node:path");
37
+
38
+ // W1-01 (ISS-011) parity: route every shared-ledger append through the
39
+ // advisory-lock primitive so concurrent PostToolUse fires (multi-window /
40
+ // parallel edits) never interleave a partial line. Best-effort, drop-on-
41
+ // contention — same primitive the narrow/broad hooks use.
42
+ const { appendLockedLine } = require("./lib/injection-log.cjs");
43
+
44
+ const FABRIC_DIR_REL = ".fabric";
45
+ const EVENTS_LEDGER_FILE = "events.jsonl";
46
+
47
+ // Tool names that trigger the mutation marker. PostToolUse fires on many tool
48
+ // names across clients; we only react to the file-edit tools (matches the
49
+ // PreToolUse narrow hint's EDIT_TOOL_NAMES so Pre/Post pair on the same set).
50
+ const EDIT_TOOL_NAMES = new Set(["Edit", "Write", "MultiEdit"]);
51
+
52
+ /**
53
+ * Read stdin (or a test-supplied raw string) as JSON. Returns null on any
54
+ * parse failure — the hook stays silent rather than crashing the tool pipeline.
55
+ */
56
+ function readPayload(rawStdin) {
57
+ if (typeof rawStdin !== "string" || rawStdin.length === 0) return null;
58
+ try {
59
+ const parsed = JSON.parse(rawStdin);
60
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
61
+ return null;
62
+ }
63
+ return parsed;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Extract the tool name. Mirrors the narrow hint's probe:
71
+ * - Claude Code / Codex: { tool_name, ... }
72
+ * - Cursor (legacy): { tool, ... }
73
+ */
74
+ function extractToolName(payload) {
75
+ if (!payload || typeof payload !== "object") return null;
76
+ if (typeof payload.tool_name === "string") return payload.tool_name;
77
+ if (typeof payload.tool === "string") return payload.tool;
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Extract the tool_input object, accepting both the `tool_input`
83
+ * (Claude/Codex) and `input` (Cursor) conventions.
84
+ */
85
+ function extractToolInput(payload) {
86
+ if (!payload || typeof payload !== "object") return null;
87
+ if (payload.tool_input && typeof payload.tool_input === "object") {
88
+ return payload.tool_input;
89
+ }
90
+ if (payload.input && typeof payload.input === "object") {
91
+ return payload.input;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Pull file paths out of a tool_input object. Same three shapes the narrow
98
+ * hint handles (single file_path / array file_paths / MultiEdit edits[]).
99
+ * Returns a deduped array of strings — empty when none recognizable.
100
+ */
101
+ function extractPaths(toolInput) {
102
+ if (!toolInput || typeof toolInput !== "object") return [];
103
+ const collected = [];
104
+
105
+ if (typeof toolInput.file_path === "string" && toolInput.file_path.length > 0) {
106
+ collected.push(toolInput.file_path);
107
+ }
108
+ if (Array.isArray(toolInput.file_paths)) {
109
+ for (const p of toolInput.file_paths) {
110
+ if (typeof p === "string" && p.length > 0) collected.push(p);
111
+ }
112
+ }
113
+ if (Array.isArray(toolInput.edits)) {
114
+ for (const edit of toolInput.edits) {
115
+ if (
116
+ edit &&
117
+ typeof edit === "object" &&
118
+ typeof edit.file_path === "string" &&
119
+ edit.file_path.length > 0
120
+ ) {
121
+ collected.push(edit.file_path);
122
+ }
123
+ }
124
+ }
125
+
126
+ const seen = new Set();
127
+ const out = [];
128
+ for (const p of collected) {
129
+ if (seen.has(p)) continue;
130
+ seen.add(p);
131
+ out.push(p);
132
+ }
133
+ return out;
134
+ }
135
+
136
+ /**
137
+ * Extract the per-call id. Claude Code's PostToolUse payload carries the id of
138
+ * the originating tool_use block as `tool_use_id`; older drafts / other clients
139
+ * variously used `tool_call_id` / `call_id` / `id`. We probe in that order.
140
+ *
141
+ * Returns null when none is present — the caller then synthesizes a best-effort
142
+ * fallback key so the marker still lands (per W2-T3: "缺失则用 best-effort
143
+ * fallback key 但仍 append"). A fallback key cannot pair with the Pre half but
144
+ * still records the mutation, which is strictly better than dropping it.
145
+ */
146
+ function extractToolCallId(payload) {
147
+ if (!payload || typeof payload !== "object") return null;
148
+ const candidates = ["tool_use_id", "tool_call_id", "call_id", "id"];
149
+ for (const key of candidates) {
150
+ const v = payload[key];
151
+ if (typeof v === "string" && v.length > 0) return v;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * Normalize a path to a project-relative, forward-slash form. Drops paths that
158
+ * escape the project tree (mirrors appendEditIntentToLedger in the narrow
159
+ * hook). Returns null for out-of-tree / empty.
160
+ */
161
+ function normalizePath(projectRoot, p) {
162
+ if (typeof p !== "string" || p.length === 0) return null;
163
+ let rel;
164
+ if (isAbsolute(p)) {
165
+ rel = relative(projectRoot, p);
166
+ if (rel.startsWith("..")) return null;
167
+ } else {
168
+ if (p.startsWith("..")) return null;
169
+ rel = p;
170
+ }
171
+ const slashed = rel.split(/[\\/]/).join("/");
172
+ return slashed.length > 0 ? slashed : null;
173
+ }
174
+
175
+ /**
176
+ * Append one `file_mutated` marker per edited path to `.fabric/events.jsonl`.
177
+ * Best-effort:
178
+ * - Skips silently when `.fabric/` does not exist (project not init'd).
179
+ * - Skips silently when there are no in-tree paths.
180
+ * - ANY error (append, JSON throw) is swallowed — never blocks the pipeline.
181
+ *
182
+ * One JSON line per path (PIPE_BUF-atomic). The per-path lines share a single
183
+ * tool_call_id so doctor can group them as one tool call.
184
+ */
185
+ function appendFileMutated(projectRoot, now, paths, toolCallId, toolName, sessionId) {
186
+ try {
187
+ const fabricDir = join(projectRoot, FABRIC_DIR_REL);
188
+ if (!existsSync(fabricDir)) return;
189
+ const pathList = Array.isArray(paths)
190
+ ? paths.map((p) => normalizePath(projectRoot, p)).filter((p) => p !== null)
191
+ : [];
192
+ if (pathList.length === 0) return;
193
+ const tsMs = now instanceof Date ? now.getTime() : Number(now);
194
+ // Best-effort fallback key when the client omits the tool-call id: the
195
+ // mutation is still recorded (can't pair with the Pre half, but the path
196
+ // signal is preserved). Per-fire UUID keeps parallel fires distinct.
197
+ const callId =
198
+ typeof toolCallId === "string" && toolCallId.length > 0
199
+ ? toolCallId
200
+ : `fallback:${randomUUID()}`;
201
+ const validSessionId =
202
+ typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
203
+ const validToolName =
204
+ typeof toolName === "string" && toolName.length > 0 ? toolName : null;
205
+ const lines =
206
+ pathList
207
+ .map((p) =>
208
+ JSON.stringify({
209
+ kind: "fabric-event",
210
+ id: `event:${randomUUID()}`,
211
+ ts: tsMs,
212
+ schema_version: 1,
213
+ ...(validSessionId ? { session_id: validSessionId } : {}),
214
+ event_type: "file_mutated",
215
+ path: p,
216
+ tool_call_id: callId,
217
+ ...(validToolName ? { tool_name: validToolName } : {}),
218
+ }),
219
+ )
220
+ .join("\n") + "\n";
221
+ appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), lines);
222
+ } catch {
223
+ // Silent — marker failure must never block the tool pipeline.
224
+ }
225
+ }
226
+
227
+ // -----------------------------------------------------------------------------
228
+ // Main — invoked as a CLI (require.main === module) and in-process by tests.
229
+ // Wraps the entire flow in try/catch: ANY error → silent exit 0.
230
+ // -----------------------------------------------------------------------------
231
+
232
+ function main(env) {
233
+ try {
234
+ const cwd = (env && env.cwd) || process.cwd();
235
+ const now = (env && env.now) || new Date();
236
+ const payload =
237
+ env && env.payload !== undefined ? env.payload : readPayload(env && env.stdin);
238
+ if (payload === null || payload === undefined) return;
239
+
240
+ const toolName = extractToolName(payload);
241
+ if (!toolName || !EDIT_TOOL_NAMES.has(toolName)) return;
242
+
243
+ const toolInput = extractToolInput(payload);
244
+ const paths = extractPaths(toolInput);
245
+ if (paths.length === 0) return;
246
+
247
+ const toolCallId = extractToolCallId(payload);
248
+ const sessionId =
249
+ payload && typeof payload === "object" && typeof payload.session_id === "string"
250
+ ? payload.session_id
251
+ : null;
252
+
253
+ appendFileMutated(cwd, now, paths, toolCallId, toolName, sessionId);
254
+ } catch {
255
+ // Silent — never block the tool pipeline on hook failure.
256
+ }
257
+ }
258
+
259
+ module.exports = {
260
+ main,
261
+ readPayload,
262
+ extractToolName,
263
+ extractToolInput,
264
+ extractPaths,
265
+ extractToolCallId,
266
+ normalizePath,
267
+ appendFileMutated,
268
+ CONSTANTS: {
269
+ FABRIC_DIR_REL,
270
+ EVENTS_LEDGER_FILE,
271
+ EDIT_TOOL_NAMES,
272
+ },
273
+ };
274
+
275
+ if (require.main === module) {
276
+ // Read stdin synchronously (small hook payloads, no concurrency concerns).
277
+ let stdinRaw = "";
278
+ try {
279
+ stdinRaw = require("node:fs").readFileSync(0, "utf8");
280
+ } catch {
281
+ // No stdin — proceed with empty payload (E*: no append without paths).
282
+ }
283
+ main({ cwd: process.cwd(), now: new Date(), stdin: stdinRaw });
284
+ process.exit(0);
285
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lifecycle-refactor W2-T2 — SessionEnd marker hook (previously dormant).
4
+ *
5
+ * SessionEnd fires once when a client session boots down. This hook is a
6
+ * PURE MARKER: it appends a single `session_ended` event (session_id + ts,
7
+ * via the shared envelope) to `.fabric/events.jsonl` and does NOTHING else.
8
+ *
9
+ * Design (lifecycle-concept-final.md §1 FROZEN invariants + §5 row2):
10
+ * - ZERO compute: the hook never reads/aggregates the ledger. ALL
11
+ * surfaced→cited→edited funnel reconciliation is doctor-side (offline).
12
+ * - hook = nudge/marker, never a gate (KT-DEC-0007): every error path ends
13
+ * in a silent exit 0; we never throw upward.
14
+ * - Front-stage O(1): one advisory-locked append, no traversal.
15
+ * - Per-event session_id: when the client omits session_id we DEGRADE by
16
+ * skipping the append entirely (a marker with no session is useless for
17
+ * the per-session funnel doctor reconstructs).
18
+ * - Hooks never require() the server package — only the co-located lib/*.cjs.
19
+ *
20
+ * The emitted line matches `sessionEndedEventSchema`
21
+ * (packages/shared/src/schemas/event-ledger.ts):
22
+ * { kind:"fabric-event", id, ts, schema_version:1, session_id?, event_type:"session_ended" }
23
+ *
24
+ * Stdout/stderr are intentionally empty — SessionEnd is observation-only.
25
+ */
26
+
27
+ const { randomUUID } = require("node:crypto");
28
+ const { existsSync } = require("node:fs");
29
+ const { join } = require("node:path");
30
+
31
+ // W1-01 (ISS-011) parity: route the shared-ledger append through the
32
+ // advisory-lock primitive so concurrent SessionEnd fires (multi-window) never
33
+ // interleave a partial line. Drop-on-contention, best-effort — same primitive
34
+ // the narrow/broad hooks use.
35
+ const { appendLockedLine } = require("./lib/injection-log.cjs");
36
+
37
+ const FABRIC_DIR_REL = ".fabric";
38
+ const EVENTS_LEDGER_FILE = "events.jsonl";
39
+
40
+ /**
41
+ * Read stdin (or a test-supplied raw string) as JSON. Returns null on any
42
+ * parse failure — the hook stays silent rather than crashing session teardown.
43
+ */
44
+ function readPayload(rawStdin) {
45
+ if (typeof rawStdin !== "string" || rawStdin.length === 0) return null;
46
+ try {
47
+ const parsed = JSON.parse(rawStdin);
48
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
49
+ return null;
50
+ }
51
+ return parsed;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Extract the REAL payload session_id (never a synthetic fallback). The
59
+ * funnel reconciliation in doctor keys on the client's own session id, so a
60
+ * fabricated id would silently corrupt the per-session join. Returns null when
61
+ * absent — the caller then skips the append (degraded, per §1).
62
+ */
63
+ function extractSessionId(payload) {
64
+ if (
65
+ payload &&
66
+ typeof payload === "object" &&
67
+ typeof payload.session_id === "string" &&
68
+ payload.session_id.length > 0
69
+ ) {
70
+ return payload.session_id;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Append one `session_ended` marker to `.fabric/events.jsonl`. Best-effort:
77
+ * - Skips silently when `.fabric/` does not exist (project not init'd).
78
+ * - Skips silently when sessionId is null (degraded — no session to mark).
79
+ * - ANY error (append, JSON throw) is swallowed — never blocks teardown.
80
+ */
81
+ function appendSessionEnded(projectRoot, now, sessionId) {
82
+ try {
83
+ if (typeof sessionId !== "string" || sessionId.length === 0) return;
84
+ const fabricDir = join(projectRoot, FABRIC_DIR_REL);
85
+ if (!existsSync(fabricDir)) return;
86
+ const tsMs = now instanceof Date ? now.getTime() : Number(now);
87
+ const event = {
88
+ kind: "fabric-event",
89
+ id: `event:${randomUUID()}`,
90
+ ts: tsMs,
91
+ schema_version: 1,
92
+ session_id: sessionId,
93
+ event_type: "session_ended",
94
+ };
95
+ appendLockedLine(join(fabricDir, EVENTS_LEDGER_FILE), JSON.stringify(event) + "\n");
96
+ } catch {
97
+ // Silent — marker failure must never block session teardown.
98
+ }
99
+ }
100
+
101
+ // -----------------------------------------------------------------------------
102
+ // Main — invoked as a CLI (require.main === module) and in-process by tests.
103
+ // Wraps the entire flow in try/catch: ANY error → silent exit 0.
104
+ // -----------------------------------------------------------------------------
105
+
106
+ function main(env) {
107
+ try {
108
+ const cwd = (env && env.cwd) || process.cwd();
109
+ const now = (env && env.now) || new Date();
110
+ const payload =
111
+ env && env.payload !== undefined ? env.payload : readPayload(env && env.stdin);
112
+ const sessionId = extractSessionId(payload);
113
+ appendSessionEnded(cwd, now, sessionId);
114
+ } catch {
115
+ // Silent — never block session teardown on hook failure.
116
+ }
117
+ }
118
+
119
+ module.exports = {
120
+ main,
121
+ readPayload,
122
+ extractSessionId,
123
+ appendSessionEnded,
124
+ CONSTANTS: {
125
+ FABRIC_DIR_REL,
126
+ EVENTS_LEDGER_FILE,
127
+ },
128
+ };
129
+
130
+ if (require.main === module) {
131
+ // Read stdin synchronously (small hook payloads, no concurrency concerns).
132
+ let stdinRaw = "";
133
+ try {
134
+ stdinRaw = require("node:fs").readFileSync(0, "utf8");
135
+ } catch {
136
+ // No stdin — proceed with empty payload (degrades to a no-op append).
137
+ }
138
+ main({ cwd: process.cwd(), now: new Date(), stdin: stdinRaw });
139
+ process.exit(0);
140
+ }
@@ -113,6 +113,12 @@ Assign `relevance_scope` ∈ {narrow, broad} + derive `relevance_paths` BEFORE b
113
113
 
114
114
  `Read ref/phase-3-5-scope.md` for the 6-step relevance_paths derivation pseudocode (COLLECT → DEDUPE → BLACKLIST → PUBLIC-PREFIX GENERALIZE → SCOPE GATE → ATTACH READ-ONLY EVIDENCE), worked example (5 sample paths → glob + literal output), and narrow↔broad inline-edit re-derivation rules.
115
115
 
116
+ ### Phase 3.6 — Related-edge Extraction (§7 graph generation)
117
+
118
+ For each candidate, identify the **`related`** graph edges to other KB entries — the store-qualified `stable_id`s this entry semantically links to (the decision it supersedes, the pitfall it explains, the model it instantiates). You discovered these ids during the session via `fab_recall` / plan-context, so cite the ones you actually saw, NEVER invent stable_ids. Because `fab_extract_knowledge` has no dedicated `related` input, record the candidate edges as one line inside `session_context` (e.g. `related: team:KT-DEC-0007, team:KT-PIT-0011`) so they survive to approve-time frontmatter authoring (`fabric-review` writes the canonical `related: [...]` frontmatter).
119
+
120
+ **§4 privacy iron law — KT→KP is FORBIDDEN.** A **team** (`KT-*`) entry's `related` MUST NOT point at a **personal** (`KP-*`) id: that would write a personal-knowledge topology pointer into the project's shared physical ledger (`./.fabric/agents.meta.json`). Allowed: `KT→KT`, `KP→KP`, `KP→KT`. Forbidden: `KT→KP` only. When unsure whether a target is personal, OMIT the edge. (The meta-builder also strips any KT→KP edge that slips through, but do not rely on that — author clean edges.)
121
+
116
122
  ### Phase 4 — Persist via MCP
117
123
 
118
124
  For each user-confirmed candidate, call `fab_extract_knowledge` ONCE (NEVER batch). Required: `source_sessions[]`, `recent_paths[]` (cap 20), `user_messages_summary`, `type` (plural form: decisions/pitfalls/guidelines/models/processes), `slug`, `layer`, `relevance_scope`, `relevance_paths[]`, `proposed_reason` (enum), `session_context` (3-5 line narrative). Four OPTIONAL rc.23 triage fields (`intent_clues`, `tech_stack`, `impact`, `must_read_if`) — populate when clean, **omit rather than guess**.
@@ -155,7 +161,7 @@ MANDATORY closing step on EVERY invocation (Phase 4 success path + every early-e
155
161
  - 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+.
156
162
  - NEVER batch multiple candidates into a single fab_extract_knowledge call; one call per candidate.
157
163
  - NEVER paraphrase the verbatim layer heuristic block above — the Chinese text is contract-locked.
158
- - 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`.
164
+ - 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`, `related`, `KT→KP`.
159
165
 
160
166
  ## Worked Examples / E5 Cron / Dry-run (ref-only)
161
167
 
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadProjectConfig,
4
- saveProjectConfig
5
- } from "./chunk-LFIKMVY7.js";
6
- import {
7
- loadGlobalConfig,
8
- resolveGlobalRoot,
9
- saveGlobalConfig
10
- } from "./chunk-RYAFBNES.js";
11
-
12
- // src/store/store-ops.ts
13
- import { randomUUID } from "crypto";
14
- import { existsSync } from "fs";
15
- import { join } from "path";
16
- import {
17
- addMountedStore,
18
- bindRequiredStore,
19
- detachMountedStore,
20
- explainStore,
21
- initStore,
22
- storeRelativePath
23
- } from "@fenglimg/fabric-shared";
24
- var NO_GLOBAL_CONFIG = "no global Fabric config found \u2014 run `fabric install --global <url>` first";
25
- function requireConfig(globalRoot) {
26
- const config = loadGlobalConfig(globalRoot);
27
- if (config === null) {
28
- throw new Error(NO_GLOBAL_CONFIG);
29
- }
30
- return config;
31
- }
32
- function storeList(globalRoot = resolveGlobalRoot()) {
33
- return requireConfig(globalRoot).stores;
34
- }
35
- function storeAdd(store, globalRoot = resolveGlobalRoot()) {
36
- const next = addMountedStore(requireConfig(globalRoot), store);
37
- saveGlobalConfig(next, globalRoot);
38
- return next;
39
- }
40
- function storeCreate(alias, now, options = {}) {
41
- const globalRoot = options.globalRoot ?? resolveGlobalRoot();
42
- const config = requireConfig(globalRoot);
43
- const uuid = options.uuid ?? randomUUID();
44
- const storeDir = join(globalRoot, storeRelativePath(uuid));
45
- initStore(
46
- storeDir,
47
- { store_uuid: uuid, created_at: now, canonical_alias: alias },
48
- { git: options.git }
49
- );
50
- const mounted = options.remote === void 0 ? { store_uuid: uuid, alias } : { store_uuid: uuid, alias, remote: options.remote };
51
- const next = addMountedStore(config, mounted);
52
- saveGlobalConfig(next, globalRoot);
53
- return { config: next, store_uuid: uuid, storeDir };
54
- }
55
- function assertStoreMountable(uuid, globalRoot = resolveGlobalRoot()) {
56
- const storeDir = join(globalRoot, storeRelativePath(uuid));
57
- if (!existsSync(join(storeDir, "store.json"))) {
58
- throw new Error(
59
- `cannot mount store ${uuid}: no store tree at ${storeDir} \u2014 clone it first (\`fabric install --global --url <remote>\`) or create it locally, then re-run \`fabric store add\`. Refusing to register a phantom store.`
60
- );
61
- }
62
- }
63
- function storeRemove(alias, globalRoot = resolveGlobalRoot()) {
64
- const result = detachMountedStore(requireConfig(globalRoot), alias);
65
- saveGlobalConfig(result.config, globalRoot);
66
- return result;
67
- }
68
- function storeExplain(alias, globalRoot = resolveGlobalRoot()) {
69
- return explainStore(requireConfig(globalRoot), alias);
70
- }
71
- var NO_PROJECT_CONFIG = "no project Fabric config \u2014 run `fabric install` in this repo first";
72
- function requireProjectConfig(projectRoot) {
73
- const config = loadProjectConfig(projectRoot);
74
- if (config === null) {
75
- throw new Error(NO_PROJECT_CONFIG);
76
- }
77
- return config;
78
- }
79
- function storeBind(projectRoot, entry) {
80
- const config = requireProjectConfig(projectRoot);
81
- const next = {
82
- ...config,
83
- required_stores: bindRequiredStore(config.required_stores ?? [], entry)
84
- };
85
- saveProjectConfig(next, projectRoot);
86
- return next;
87
- }
88
- function storeSwitchWrite(projectRoot, alias) {
89
- const config = requireProjectConfig(projectRoot);
90
- const next = { ...config, active_write_store: alias };
91
- saveProjectConfig(next, projectRoot);
92
- return next;
93
- }
94
- function missingRequiredStores(projectRoot, globalRoot = resolveGlobalRoot()) {
95
- const project = loadProjectConfig(projectRoot);
96
- if (project === null || project.required_stores === void 0) {
97
- return [];
98
- }
99
- const global = loadGlobalConfig(globalRoot);
100
- const mounted = new Set(
101
- (global?.stores ?? []).flatMap((s) => [s.alias, s.store_uuid])
102
- );
103
- return project.required_stores.filter((r) => !mounted.has(r.id));
104
- }
105
-
106
- export {
107
- storeList,
108
- storeAdd,
109
- storeCreate,
110
- assertStoreMountable,
111
- storeRemove,
112
- storeExplain,
113
- storeBind,
114
- storeSwitchWrite,
115
- missingRequiredStores
116
- };
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadProjectConfig
4
- } from "./chunk-LFIKMVY7.js";
5
- import {
6
- loadGlobalConfig,
7
- resolveGlobalRoot
8
- } from "./chunk-RYAFBNES.js";
9
-
10
- // src/store/scope-explain.ts
11
- import {
12
- createStoreResolver
13
- } from "@fenglimg/fabric-shared";
14
- function buildResolveInput(projectRoot, globalRoot = resolveGlobalRoot()) {
15
- const global = loadGlobalConfig(globalRoot);
16
- if (global === null) {
17
- return null;
18
- }
19
- const project = loadProjectConfig(projectRoot);
20
- return {
21
- uid: global.uid,
22
- mountedStores: global.stores.map((s) => ({
23
- store_uuid: s.store_uuid,
24
- alias: s.alias,
25
- ...s.remote === void 0 ? {} : { remote: s.remote },
26
- writable: s.writable ?? true,
27
- personal: s.personal ?? false
28
- })),
29
- requiredStores: (project?.required_stores ?? []).map((r) => ({
30
- id: r.id,
31
- ...r.suggested_remote === void 0 ? {} : { suggested_remote: r.suggested_remote }
32
- })),
33
- ...project?.active_write_store === void 0 ? {} : { activeWriteAlias: project.active_write_store }
34
- };
35
- }
36
- function scopeExplain(projectRoot, scope, globalRoot = resolveGlobalRoot()) {
37
- const input = buildResolveInput(projectRoot, globalRoot);
38
- if (input === null) {
39
- return null;
40
- }
41
- const resolver = createStoreResolver();
42
- return {
43
- scope,
44
- readSet: resolver.resolveReadSet(input),
45
- writeTarget: resolver.resolveWriteTarget(input, scope).target
46
- };
47
- }
48
-
49
- export {
50
- buildResolveInput,
51
- scopeExplain
52
- };
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/store/project-config-io.ts
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { join } from "path";
6
- import { fabricConfigSchema } from "@fenglimg/fabric-shared";
7
- function projectConfigPath(projectRoot) {
8
- return join(projectRoot, ".fabric", "fabric-config.json");
9
- }
10
- function loadProjectConfig(projectRoot) {
11
- const path = projectConfigPath(projectRoot);
12
- if (!existsSync(path)) {
13
- return null;
14
- }
15
- return fabricConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
16
- }
17
- function saveProjectConfig(config, projectRoot) {
18
- const validated = fabricConfigSchema.parse(config);
19
- mkdirSync(join(projectRoot, ".fabric"), { recursive: true });
20
- writeFileSync(projectConfigPath(projectRoot), `${JSON.stringify(validated, null, 2)}
21
- `, "utf8");
22
- }
23
-
24
- export {
25
- loadProjectConfig,
26
- saveProjectConfig
27
- };
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/store/global-config-io.ts
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { homedir } from "os";
6
- import { join } from "path";
7
- import { globalConfigSchema } from "@fenglimg/fabric-shared";
8
- function resolveGlobalRoot() {
9
- return join(process.env.FABRIC_HOME ?? homedir(), ".fabric");
10
- }
11
- function globalConfigPath(globalRoot = resolveGlobalRoot()) {
12
- return join(globalRoot, "fabric-global.json");
13
- }
14
- function loadGlobalConfig(globalRoot = resolveGlobalRoot()) {
15
- const path = globalConfigPath(globalRoot);
16
- if (!existsSync(path)) {
17
- return null;
18
- }
19
- return globalConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
20
- }
21
- function saveGlobalConfig(config, globalRoot = resolveGlobalRoot()) {
22
- const validated = globalConfigSchema.parse(config);
23
- mkdirSync(globalRoot, { recursive: true });
24
- writeFileSync(globalConfigPath(globalRoot), `${JSON.stringify(validated, null, 2)}
25
- `, "utf8");
26
- }
27
-
28
- export {
29
- resolveGlobalRoot,
30
- globalConfigPath,
31
- loadGlobalConfig,
32
- saveGlobalConfig
33
- };