@fenglimg/fabric-shared 2.2.0-rc.1 → 2.2.0-rc.10

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.
@@ -1,5 +1,5 @@
1
1
  // src/schemas/api-contracts.ts
2
- import { z as z2 } from "zod";
2
+ import { z as z3 } from "zod";
3
3
 
4
4
  // src/onboard-slots.ts
5
5
  import { z } from "zod";
@@ -13,63 +13,91 @@ var ONBOARD_SLOT_NAMES = [
13
13
  var onboardSlotSchema = z.enum(ONBOARD_SLOT_NAMES);
14
14
  var ONBOARD_SLOT_TOTAL = ONBOARD_SLOT_NAMES.length;
15
15
 
16
+ // src/schemas/scope.ts
17
+ import { z as z2 } from "zod";
18
+ var PERSONAL_SCOPE = "personal";
19
+ var KNOWN_SCOPE_PREFIXES = ["personal", "team", "project", "org"];
20
+ var SCOPE_COORDINATE_PATTERN = /^[a-z0-9_-]+(:[a-z0-9_-]+)*$/u;
21
+ var scopeCoordinateSchema = z2.string().min(1).regex(
22
+ SCOPE_COORDINATE_PATTERN,
23
+ "scope coordinate must be ':'-joined lowercase [a-z0-9_-] segments"
24
+ );
25
+ function scopeRoot(coordinate) {
26
+ const colon = coordinate.indexOf(":");
27
+ return colon === -1 ? coordinate : coordinate.slice(0, colon);
28
+ }
29
+ function isPersonalScope(coordinate) {
30
+ return scopeRoot(coordinate) === PERSONAL_SCOPE;
31
+ }
32
+ var entryScopeMetadataSchema = z2.object({
33
+ semantic_scope: scopeCoordinateSchema,
34
+ // Store alias or UUID. Validated as a non-empty string here; the resolver
35
+ // (P0.6) maps alias→UUID and verifies the store is in the read-set.
36
+ visibility_store: z2.string().min(1)
37
+ }).strict();
38
+
16
39
  // src/schemas/api-contracts.ts
17
- var structuredWarningSchema = z2.object({
18
- code: z2.string(),
19
- file: z2.string(),
20
- line: z2.number().optional(),
21
- action_hint: z2.string()
40
+ var structuredWarningSchema = z3.object({
41
+ code: z3.string(),
42
+ file: z3.string(),
43
+ line: z3.number().optional(),
44
+ message: z3.string().optional(),
45
+ action_hint: z3.string()
22
46
  });
23
- var _knowledgeTypeEnum = z2.enum(["models", "decisions", "guidelines", "pitfalls", "processes"]);
24
- var _maturityEnum = z2.enum(["draft", "verified", "proven"]);
25
- var _layerEnum = z2.enum(["personal", "team"]);
26
- var _ruleDescriptionSchema = z2.object({
27
- summary: z2.string(),
28
- intent_clues: z2.array(z2.string()),
29
- tech_stack: z2.array(z2.string()),
30
- impact: z2.array(z2.string()),
31
- must_read_if: z2.string(),
32
- entities: z2.array(z2.string()).optional(),
47
+ var _knowledgeTypeEnum = z3.enum(["models", "decisions", "guidelines", "pitfalls", "processes"]);
48
+ var _maturityEnum = z3.enum(["draft", "verified", "proven"]);
49
+ var _layerEnum = z3.enum(["personal", "team"]);
50
+ var _ruleDescriptionSchema = z3.object({
51
+ summary: z3.string(),
52
+ intent_clues: z3.array(z3.string()),
53
+ tech_stack: z3.array(z3.string()),
54
+ impact: z3.array(z3.string()),
55
+ must_read_if: z3.string(),
56
+ entities: z3.array(z3.string()).optional(),
33
57
  // v2.0: optional knowledge-entry fields. Absent for v1.x rules; present for
34
58
  // entries that declare frontmatter `id/type/maturity/layer`.
35
- id: z2.string().optional(),
59
+ id: z3.string().optional(),
36
60
  knowledge_type: _knowledgeTypeEnum.optional(),
37
61
  maturity: _maturityEnum.optional(),
38
62
  knowledge_layer: _layerEnum.optional(),
39
- layer_reason: z2.string().optional(),
40
- created_at: z2.string().optional(),
63
+ layer_reason: z3.string().optional(),
64
+ created_at: z3.string().optional(),
41
65
  // v2.0.0-rc.38 UX-3 (D-MCP fold ③): these three were previously carried ONLY
42
66
  // as top-level mirrors on the index item. With the mirrors removed,
43
67
  // `description` becomes their canonical (and only) home, so the schema must
44
68
  // validate them here. Optional + default-safe (tags/[]/broad) so legacy
45
69
  // entries without frontmatter still parse.
46
- tags: z2.array(z2.string()).optional(),
47
- relevance_scope: z2.enum(["narrow", "broad"]).optional(),
48
- relevance_paths: z2.array(z2.string()).optional(),
70
+ tags: z3.array(z3.string()).optional(),
71
+ relevance_scope: z3.enum(["narrow", "broad"]).optional(),
72
+ relevance_paths: z3.array(z3.string()).optional(),
49
73
  // v2.2 H2-related (W1-T7) — W1-REVIEW codex HIGH-2: the MCP-facing description
50
74
  // schema must also carry `related`, else zod strips the graph edges on output
51
75
  // validation and they never reach the client (MC1 include_related / fabric-
52
76
  // connect would see nothing). Mirrors the agents-meta ruleDescriptionSchema.
53
- related: z2.array(z2.string()).optional()
77
+ related: z3.array(z3.string()).optional()
54
78
  });
55
- var _descriptionIndexItemSchema = z2.object({
56
- stable_id: z2.string(),
57
- description: _ruleDescriptionSchema
79
+ var _descriptionIndexItemSchema = z3.object({
80
+ stable_id: z3.string(),
81
+ description: _ruleDescriptionSchema,
82
+ // recall dedupe marker: true when this candidate is ALSO injected in full at
83
+ // SessionStart ("ALWAYS-ACTIVE RULES" = broad model/guideline). MUST be
84
+ // declared here or zod .strip() drops it at the MCP boundary (KT-PIT-0005),
85
+ // silently breaking the marker even though recall() sets it. Only ever true.
86
+ always_active: z3.boolean().optional()
58
87
  });
59
- var _requirementProfileSchema = z2.object({
60
- target_path: z2.string(),
61
- known_tech: z2.array(z2.string()),
62
- user_intent: z2.string(),
63
- detected_entities: z2.array(z2.string())
88
+ var _requirementProfileSchema = z3.object({
89
+ target_path: z3.string(),
90
+ known_tech: z3.array(z3.string()),
91
+ detected_entities: z3.array(z3.string())
64
92
  });
65
- var planContextInputSchema = z2.object({
66
- paths: z2.array(z2.string()).min(1).describe("Candidate file paths to build neutral rule selection context for"),
67
- intent: z2.string().optional().describe("User-stated requirement or implementation intent; used only to build a neutral requirement profile"),
68
- known_tech: z2.array(z2.string()).optional().describe("Known technologies involved in the requirement profile"),
69
- detected_entities: z2.record(z2.array(z2.string())).optional().describe("Optional path-keyed detected entities for the requirement profile"),
70
- client_hash: z2.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection"),
71
- correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
72
- session_id: z2.string().optional().describe(
93
+ var planContextInputSchema = z3.object({
94
+ paths: z3.array(z3.string()).min(1).describe("Candidate file paths to build neutral rule selection context for"),
95
+ intent: z3.string().optional().describe("User-stated requirement or implementation intent; used only to build a neutral requirement profile"),
96
+ known_tech: z3.array(z3.string()).optional().describe("Known technologies involved in the requirement profile"),
97
+ detected_entities: z3.record(z3.array(z3.string())).optional().describe("Optional path-keyed detected entities for the requirement profile"),
98
+ client_hash: z3.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection"),
99
+ correlation_id: z3.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
100
+ session_id: z3.string().optional().describe(
73
101
  "Recommended: pass the current client session id (Claude Code: $session_id; Codex: corresponding identifier) \u2014 enables cross-session debt tracking in fabric doctor and accurate archive-hint cross-session count. Falls back gracefully if omitted."
74
102
  ),
75
103
  // v2.0-rc.5 A3 (TASK-007): `include_deprecated` removed — it was a no-op
@@ -78,55 +106,58 @@ var planContextInputSchema = z2.object({
78
106
  // v2/rc.2 (Q6): client-supplied layer scope. When omitted, the server
79
107
  // falls back to fabric-config.default_layer_filter (TASK-002) so a single
80
108
  // workspace policy controls the default. Explicit values override.
81
- layer_filter: z2.enum(["team", "personal", "both"]).optional().describe(
109
+ layer_filter: z3.enum(["team", "personal", "both"]).optional().describe(
82
110
  "Restrict description_index to the named layer. Default: fabric-config.default_layer_filter (TASK-002)."
83
111
  ),
84
112
  // v2.0-rc.5 C3 (TASK-012): explicit path context for `narrow` relevance
85
113
  // filtering. When omitted, the server falls back to `paths` so existing
86
114
  // callers see narrowing against the requested paths. When the resolved
87
115
  // list is empty, the narrow filter fails open (every narrow entry passes).
88
- target_paths: z2.array(z2.string()).optional().describe(
116
+ target_paths: z3.array(z3.string()).optional().describe(
89
117
  "Path context for narrow-scope relevance filtering. Defaults to `paths`; empty = no filter."
90
118
  )
91
119
  });
92
- var _preflightDiagnosticSchema = z2.object({
120
+ var _preflightDiagnosticSchema = z3.object({
93
121
  // v2.0.0-rc.38 UX-2: `empty_shell_suppressed` surfaces draft entries whose
94
122
  // description carries no selection signal (summary === stable_id + empty
95
123
  // intent_clues/tech_stack/impact). They are filtered out of `candidates` to
96
124
  // cut noise; this diagnostic names them so `fabric doctor` /
97
125
  // --enrich-descriptions can prompt enrichment.
98
- code: z2.enum(["missing_description", "empty_shell_suppressed"]),
99
- severity: z2.literal("warn"),
100
- message: z2.string(),
101
- stable_ids: z2.array(z2.string()).optional(),
102
- path: z2.string().optional()
126
+ code: z3.enum(["missing_description", "empty_shell_suppressed"]),
127
+ severity: z3.literal("warn"),
128
+ message: z3.string(),
129
+ stable_ids: z3.array(z3.string()).optional(),
130
+ path: z3.string().optional()
103
131
  });
104
- var planContextOutputSchema = z2.object({
105
- revision_hash: z2.string(),
106
- stale: z2.boolean(),
107
- selection_token: z2.string(),
108
- entries: z2.array(
109
- z2.object({
110
- path: z2.string(),
132
+ var planContextOutputSchema = z3.object({
133
+ revision_hash: z3.string(),
134
+ stale: z3.boolean(),
135
+ selection_token: z3.string(),
136
+ entries: z3.array(
137
+ z3.object({
138
+ path: z3.string(),
111
139
  requirement_profile: _requirementProfileSchema
112
140
  })
113
141
  ),
114
- candidates: z2.array(_descriptionIndexItemSchema),
142
+ // v2.2 payload de-dup: single top-level echo of the caller's `intent` (was
143
+ // duplicated into every entry's requirement_profile). Omitted when no intent.
144
+ intent: z3.string().optional(),
145
+ candidates: z3.array(_descriptionIndexItemSchema),
115
146
  // v2.2 A-INFRA-3 (W1-T3-TOPK) / MC4-payload-budget (W1-T4): number of
116
147
  // lower-ranked candidates dropped by the unified truncation chain (top_k cap
117
148
  // + payload-budget trim). Present and > 0 ONLY when truncation fired, so the
118
149
  // steady-state wire shape is unchanged. Lets the LLM know the returned set is
119
150
  // not exhaustive ("N more exist; narrow your intent").
120
- omitted_candidate_count: z2.number().int().nonnegative().optional(),
121
- preflight_diagnostics: z2.array(_preflightDiagnosticSchema),
122
- warnings: z2.array(structuredWarningSchema).optional(),
151
+ omitted_candidate_count: z3.number().int().nonnegative().optional(),
152
+ preflight_diagnostics: z3.array(_preflightDiagnosticSchema),
153
+ warnings: z3.array(structuredWarningSchema).optional(),
123
154
  // v2.0.0-rc.22 Scope D T-D2: optional auto-heal banner fields. Surfaced
124
155
  // ONLY when the loadActiveMetaOrStale call detected drift and rebuilt the
125
156
  // meta in-place. Downstream CLI / hint renderers use this pair to render a
126
157
  // "knowledge meta auto-healed (was <prev>, now <curr>)" notice without
127
158
  // having to query the event ledger.
128
- auto_healed: z2.boolean().optional(),
129
- previous_revision_hash: z2.string().optional(),
159
+ auto_healed: z3.boolean().optional(),
160
+ previous_revision_hash: z3.string().optional(),
130
161
  // v2.0.0-rc.37 NEW-24: stale-id redirect map. Populated when one or more
131
162
  // recent fab_review modify-layer flips reassigned a canonical stable_id
132
163
  // and the NEW id is in this response's description_index. Callers that
@@ -134,7 +165,13 @@ var planContextOutputSchema = z2.object({
134
165
  // the new id before issuing fab_get_knowledge_sections / fab_recall. Empty
135
166
  // (field omitted) when no actionable redirects exist for the surfaced
136
167
  // candidate set. See packages/server/src/services/id-redirect.ts.
137
- redirects: z2.record(z2.string()).optional()
168
+ redirects: z3.record(z3.string()).optional(),
169
+ // lifecycle-refactor W3-T2 (§7 图谱消费): related-expansion provenance map
170
+ // (appended id → surfaced source id). Present only when `include_related` was
171
+ // requested AND at least one in-corpus one-hop neighbour was appended. Omitted
172
+ // on the graph-empty / steady-state path. Additive — declare it here or zod
173
+ // strips it on output validation.
174
+ related_appended: z3.record(z3.string()).optional()
138
175
  });
139
176
  var planContextAnnotations = {
140
177
  readOnlyHint: true,
@@ -143,63 +180,69 @@ var planContextAnnotations = {
143
180
  openWorldHint: false,
144
181
  title: "Plan rule context"
145
182
  };
146
- var planContextHintNarrowEntrySchema = z2.object({
147
- id: z2.string(),
148
- type: z2.string(),
149
- maturity: z2.string(),
150
- summary: z2.string()
183
+ var planContextHintNarrowEntrySchema = z3.object({
184
+ id: z3.string(),
185
+ type: z3.string(),
186
+ maturity: z3.string(),
187
+ summary: z3.string(),
188
+ // W2-2 (KT-DEC-0027): the entry's must_read_if trigger hook, forwarded for the
189
+ // SessionStart REFERENCE rendering (decision/pitfall/process → title + hook).
190
+ // Optional — omitted when the frontmatter declares none.
191
+ must_read_if: z3.string().optional()
151
192
  });
152
- var planContextHintOutputSchema = z2.object({
153
- version: z2.literal(1),
154
- revision_hash: z2.string(),
155
- target_paths: z2.array(z2.string()),
156
- narrow: z2.array(planContextHintNarrowEntrySchema),
157
- broad_count: z2.number().int().nonnegative()
193
+ var planContextHintOutputSchema = z3.object({
194
+ version: z3.literal(1),
195
+ revision_hash: z3.string(),
196
+ target_paths: z3.array(z3.string()),
197
+ narrow: z3.array(planContextHintNarrowEntrySchema),
198
+ broad_count: z3.number().int().nonnegative()
158
199
  });
159
- var knowledgeSectionsInputSchema = z2.object({
160
- selection_token: z2.string().min(1).describe("Selection token returned by fab_plan_context"),
161
- ai_selected_stable_ids: z2.array(z2.string()).describe(
200
+ var knowledgeSectionsInputSchema = z3.object({
201
+ selection_token: z3.string().min(1).describe("Selection token returned by fab_plan_context"),
202
+ ai_selected_stable_ids: z3.array(z3.string()).describe(
162
203
  "Stable ids picked from fab_plan_context candidates[].stable_id; choose 1..N to fetch bodies for"
163
204
  ),
164
- ai_selection_reasons: z2.record(z2.string().min(1)).describe("Reason for each AI-selected L1 stable_id"),
165
- correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
166
- session_id: z2.string().optional().describe("Optional caller-provided session id for Event Ledger records"),
205
+ ai_selection_reasons: z3.record(z3.string().min(1)).optional().default({}).describe(
206
+ "Optional reason for each AI-selected L1 stable_id (audit telemetry). Omit to fetch bodies without annotating \u2014 server defaults to {} rather than rejecting the documented two-step call."
207
+ ),
208
+ correlation_id: z3.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
209
+ session_id: z3.string().optional().describe("Optional caller-provided session id for Event Ledger records"),
167
210
  // v2.0 rc.5 TASK-014 (C5): optional client identity hash propagated into
168
211
  // knowledge_consumed events. Falls back to empty string when unset — full
169
212
  // client-identity propagation deferred to rc.6.
170
- client_hash: z2.string().optional().describe("Optional caller-provided client hash propagated into knowledge_consumed events")
213
+ client_hash: z3.string().optional().describe("Optional caller-provided client hash propagated into knowledge_consumed events")
171
214
  });
172
- var knowledgeSectionsOutputSchema = z2.object({
173
- revision_hash: z2.string(),
215
+ var knowledgeSectionsOutputSchema = z3.object({
216
+ revision_hash: z3.string(),
174
217
  // v2.0.0-rc.38 UX-13 (D-MCP step-2 audit): the deprecated `precedence`
175
218
  // L2/L1/L0 tuple (flagged "removed in rc.24" but still emitted) is gone — it
176
219
  // was a constant 3-string field on every response read by no production
177
- // consumer. Use rules[].level for ordering.
178
- selected_stable_ids: z2.array(z2.string()),
179
- rules: z2.array(
180
- z2.object({
181
- stable_id: z2.string(),
182
- level: z2.enum(["L0", "L1", "L2"]),
183
- path: z2.string(),
220
+ // consumer. v2.0.0-rc.38 Goal B: the dead L0/L1/L2 `level` axis was retired
221
+ // too (dead-write — no consumer ordered by it).
222
+ selected_stable_ids: z3.array(z3.string()),
223
+ rules: z3.array(
224
+ z3.object({
225
+ stable_id: z3.string(),
226
+ path: z3.string(),
184
227
  // v2.0.0-rc.23 TASK-013 (F8b): replaced the legacy
185
228
  // `sections: Record<string,string>` (keyed by the 4-element A-set enum)
186
229
  // with the full markdown body (frontmatter stripped). Callers scan the
187
230
  // body for whichever B-set heading they need (Summary / Why proposed /
188
231
  // Session context / Evidence) — section-name discipline is now a writer
189
232
  // convention, not an API contract.
190
- body: z2.string()
233
+ body: z3.string()
191
234
  })
192
235
  ),
193
- diagnostics: z2.array(
236
+ diagnostics: z3.array(
194
237
  // v2.0.0-rc.23 TASK-013 (F8b): `missing_section` was removed alongside the
195
238
  // A-set enum. `missing_knowledge_metadata` stays as the warn-level signal
196
239
  // for un-migrated v1.x entries (no knowledge_type AND no knowledge_layer
197
240
  // in frontmatter). Does NOT block selection.
198
- z2.object({
199
- code: z2.literal("missing_knowledge_metadata"),
200
- severity: z2.literal("warn"),
201
- stable_id: z2.string(),
202
- message: z2.string()
241
+ z3.object({
242
+ code: z3.enum(["missing_knowledge_metadata", "unresolved_selected_id"]),
243
+ severity: z3.literal("warn"),
244
+ stable_id: z3.string(),
245
+ message: z3.string()
203
246
  })
204
247
  ),
205
248
  // v2/rc.3 (Q6) + v2.0.0-rc.37 NEW-24: present iff at least one stable_id in
@@ -209,105 +252,96 @@ var knowledgeSectionsOutputSchema = z2.object({
209
252
  // (old_id → new_id) when multiple rewrites fire in one fetch. Both shapes
210
253
  // are accepted for forward-compat; readers should branch on shape and
211
254
  // refresh their cached ids accordingly.
212
- redirect_to: z2.union([
213
- z2.object({ stable_id: z2.string() }),
214
- z2.record(z2.string())
255
+ redirect_to: z3.union([
256
+ z3.object({ stable_id: z3.string() }),
257
+ z3.record(z3.string())
215
258
  ]).optional().describe(
216
259
  "Post-layer-flip redirect. Pre-rc.37: { stable_id } shape from rc.3 fab_review/modify. rc.37+: also accepts a (old_id \u2192 new_id) map for fab_get_knowledge_sections / fab_recall transparent rewrite."
217
260
  ),
218
- warnings: z2.array(structuredWarningSchema).optional()
261
+ warnings: z3.array(structuredWarningSchema).optional()
219
262
  });
220
263
  var knowledgeSectionsAnnotations = {
221
264
  readOnlyHint: true,
222
265
  idempotentHint: true,
223
266
  destructiveHint: false,
224
267
  openWorldHint: false,
225
- title: "Filter rule sections"
268
+ title: "Fetch knowledge entry bodies"
226
269
  };
227
- var recallInputSchema = z2.object({
228
- paths: z2.array(z2.string()).min(1).describe(
229
- "Candidate file paths to recall Fabric rules for. Same semantics as fab_plan_context.paths."
230
- ),
231
- intent: z2.string().optional().describe("User-stated requirement or implementation intent; used to build a neutral requirement profile."),
232
- known_tech: z2.array(z2.string()).optional().describe("Known technologies involved."),
233
- detected_entities: z2.record(z2.array(z2.string())).optional().describe("Optional path-keyed detected entities."),
234
- client_hash: z2.string().optional().describe("Revision hash from a prior call; enables stale detection."),
235
- correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
236
- session_id: z2.string().optional().describe(
270
+ var recallInputSchema = z3.object({
271
+ paths: z3.array(z3.string()).min(1).describe(
272
+ "Candidate file paths to recall Fabric knowledge entries for. Same semantics as fab_plan_context.paths."
273
+ ),
274
+ intent: z3.string().optional().describe("User-stated requirement or implementation intent; used to build a neutral requirement profile."),
275
+ known_tech: z3.array(z3.string()).optional().describe("Known technologies involved."),
276
+ detected_entities: z3.record(z3.array(z3.string())).optional().describe("Optional path-keyed detected entities."),
277
+ client_hash: z3.string().optional().describe("Revision hash from a prior call; enables stale detection."),
278
+ correlation_id: z3.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
279
+ session_id: z3.string().optional().describe(
237
280
  "Current client session id (Claude Code: $session_id; Codex: corresponding identifier). Enables cross-session debt tracking. Falls back gracefully if omitted."
238
281
  ),
239
- layer_filter: z2.enum(["team", "personal", "both"]).optional().describe(
282
+ layer_filter: z3.enum(["team", "personal", "both"]).optional().describe(
240
283
  "Restrict recall to the named layer. Default: fabric-config.default_layer_filter."
241
284
  ),
242
- target_paths: z2.array(z2.string()).optional().describe(
285
+ target_paths: z3.array(z3.string()).optional().describe(
243
286
  "Path context for narrow-scope relevance filtering. Defaults to `paths`; empty = no filter."
244
287
  ),
245
- ids: z2.array(z2.string()).optional().describe(
246
- "Optional explicit stable_ids to fetch. When omitted, fab_recall returns ALL entries plan-context surfaces (the common case after rc.37 selectable-filter removal). When provided, filters fetched bodies to this set."
288
+ ids: z3.array(z3.string()).optional().describe(
289
+ "Optional explicit stable_ids to SCOPE the returned read paths. When omitted, `paths` carries one read path per surfaced candidate. The candidate DESCRIPTION index is always returned in full for discovery \u2014 `ids` only narrows which read paths are surfaced (e.g. when you already know which entries to Read). Stale ids are redirect-rewritten before matching."
247
290
  ),
248
- // v2.2 MC1-recall-pack (W2-T4): graph expansion.
249
- include_related: z2.boolean().optional().describe(
250
- "When true, also fetch the one-hop `related` graph neighbours (of the selected entries) that are present in the candidate set. Useful with a scoped `ids` to pull connected knowledge in one call."
291
+ // W1-3 / KT-DEC-0031: graph expansion (surface related read paths, no body).
292
+ include_related: z3.boolean().optional().describe(
293
+ "When true, also surface the one-hop `related` graph neighbours (of the surfaced entries) that are present in the candidate set \u2014 their descriptions and read paths, NOT their bodies."
251
294
  )
252
295
  });
253
- var recallOutputSchema = z2.object({
254
- revision_hash: z2.string(),
255
- stale: z2.boolean(),
256
- // Selection token surfaced for callers who want to continue the conversation
257
- // with fab_get_knowledge_sections (e.g. fetch additional ids later) — every
258
- // recall response is still token-backed internally.
259
- selection_token: z2.string(),
296
+ var recallOutputSchema = z3.object({
297
+ revision_hash: z3.string(),
298
+ stale: z3.boolean(),
260
299
  // v2.0.0-rc.38 UX-1/UX-4: mirrors planContextOutputSchema fold ① — per-path
261
300
  // description_index collapsed into a single top-level `candidates`, and
262
301
  // `preflight_diagnostics` lifted out of the removed `shared` wrapper.
263
- entries: z2.array(
264
- z2.object({
265
- path: z2.string(),
302
+ entries: z3.array(
303
+ z3.object({
304
+ path: z3.string(),
266
305
  requirement_profile: _requirementProfileSchema
267
306
  })
268
307
  ),
269
- candidates: z2.array(_descriptionIndexItemSchema),
270
- // v2.2 W1-REVIEW codex MED-5: fab_recall spreads `...planResult`, so it carries
271
- // the truncation signal too. Declare it here or zod strips it on output
272
- // validation the RECOMMENDED one-step entry would silently lose the "more
273
- // candidates exist" signal that plan_context surfaces.
274
- omitted_candidate_count: z2.number().int().nonnegative().optional(),
275
- preflight_diagnostics: z2.array(_preflightDiagnosticSchema),
276
- // Same shape as knowledgeSectionsOutputSchema.rules full body keyed by stable_id.
277
- rules: z2.array(
278
- z2.object({
279
- stable_id: z2.string(),
280
- level: z2.enum(["L0", "L1", "L2"]),
281
- path: z2.string(),
282
- body: z2.string()
283
- })
284
- ),
285
- selected_stable_ids: z2.array(z2.string()),
286
- diagnostics: z2.array(
287
- z2.object({
288
- code: z2.literal("missing_knowledge_metadata"),
289
- severity: z2.literal("warn"),
290
- stable_id: z2.string(),
291
- message: z2.string()
308
+ // v2.2 payload de-dup: single top-level echo of the caller's `intent` (was
309
+ // duplicated into every entry's requirement_profile). Omitted when no intent.
310
+ intent: z3.string().optional(),
311
+ // W1 (KT-DEC-0026): the discovery index every surfaced candidate's
312
+ // DESCRIPTION (summary / intent_clues / must_read_if / related ...). No body.
313
+ candidates: z3.array(_descriptionIndexItemSchema),
314
+ // W1 (KT-DEC-0026): the read-path index — one entry per surfaced candidate
315
+ // (scoped by `ids` when provided), in ranked order. `path` is the on-disk
316
+ // knowledge file the agent Reads to load the body on demand; `store` is the
317
+ // originating store alias (omitted for unqualified entries).
318
+ paths: z3.array(
319
+ z3.object({
320
+ stable_id: z3.string(),
321
+ path: z3.string(),
322
+ store: z3.object({ alias: z3.string() }).optional()
292
323
  })
293
324
  ),
294
- warnings: z2.array(structuredWarningSchema).optional(),
295
- auto_healed: z2.boolean().optional(),
296
- previous_revision_hash: z2.string().optional(),
297
- // v2.0.0-rc.37 NEW-24: parallel to planContextOutputSchema.redirects — see
298
- // that field for semantics. fab_recall transparently rewrites any old ids
299
- // passed via `ids` before fetching bodies; the surfaced map still exposes
300
- // the substitution to callers that want to refresh their cached state.
301
- redirects: z2.record(z2.string()).optional(),
302
- // v2.2 MC1-recall-pack (W2-T4): packaging incrementsa standing behavioral
303
- // directive (cite-before-edit), dynamic next-step hints, and a truncation
304
- // summary, so the one-call recall is self-describing.
305
- directive: z2.string(),
306
- next_steps: z2.array(z2.string()).optional(),
307
- truncation: z2.object({
308
- omitted_candidate_count: z2.number().int().nonnegative(),
309
- returned_candidate_count: z2.number().int().nonnegative()
310
- }).optional()
325
+ // Number of lower-ranked candidates dropped by the retrieval budget. Present
326
+ // (and > 0) ONLY when truncation fired — keeps the steady-state wire shape
327
+ // unchanged while signalling "more exist; narrow your intent".
328
+ omitted_candidate_count: z3.number().int().nonnegative().optional(),
329
+ preflight_diagnostics: z3.array(_preflightDiagnosticSchema),
330
+ warnings: z3.array(structuredWarningSchema).optional(),
331
+ auto_healed: z3.boolean().optional(),
332
+ previous_revision_hash: z3.string().optional(),
333
+ // v2.0.0-rc.37 NEW-24: parallel to planContextOutputSchema.redirects stale
334
+ // (pre layer-flip) ids in `ids` are redirect-rewritten before matching; the
335
+ // surfaced map exposes the substitution so callers refresh cached state.
336
+ redirects: z3.record(z3.string()).optional(),
337
+ // lifecycle-refactor W3-T2 (§7 图谱消费): related-expansion provenance map
338
+ // (appended id → surfaced source id). Present only when include_related
339
+ // appended an in-corpus neighbour. Omitted on the steady-state path.
340
+ related_appended: z3.record(z3.string()).optional(),
341
+ // v2.2 MC1-recall-pack: standing behavioral directive (cite-before-edit) +
342
+ // dynamic discovery hints, so the one-call recall is self-describing.
343
+ directive: z3.string(),
344
+ next_steps: z3.array(z3.string()).optional()
311
345
  });
312
346
  var recallAnnotations = {
313
347
  readOnlyHint: true,
@@ -316,34 +350,34 @@ var recallAnnotations = {
316
350
  openWorldHint: false,
317
351
  title: "Recall Fabric knowledge (one-call)"
318
352
  };
319
- var archiveScanInputSchema = z2.object({
320
- range: z2.union([z2.array(z2.string()).min(1), z2.literal("all")]).optional().describe(
353
+ var archiveScanInputSchema = z3.object({
354
+ range: z3.union([z3.array(z3.string()).min(1), z3.literal("all")]).optional().describe(
321
355
  "Phase 0 scope: explicit session_id[] to constrain the scan, or the 'all' sentinel. Omitted = scan everything since the last knowledge_proposed anchor."
322
356
  ),
323
- now_ms: z2.number().int().nonnegative().optional().describe("Override for the anti-loop cooldown clock (testing). Defaults to Date.now()."),
324
- correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
325
- session_id: z2.string().optional().describe("Current client session id; recorded for cross-session debt tracking.")
357
+ now_ms: z3.number().int().nonnegative().optional().describe("Override for the anti-loop cooldown clock (testing). Defaults to Date.now()."),
358
+ correlation_id: z3.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
359
+ session_id: z3.string().optional().describe("Current client session id; recorded for cross-session debt tracking.")
326
360
  });
327
- var archiveScanOutputSchema = z2.object({
361
+ var archiveScanOutputSchema = z3.object({
328
362
  // ts of the most recent knowledge_proposed event (the lower bound), or null
329
363
  // when the workspace has never archived (scan everything).
330
- anchor_ts: z2.number().nullable(),
364
+ anchor_ts: z3.number().nullable(),
331
365
  // Distinct session_ids since the anchor that survived the outcome filter,
332
366
  // in first-seen order — ready for the Skill to load digests + stitch.
333
- session_ids: z2.array(z2.string()),
367
+ session_ids: z3.array(z3.string()),
334
368
  // Sessions dropped by the filter, with the rule that fired (audit/debug).
335
- dropped: z2.array(
336
- z2.object({
337
- session_id: z2.string(),
338
- reason: z2.enum(["user_dismissed", "cooldown", "no_new_signal"])
369
+ dropped: z3.array(
370
+ z3.object({
371
+ session_id: z3.string(),
372
+ reason: z3.enum(["user_dismissed", "cooldown", "no_new_signal"])
339
373
  })
340
374
  ),
341
375
  // max ts examined across the scan — becomes the next covered_through_ts.
342
- covered_through_ts: z2.number().nullable(),
376
+ covered_through_ts: z3.number().nullable(),
343
377
  // Idempotency keys already proposed by prior archive runs but not yet
344
378
  // reviewed (Phase 4.5 cross-session pending dedupe). Drop matching candidates.
345
- already_proposed_keys: z2.array(z2.string()),
346
- warnings: z2.array(structuredWarningSchema).optional()
379
+ already_proposed_keys: z3.array(z3.string()),
380
+ warnings: z3.array(structuredWarningSchema).optional()
347
381
  });
348
382
  var archiveScanAnnotations = {
349
383
  readOnlyHint: true,
@@ -352,7 +386,7 @@ var archiveScanAnnotations = {
352
386
  openWorldHint: false,
353
387
  title: "Scan event ledger for archive candidates (deterministic)"
354
388
  };
355
- var ProposedReasonSchema = z2.enum([
389
+ var ProposedReasonSchema = z3.enum([
356
390
  "explicit-user-mark",
357
391
  "diagnostic-then-fix",
358
392
  "decision-confirmation",
@@ -360,7 +394,7 @@ var ProposedReasonSchema = z2.enum([
360
394
  "new-dependency-or-pattern",
361
395
  "dismissal-with-reason"
362
396
  ]);
363
- var PROPOSED_REASON_DESCRIPTIONS = {
397
+ var PROPOSED_REASON_DESCRIPTIONS_ZH = {
364
398
  "explicit-user-mark": "\u7528\u6237\u663E\u5F0F\u6807\u8BB0\u9700\u5F52\u6863\uFF08always / never / \u4E0B\u6B21\u6CE8\u610F \u7B49\u89C4\u8303\u6027\u8BED\u8A00\uFF09\u3002",
365
399
  "diagnostic-then-fix": "\u8BCA\u65AD\u8FC7\u7A0B\u53D1\u73B0\u65B0\u6A21\u5F0F\u6216\u8E29\u5751\uFF0C\u4FEE\u590D\u540E\u503C\u5F97\u6C89\u6DC0\u3002",
366
400
  "decision-confirmation": "\u22652 \u5019\u9009\u65B9\u6848\u7ECF\u6743\u8861\u540E\u786E\u8BA4\u9009\u578B\uFF0C\u9700\u4FDD\u7559 rationale\u3002",
@@ -368,21 +402,41 @@ var PROPOSED_REASON_DESCRIPTIONS = {
368
402
  "new-dependency-or-pattern": "\u5F15\u5165\u65B0\u4F9D\u8D56 / \u65B0\u6A21\u5F0F / \u65B0\u547D\u540D\u7EA6\u5B9A\u3002",
369
403
  "dismissal-with-reason": "\u7528\u6237\u660E\u786E\u62D2\u7EDD\u67D0\u65B9\u6848\u5E76\u7ED9\u51FA\u539F\u56E0\uFF0C\u539F\u56E0\u5373\u53EF\u5F52\u6863\u77E5\u8BC6\u3002"
370
404
  };
371
- var _sourceSessionsField = z2.array(z2.string().min(1)).min(1);
372
- var _FabExtractKnowledgeInputBaseSchema = z2.object({
405
+ var PROPOSED_REASON_DESCRIPTIONS_EN = {
406
+ "explicit-user-mark": "User explicitly marked this for archival (normative language: always / never / next time, etc.).",
407
+ "diagnostic-then-fix": "A new pattern or pitfall surfaced during diagnosis and is worth retaining after the fix.",
408
+ "decision-confirmation": "A choice was confirmed after weighing \u22652 candidate approaches; the rationale must be preserved.",
409
+ "wrong-turn-revert": "A path was tried then reverted; the wrong turn itself is a pitfall worth recording.",
410
+ "new-dependency-or-pattern": "Introduces a new dependency / pattern / naming convention.",
411
+ "dismissal-with-reason": "The user explicitly rejected an approach and gave a reason; the reason is archivable knowledge."
412
+ };
413
+ var PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE = {
414
+ "zh-CN": PROPOSED_REASON_DESCRIPTIONS_ZH,
415
+ en: PROPOSED_REASON_DESCRIPTIONS_EN
416
+ };
417
+ var _sourceSessionsField = z3.array(z3.string().min(1)).min(1);
418
+ var _FabExtractKnowledgeInputBaseSchema = z3.object({
373
419
  // v2.0.0-rc.7 T5: array form. rc.23 dropped the legacy single-string alias.
374
- source_sessions: _sourceSessionsField.optional().describe(
375
- "Originating session ids; correlates with Event Ledger records. Array form (T5+, rc.23 made it the sole accepted shape)."
376
- ),
377
- recent_paths: z2.array(z2.string()).describe("Workspace paths recently touched in the source session \u2014 used as scope hints"),
378
- user_messages_summary: z2.string().describe("Skill-side summary of the user's intent/messages, kept compact"),
379
- type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).describe("Knowledge type bucket (plural form, mirrors directory layout)"),
380
- slug: z2.string().describe("URL-safe short identifier proposed by the Skill; server may sanitize"),
381
- // rc.5 B1: dual pending root. When 'personal', the server writes to
382
- // ~/.fabric/knowledge/pending/<type>/; otherwise to .fabric/knowledge/pending/<type>/.
420
+ // v2.2 全砍 F13: REQUIRED in the base schema (was `.optional()`) so the MCP
421
+ // tool's advertised inputSchema (registerTool reads `.shape`) matches the
422
+ // requirement the superRefine enforces. Previously a caller reading the schema
423
+ // saw it optional, omitted it, and got rejected at parse a contract lie.
424
+ source_sessions: _sourceSessionsField.describe(
425
+ "Originating session ids (REQUIRED, non-empty array); correlates with Event Ledger records. Array form (T5+, rc.23 made it the sole accepted shape)."
426
+ ),
427
+ recent_paths: z3.array(z3.string()).describe("Workspace paths recently touched in the source session \u2014 used as scope hints"),
428
+ user_messages_summary: z3.string().describe("Skill-side summary of the user's intent/messages, kept compact"),
429
+ type: z3.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).describe("Knowledge type bucket (plural form, mirrors directory layout)"),
430
+ slug: z3.string().describe("URL-safe short identifier proposed by the Skill; server may sanitize"),
431
+ // Store-only cutover: layer is a compatibility audience hint. The server
432
+ // resolves the actual write target through semantic_scope/write_routes and
433
+ // writes to the selected mounted store's knowledge/pending/<type>/ tree.
383
434
  // Defaults to 'team' to preserve existing call sites (Skill bumps as needed).
384
- layer: z2.enum(["team", "personal"]).optional().describe(
385
- "Storage layer for the pending entry. 'team' writes under the workspace; 'personal' writes under the user's home. Defaults to 'team'."
435
+ layer: z3.enum(["team", "personal"]).optional().describe(
436
+ "Compatibility storage audience. 'personal' writes to the personal store; non-personal writes resolve by semantic_scope/write_routes. Defaults to 'team'."
437
+ ),
438
+ semantic_scope: z3.string().regex(SCOPE_COORDINATE_PATTERN).optional().describe(
439
+ "Logical audience/write route coordinate for this pending entry, e.g. personal, team, project:fabric-v2, org:acme:team:platform. Server validates and resolves it through write_routes."
386
440
  ),
387
441
  // v2.0.0-rc.7 T6: proposed_reason — required enum that drives `## Why
388
442
  // proposed` rendering. Skills (archive / import / review) infer the
@@ -394,7 +448,7 @@ var _FabExtractKnowledgeInputBaseSchema = z2.object({
394
448
  // captures the session goal + key turning point. Future-self review reads
395
449
  // this without conversation transcript access. Min length guards against
396
450
  // empty placeholders; cap is soft (no max), Skill caps at ~600 chars.
397
- session_context: z2.string().min(20, { message: "session_context must be \u226520 chars (3-5 lines describing goal + turning point)" }).describe(
451
+ session_context: z3.string().min(20, { message: "session_context must be \u226520 chars (3-5 lines describing goal + turning point)" }).describe(
398
452
  "3-5 line markdown blob \u2014 session goal + key turning point. Reviewed by future-self without transcript access."
399
453
  ),
400
454
  // v2.0.0-rc.8 A1 (skill-contract-fix): relevance scope/paths on the
@@ -408,10 +462,10 @@ var _FabExtractKnowledgeInputBaseSchema = z2.object({
408
462
  // a `knowledge_scope_degraded` event keyed by `pending:<idempotency_key>`.
409
463
  // NOTE: these fields MUST NOT be part of the idempotency hash inputs at
410
464
  // extract-knowledge.ts:78 — preserves rc.5→rc.7 collision detection.
411
- relevance_scope: z2.enum(["narrow", "broad"]).optional().describe(
465
+ relevance_scope: z3.enum(["narrow", "broad"]).optional().describe(
412
466
  "Optional relevance scope. 'narrow' restricts plan-context-hint surfacing to relevance_paths; 'broad' always surfaces. Omit to let the meta-builder default to 'broad'. Personal + narrow is silently degraded to broad + []."
413
467
  ),
414
- relevance_paths: z2.array(z2.string()).optional().describe(
468
+ relevance_paths: z3.array(z3.string()).optional().describe(
415
469
  "Optional path anchors for narrow scope. Workspace-relative globs or paths. Omit to let the meta-builder default to []. Ignored when scope is broad (server preserves the array for audit)."
416
470
  ),
417
471
  // v2.0.0-rc.23 TASK-006 (a-C1): four optional structured fields that the
@@ -428,16 +482,16 @@ var _FabExtractKnowledgeInputBaseSchema = z2.object({
428
482
  // relevance_paths follow the same rule). Including them would let an LLM
429
483
  // re-roll of the same observation create a second pending file just because
430
484
  // its inferred metadata wording drifted.
431
- intent_clues: z2.array(z2.string()).optional().describe(
485
+ intent_clues: z3.array(z3.string()).optional().describe(
432
486
  "Short LLM-readable triggers describing when this rule should fire and when it should not. Each item \u226480 chars, imperative phrasing (e.g. 'when editing Cocos UI batch code', 'NOT for non-batch contexts'). Optional \u2014 omit when the skill cannot infer cleanly."
433
487
  ),
434
- tech_stack: z2.array(z2.string()).optional().describe(
488
+ tech_stack: z3.array(z3.string()).optional().describe(
435
489
  "Tech stack / languages / frameworks the rule applies to (e.g. ['typescript', 'cocos-creator', 'nodejs']). Inferred from recent_paths file extensions and manifest files. Optional \u2014 omit when the rule is stack-agnostic."
436
490
  ),
437
- impact: z2.array(z2.string()).optional().describe(
491
+ impact: z3.array(z3.string()).optional().describe(
438
492
  "Consequences of ignoring this rule, used by the LLM to weight relevance vs cost. Each item \u2264120 chars (e.g. 'O(n\xB2) re-render on every frame', 'silent data loss on collision'). Optional \u2014 omit when impact is not observable."
439
493
  ),
440
- must_read_if: z2.string().optional().describe(
494
+ must_read_if: z3.string().optional().describe(
441
495
  "One-line strong trigger; when this condition holds the entry is considered required reading. Single line \u2264160 chars (e.g. 'touching anything under packages/cli/src/commands/hooks.ts'). Optional \u2014 omit when no single strong trigger fits."
442
496
  ),
443
497
  // v2.0.0-rc.37 NEW-37 (werewolf dogfood remediation): optional tags array.
@@ -448,7 +502,7 @@ var _FabExtractKnowledgeInputBaseSchema = z2.object({
448
502
  // IDEMPOTENCY: tags MUST NOT 参与 idempotency_key hash(同 relevance_*
449
503
  // / intent_clues 等可变字段一致),re-extract 时 tags 调整不应产生重复
450
504
  // pending file。
451
- tags: z2.array(z2.string()).optional().describe(
505
+ tags: z3.array(z3.string()).optional().describe(
452
506
  "Optional topic tags (2-4 kebab-case strings recommended). Drives cross-entry retrieval + topic clustering. Skill-inferred from session content; omit when not confidently inferable. Empty array allowed but discouraged (degrades narrow hint topic signal)."
453
507
  ),
454
508
  // v2.0.0-rc.23 TASK-014 (F8c): optional onboard-slot tag. The S5 slot
@@ -483,7 +537,7 @@ var _FabExtractKnowledgeInputBaseSchema = z2.object({
483
537
  // signal was captured. Like relevance_paths it MUST NOT participate in
484
538
  // the idempotency_key hash (an idempotent re-extract may surface a
485
539
  // slightly different read set without spawning a duplicate pending).
486
- evidence_paths: z2.array(z2.string()).optional().describe(
540
+ evidence_paths: z3.array(z3.string()).optional().describe(
487
541
  "Workspace-relative paths the agent CONSULTED (read but never modified) while building this knowledge. Documents context without affecting activation. Lifted from the legacy body ## Evidence markdown block into structured frontmatter so plan-context retrieval can read it as data."
488
542
  )
489
543
  });
@@ -492,7 +546,7 @@ var FabExtractKnowledgeInputSchema = _FabExtractKnowledgeInputBaseSchema.superRe
492
546
  const hasArray = Array.isArray(value.source_sessions) && value.source_sessions.length > 0;
493
547
  if (!hasArray) {
494
548
  ctx.addIssue({
495
- code: z2.ZodIssueCode.custom,
549
+ code: z3.ZodIssueCode.custom,
496
550
  message: "source_sessions (non-empty string array) is required",
497
551
  path: ["source_sessions"]
498
552
  });
@@ -500,12 +554,12 @@ var FabExtractKnowledgeInputSchema = _FabExtractKnowledgeInputBaseSchema.superRe
500
554
  }
501
555
  );
502
556
  var FabExtractKnowledgeInputShape = _FabExtractKnowledgeInputBaseSchema.shape;
503
- var FabExtractKnowledgeOutputSchema = z2.object({
504
- pending_path: z2.string().describe("Workspace-relative path to the persisted pending entry"),
505
- idempotency_key: z2.string().describe("Stable key derived from inputs; identical inputs yield identical key"),
557
+ var FabExtractKnowledgeOutputSchema = z3.object({
558
+ pending_path: z3.string().describe("Workspace-relative path to the persisted pending entry"),
559
+ idempotency_key: z3.string().describe("Stable key derived from inputs; identical inputs yield identical key"),
506
560
  // v2.0.0-rc.23 TASK-009 (d): optional warnings surface for the first-reconcile
507
561
  // gate (`meta_stale` / `reconcile_failed`). Absent on the steady-state path.
508
- warnings: z2.array(structuredWarningSchema).optional()
562
+ warnings: z3.array(structuredWarningSchema).optional()
509
563
  });
510
564
  var fabExtractKnowledgeAnnotations = {
511
565
  readOnlyHint: false,
@@ -514,21 +568,21 @@ var fabExtractKnowledgeAnnotations = {
514
568
  openWorldHint: false,
515
569
  title: "Extract pending knowledge entry"
516
570
  };
517
- var _fabReviewFiltersSchema = z2.object({
518
- type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).optional(),
519
- layer: z2.enum(["team", "personal", "both"]).optional(),
520
- maturity: z2.enum(["draft", "verified", "proven"]).optional(),
521
- tags: z2.array(z2.string()).optional(),
571
+ var _fabReviewFiltersSchema = z3.object({
572
+ type: z3.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).optional(),
573
+ layer: z3.enum(["team", "personal", "both"]).optional(),
574
+ maturity: z3.enum(["draft", "verified", "proven"]).optional(),
575
+ tags: z3.array(z3.string()).optional(),
522
576
  // rc.4 TASK-006 fix (c): ISO-8601 lower bound on entry created_at; entries
523
577
  // strictly older than this threshold are excluded from list / search
524
578
  // results. Additive optional field — existing callers unaffected.
525
- created_after: z2.string().datetime().optional(),
579
+ created_after: z3.string().datetime().optional(),
526
580
  // v2.0.0-rc.27 TASK-001 (§2.2/§2.3): opt-in surfacing of lifecycle-filtered
527
581
  // entries. Default (omit both) hides rejected entries and deferred entries
528
582
  // whose deferred_until is in the future. Pass true to include them — e.g.
529
583
  // for vacuum tooling, audit dashboards, or "show me what I parked" UX.
530
- include_rejected: z2.boolean().optional(),
531
- include_deferred: z2.boolean().optional(),
584
+ include_rejected: z3.boolean().optional(),
585
+ include_deferred: z3.boolean().optional(),
532
586
  // v2.0.0-rc.27 TASK-006 (audit §2.23): opt-in body inspection. Default
533
587
  // list/search return only frontmatter-derived fields — a malicious
534
588
  // pending entry could hide a prompt-injection payload under `## Evidence`
@@ -538,41 +592,57 @@ var _fabReviewFiltersSchema = z2.object({
538
592
  // default-off design keeps the wire payload small for routine list
539
593
  // calls; reviewer workflows pass `true` before approving so the body
540
594
  // is rendered into the reviewer's UI for visual scan.
541
- include_body: z2.boolean().optional()
595
+ include_body: z3.boolean().optional()
542
596
  }).optional();
543
- var _fabReviewModifyChangesSchema = z2.object({
544
- title: z2.string().optional(),
545
- summary: z2.string().optional(),
597
+ var _fabReviewModifyChangesSchema = z3.object({
598
+ title: z3.string().optional(),
599
+ summary: z3.string().optional(),
546
600
  // Q7: writing `layer` here triggers a layer-flip; downstream callers may
547
601
  // observe a redirect_to in fab_get_knowledge_sections if stable_id changes.
548
- layer: z2.enum(["team", "personal"]).optional(),
549
- maturity: z2.enum(["draft", "verified", "proven"]).optional(),
550
- tags: z2.array(z2.string()).optional(),
602
+ layer: z3.enum(["team", "personal"]).optional(),
603
+ maturity: z3.enum(["draft", "verified", "proven"]).optional(),
604
+ tags: z3.array(z3.string()).optional(),
551
605
  // v2.0-rc.5 C3 (TASK-012): relevance scope/paths patches. Applied to
552
606
  // pending AND canonical entries. When an explicit team→personal layer flip
553
607
  // arrives on a narrow entry, the server auto-degrades to broad + [] and
554
608
  // emits a `knowledge_scope_degraded` event regardless of what the caller
555
609
  // sent in these fields (personal-implies-broad).
556
- relevance_scope: z2.enum(["narrow", "broad"]).optional(),
557
- relevance_paths: z2.array(z2.string()).optional()
610
+ relevance_scope: z3.enum(["narrow", "broad"]).optional(),
611
+ relevance_paths: z3.array(z3.string()).optional(),
612
+ // v2.2 project-scope migration: re-scope an existing entry's resolution
613
+ // coordinate (e.g. team → project:fabric-v2) WITHOUT moving stores
614
+ // (scope ⊥ store, S42/A2). The in-place modify path keeps visibility_store
615
+ // intact, so a team→project flip just relabels who recall surfaces it to
616
+ // (G-FILTER, cross-store-recall.ts) — the entry stays physically in the
617
+ // same shared store. A personal-root coordinate is rejected here: landing
618
+ // an entry in the personal store is a store move, which is the dedicated
619
+ // modify-layer path (R5#3 privacy boundary), never an in-place scalar edit.
620
+ semantic_scope: z3.string().regex(SCOPE_COORDINATE_PATTERN).optional(),
621
+ // v2.2 graph edges (KT-DEC-0031 wiki seam): write the `related` H2 adjacency
622
+ // (bare or store-qualified stable_ids this entry points at). REPLACE semantics
623
+ // mirror tags/relevance_paths — the caller (fabric-connect) reads existing
624
+ // edges via fab_recall and sends the merged set. Absent this field the modify
625
+ // path silently dropped `related` via zod .strip() (KT-PIT-0005 recurrence),
626
+ // leaving the only programmatic related-write path non-functional.
627
+ related: z3.array(z3.string()).optional()
558
628
  });
559
- var FabReviewInputSchema = z2.discriminatedUnion("action", [
560
- z2.object({
561
- action: z2.literal("list"),
629
+ var FabReviewInputSchema = z3.discriminatedUnion("action", [
630
+ z3.object({
631
+ action: z3.literal("list"),
562
632
  filters: _fabReviewFiltersSchema
563
633
  }),
564
- z2.object({
565
- action: z2.literal("approve"),
566
- pending_paths: z2.array(z2.string()).min(1)
634
+ z3.object({
635
+ action: z3.literal("approve"),
636
+ pending_paths: z3.array(z3.string()).min(1)
567
637
  }),
568
- z2.object({
569
- action: z2.literal("reject"),
570
- pending_paths: z2.array(z2.string()).min(1),
571
- reason: z2.string().min(1)
638
+ z3.object({
639
+ action: z3.literal("reject"),
640
+ pending_paths: z3.array(z3.string()).min(1),
641
+ reason: z3.string().min(1)
572
642
  }),
573
- z2.object({
574
- action: z2.literal("modify"),
575
- pending_path: z2.string().min(1),
643
+ z3.object({
644
+ action: z3.literal("modify"),
645
+ pending_path: z3.string().min(1),
576
646
  changes: _fabReviewModifyChangesSchema
577
647
  }),
578
648
  // v2.0.0-rc.37 NEW-12: explicit modify split. `modify-content` edits scalar
@@ -581,177 +651,176 @@ var FabReviewInputSchema = z2.discriminatedUnion("action", [
581
651
  // (changes.layer REQUIRED) which may reallocate the stable_id + emit an
582
652
  // id-redirect (rc.37 NEW-24). Legacy `modify` stays for back-compat and
583
653
  // routes by whether changes.layer is present.
584
- z2.object({
585
- action: z2.literal("modify-content"),
586
- pending_path: z2.string().min(1),
654
+ z3.object({
655
+ action: z3.literal("modify-content"),
656
+ pending_path: z3.string().min(1),
587
657
  changes: _fabReviewModifyChangesSchema
588
658
  }),
589
- z2.object({
590
- action: z2.literal("modify-layer"),
591
- pending_path: z2.string().min(1),
659
+ z3.object({
660
+ action: z3.literal("modify-layer"),
661
+ pending_path: z3.string().min(1),
592
662
  changes: _fabReviewModifyChangesSchema.extend({
593
- layer: z2.enum(["team", "personal"])
663
+ layer: z3.enum(["team", "personal"])
594
664
  })
595
665
  }),
596
- z2.object({
597
- action: z2.literal("search"),
598
- query: z2.string().min(1),
666
+ z3.object({
667
+ action: z3.literal("search"),
668
+ query: z3.string().min(1),
599
669
  filters: _fabReviewFiltersSchema
600
670
  }),
601
- z2.object({
602
- action: z2.literal("defer"),
603
- pending_paths: z2.array(z2.string()).min(1),
604
- until: z2.string().datetime().optional(),
605
- reason: z2.string().optional()
671
+ z3.object({
672
+ action: z3.literal("defer"),
673
+ pending_paths: z3.array(z3.string()).min(1),
674
+ until: z3.string().datetime().optional(),
675
+ reason: z3.string().optional()
606
676
  })
607
677
  ]);
608
678
  var FabReviewInputShape = {
609
- action: z2.enum(["list", "approve", "reject", "modify", "modify-content", "modify-layer", "search", "defer"]).describe(
679
+ action: z3.enum(["list", "approve", "reject", "modify", "modify-content", "modify-layer", "search", "defer"]).describe(
610
680
  "Action selector. Discriminates the per-action fields below; required. modify-content edits scalars (no layer); modify-layer is the layer-flip path (changes.layer required); modify is the legacy combined alias."
611
681
  ),
612
682
  filters: _fabReviewFiltersSchema.describe(
613
683
  "Optional filters (type/layer/maturity/tags/created_after). Used by action=list and action=search."
614
684
  ),
615
- pending_paths: z2.array(z2.string()).min(1).optional().describe(
685
+ pending_paths: z3.array(z3.string()).min(1).optional().describe(
616
686
  "Workspace-relative pending entry paths. Required when action=approve|reject|defer (non-empty array)."
617
687
  ),
618
- pending_path: z2.string().min(1).optional().describe(
688
+ pending_path: z3.string().min(1).optional().describe(
619
689
  "Workspace-relative pending OR canonical entry path. Required when action=modify."
620
690
  ),
621
- reason: z2.string().optional().describe(
691
+ reason: z3.string().optional().describe(
622
692
  "Reason string. Required (non-empty) when action=reject; optional when action=defer."
623
693
  ),
624
694
  changes: _fabReviewModifyChangesSchema.optional().describe(
625
- "Frontmatter scalar patches (title/summary/layer/maturity/tags/relevance_*). Required when action=modify."
695
+ "Frontmatter scalar patches (title/summary/layer/maturity/tags/relevance_*/semantic_scope/related). Required when action=modify. semantic_scope re-scopes the entry's resolution coordinate in place (e.g. team \u2192 project:<id>) without moving stores; personal-root coordinates are rejected (use modify-layer)."
626
696
  ),
627
- query: z2.string().min(1).optional().describe(
697
+ query: z3.string().min(1).optional().describe(
628
698
  "Substring query against title/summary/tags/path. Required (non-empty) when action=search."
629
699
  ),
630
- until: z2.string().datetime().optional().describe(
700
+ until: z3.string().datetime().optional().describe(
631
701
  "ISO-8601 datetime upper bound for the deferral. Optional; used only when action=defer."
632
702
  )
633
703
  };
634
- var _fabReviewListItemSchema = z2.object({
635
- pending_path: z2.string(),
704
+ var _fabReviewListItemSchema = z3.object({
705
+ pending_path: z3.string(),
636
706
  // v2.0.0-rc.27 TASK-001 (§2.12): for personal-layer entries `pending_path`
637
707
  // carries the human-friendly `~/...` form (legacy contract) while
638
708
  // `pending_path_absolute` carries the os-expanded absolute path. Programmatic
639
709
  // consumers (Read tool, fs.readFile, downstream MCP servers) should prefer
640
710
  // the absolute variant — the `~` is a shell-only sigil that breaks every
641
711
  // non-shell consumer. Team entries omit this field because their
642
- // `pending_path` is already project-relative and unambiguous.
643
- pending_path_absolute: z2.string().optional(),
644
- type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
645
- layer: z2.enum(["team", "personal"]),
646
- maturity: z2.enum(["draft", "verified", "proven"]),
647
- tags: z2.array(z2.string()).optional(),
648
- title: z2.string().optional(),
649
- summary: z2.string().optional(),
650
- // rc.5 B1: dual pending root. 'team' = workspace .fabric/knowledge/pending,
651
- // 'personal' = ~/.fabric/knowledge/pending. Distinct from `layer` (frontmatter):
652
- // origin reflects where the pending file actually lives on disk; layer reflects
653
- // the declared classification that will drive the approve destination.
654
- origin: z2.enum(["team", "personal"]).optional(),
712
+ // `pending_path` is already store-resolved and unambiguous.
713
+ pending_path_absolute: z3.string().optional(),
714
+ type: z3.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
715
+ layer: z3.enum(["team", "personal"]),
716
+ maturity: z3.enum(["draft", "verified", "proven"]),
717
+ tags: z3.array(z3.string()).optional(),
718
+ title: z3.string().optional(),
719
+ summary: z3.string().optional(),
720
+ // Store-only cutover: origin reflects the resolved store audience where the
721
+ // pending file lives; layer reflects the declared classification that will
722
+ // drive approval semantics.
723
+ origin: z3.enum(["team", "personal"]).optional(),
655
724
  // v2.0.0-rc.27 TASK-001 (§2.2/§2.3): frontmatter status markers. Default
656
725
  // "active" (or absent). `rejected` entries are excluded from list/search
657
726
  // unless filters.include_rejected=true; `deferred` entries are excluded
658
727
  // when deferred_until is in the future. Authored by reject/defer write
659
728
  // paths — never by extract or approve.
660
- status: z2.enum(["active", "rejected", "deferred"]).optional(),
661
- deferred_until: z2.string().datetime().optional(),
729
+ status: z3.enum(["active", "rejected", "deferred"]).optional(),
730
+ deferred_until: z3.string().datetime().optional(),
662
731
  // v2.0.0-rc.27 TASK-006 (audit §2.23): full body content (everything
663
732
  // after the closing `---` of frontmatter). Surfaced only when caller
664
733
  // passes `filters.include_body: true`. Default-omitted to keep payload
665
734
  // small for routine list calls.
666
- body: z2.string().optional()
735
+ body: z3.string().optional()
667
736
  });
668
- var _fabReviewSearchItemSchema = z2.object({
669
- // Search hits live in one of two trees:
670
- // - "pending" → .fabric/knowledge/pending/ (or ~/.fabric/knowledge/pending/)
671
- // - "canonical" → .fabric/knowledge/{decisions,pitfalls,...} (or personal mirror)
672
- area: z2.enum(["pending", "canonical"]),
673
- path: z2.string(),
674
- path_absolute: z2.string().optional(),
675
- type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
676
- layer: z2.enum(["team", "personal"]),
677
- maturity: z2.enum(["draft", "verified", "proven"]),
678
- tags: z2.array(z2.string()).optional(),
679
- title: z2.string().optional(),
680
- summary: z2.string().optional(),
681
- origin: z2.enum(["team", "personal"]).optional(),
682
- status: z2.enum(["active", "rejected", "deferred"]).optional(),
683
- deferred_until: z2.string().datetime().optional(),
684
- body: z2.string().optional(),
737
+ var _fabReviewSearchItemSchema = z3.object({
738
+ // Search hits live in one of two store trees:
739
+ // - "pending" → mounted store `knowledge/pending/`
740
+ // - "canonical" → mounted store `knowledge/{decisions,pitfalls,...}`
741
+ area: z3.enum(["pending", "canonical"]),
742
+ path: z3.string(),
743
+ path_absolute: z3.string().optional(),
744
+ type: z3.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
745
+ layer: z3.enum(["team", "personal"]),
746
+ maturity: z3.enum(["draft", "verified", "proven"]),
747
+ tags: z3.array(z3.string()).optional(),
748
+ title: z3.string().optional(),
749
+ summary: z3.string().optional(),
750
+ origin: z3.enum(["team", "personal"]).optional(),
751
+ status: z3.enum(["active", "rejected", "deferred"]).optional(),
752
+ deferred_until: z3.string().datetime().optional(),
753
+ body: z3.string().optional(),
685
754
  // For pending hits the upstream stable_id may still be unassigned — keep it
686
755
  // optional so canonical hits (which always have one) parse alongside pending
687
756
  // hits in the same array.
688
- stable_id: z2.string().optional()
757
+ stable_id: z3.string().optional()
689
758
  });
690
- var FabReviewOutputSchema = z2.discriminatedUnion("action", [
691
- z2.object({
692
- action: z2.literal("list"),
693
- items: z2.array(_fabReviewListItemSchema),
694
- warnings: z2.array(structuredWarningSchema).optional()
759
+ var FabReviewOutputSchema = z3.discriminatedUnion("action", [
760
+ z3.object({
761
+ action: z3.literal("list"),
762
+ items: z3.array(_fabReviewListItemSchema),
763
+ warnings: z3.array(structuredWarningSchema).optional()
695
764
  }),
696
- z2.object({
697
- action: z2.literal("approve"),
698
- approved: z2.array(z2.object({ pending_path: z2.string(), stable_id: z2.string() })),
699
- warnings: z2.array(structuredWarningSchema).optional()
765
+ z3.object({
766
+ action: z3.literal("approve"),
767
+ approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })),
768
+ warnings: z3.array(structuredWarningSchema).optional()
700
769
  }),
701
- z2.object({
702
- action: z2.literal("reject"),
703
- rejected: z2.array(z2.string()),
704
- warnings: z2.array(structuredWarningSchema).optional()
770
+ z3.object({
771
+ action: z3.literal("reject"),
772
+ rejected: z3.array(z3.string()),
773
+ warnings: z3.array(structuredWarningSchema).optional()
705
774
  }),
706
- z2.object({
707
- action: z2.literal("modify"),
708
- pending_path: z2.string(),
775
+ z3.object({
776
+ action: z3.literal("modify"),
777
+ pending_path: z3.string(),
709
778
  // When a layer-flip occurred, prior_stable_id and new_stable_id differ.
710
- prior_stable_id: z2.string().optional(),
711
- new_stable_id: z2.string().optional(),
712
- warnings: z2.array(structuredWarningSchema).optional()
779
+ prior_stable_id: z3.string().optional(),
780
+ new_stable_id: z3.string().optional(),
781
+ warnings: z3.array(structuredWarningSchema).optional()
713
782
  }),
714
- z2.object({
715
- action: z2.literal("search"),
783
+ z3.object({
784
+ action: z3.literal("search"),
716
785
  // v2.0.0-rc.29 TASK-007 (BUG-M4): search returns the new search-item
717
786
  // shape with `area` discriminator + neutrally-named `path` field.
718
- items: z2.array(_fabReviewSearchItemSchema),
719
- warnings: z2.array(structuredWarningSchema).optional()
787
+ items: z3.array(_fabReviewSearchItemSchema),
788
+ warnings: z3.array(structuredWarningSchema).optional()
720
789
  }),
721
- z2.object({
722
- action: z2.literal("defer"),
723
- deferred: z2.array(z2.string()),
724
- warnings: z2.array(structuredWarningSchema).optional()
790
+ z3.object({
791
+ action: z3.literal("defer"),
792
+ deferred: z3.array(z3.string()),
793
+ warnings: z3.array(structuredWarningSchema).optional()
725
794
  })
726
795
  ]);
727
796
  var FabReviewOutputShape = {
728
- action: z2.enum(["list", "approve", "reject", "modify", "search", "defer"]).describe(
797
+ action: z3.enum(["list", "approve", "reject", "modify", "search", "defer"]).describe(
729
798
  "Echoes the input action; clients can switch on it for per-variant fields below."
730
799
  ),
731
- items: z2.array(z2.union([_fabReviewListItemSchema, _fabReviewSearchItemSchema])).optional().describe(
800
+ items: z3.array(z3.union([_fabReviewListItemSchema, _fabReviewSearchItemSchema])).optional().describe(
732
801
  "Pending entries (action=list, `pending_path` shape) or pending+canonical entries (action=search, `area`+`path` shape)."
733
802
  ),
734
- approved: z2.array(z2.object({ pending_path: z2.string(), stable_id: z2.string() })).optional().describe(
803
+ approved: z3.array(z3.object({ pending_path: z3.string(), stable_id: z3.string() })).optional().describe(
735
804
  "Allocated stable ids paired with their original pending paths. Present when action=approve."
736
805
  ),
737
- rejected: z2.array(z2.string()).optional().describe(
806
+ rejected: z3.array(z3.string()).optional().describe(
738
807
  "Pending paths that were rejected (files retained on disk; doctor owns vacuum). Present when action=reject."
739
808
  ),
740
- pending_path: z2.string().optional().describe(
809
+ pending_path: z3.string().optional().describe(
741
810
  "Echoed target path for the modification. Present when action=modify."
742
811
  ),
743
- prior_stable_id: z2.string().optional().describe(
812
+ prior_stable_id: z3.string().optional().describe(
744
813
  "Prior stable id. Present when action=modify AND a layer-flip reallocated the id."
745
814
  ),
746
- new_stable_id: z2.string().optional().describe(
815
+ new_stable_id: z3.string().optional().describe(
747
816
  "New stable id after reallocation. Present when action=modify AND a layer-flip reallocated the id."
748
817
  ),
749
- deferred: z2.array(z2.string()).optional().describe(
818
+ deferred: z3.array(z3.string()).optional().describe(
750
819
  "Pending paths that were deferred (files retained on disk). Present when action=defer."
751
820
  ),
752
821
  // v2.0.0-rc.23 TASK-009 (d): optional warnings surface for the first-reconcile
753
822
  // gate (`meta_stale` / `reconcile_failed`). Absent on the steady-state path.
754
- warnings: z2.array(structuredWarningSchema).optional()
823
+ warnings: z3.array(structuredWarningSchema).optional()
755
824
  };
756
825
  var fabReviewAnnotations = {
757
826
  readOnlyHint: false,
@@ -760,35 +829,35 @@ var fabReviewAnnotations = {
760
829
  openWorldHint: false,
761
830
  title: "Review pending knowledge entries"
762
831
  };
763
- var citeContractMetricsSchema = z2.object({
764
- decisions_cited: z2.number().int().nonnegative(),
765
- pitfalls_cited: z2.number().int().nonnegative(),
766
- contract_with: z2.number().int().nonnegative(),
767
- contract_missing: z2.number().int().nonnegative(),
768
- hard_violated: z2.number().int().nonnegative(),
769
- cite_id_unresolved: z2.number().int().nonnegative(),
770
- skip_count: z2.record(z2.string(), z2.number().int().nonnegative())
832
+ var citeContractMetricsSchema = z3.object({
833
+ decisions_cited: z3.number().int().nonnegative(),
834
+ pitfalls_cited: z3.number().int().nonnegative(),
835
+ contract_with: z3.number().int().nonnegative(),
836
+ contract_missing: z3.number().int().nonnegative(),
837
+ hard_violated: z3.number().int().nonnegative(),
838
+ cite_id_unresolved: z3.number().int().nonnegative(),
839
+ skip_count: z3.record(z3.string(), z3.number().int().nonnegative())
771
840
  });
772
- var citeLayerTypeBreakdownSchema = z2.object({
773
- team: z2.record(z2.string(), z2.number().int().nonnegative()),
774
- personal: z2.record(z2.string(), z2.number().int().nonnegative())
841
+ var citeLayerTypeBreakdownSchema = z3.object({
842
+ team: z3.record(z3.string(), z3.number().int().nonnegative()),
843
+ personal: z3.record(z3.string(), z3.number().int().nonnegative())
775
844
  });
776
- var citeCoverageReportSchema = z2.object({
777
- status: z2.enum(["ok", "skipped"]),
778
- marker_ts: z2.number().int().nonnegative(),
779
- marker_emitted_now: z2.boolean(),
780
- since_ts: z2.number().int().nonnegative(),
781
- client_filter: z2.enum(["cc", "codex", "cursor", "all"]),
845
+ var citeCoverageReportSchema = z3.object({
846
+ status: z3.enum(["ok", "skipped"]),
847
+ marker_ts: z3.number().int().nonnegative(),
848
+ marker_emitted_now: z3.boolean(),
849
+ since_ts: z3.number().int().nonnegative(),
850
+ client_filter: z3.enum(["cc", "codex", "all"]),
782
851
  // v2.0.0-rc.24 TASK-08: layer filter discriminator. Optional so pre-TASK-10
783
852
  // CLI callers (which never set the flag) still parse. Defaults to "all" at
784
853
  // the service layer.
785
- layer_filter: z2.enum(["team", "personal", "all"]).optional(),
786
- metrics: z2.object({
787
- edits_touched: z2.number().int().nonnegative(),
788
- qualifying_cites: z2.number().int().nonnegative(),
789
- recalled_unverified: z2.number().int().nonnegative(),
790
- expected_but_missed: z2.number().int().nonnegative(),
791
- total_turns: z2.number().int().nonnegative(),
854
+ layer_filter: z3.enum(["team", "personal", "all"]).optional(),
855
+ metrics: z3.object({
856
+ edits_touched: z3.number().int().nonnegative(),
857
+ qualifying_cites: z3.number().int().nonnegative(),
858
+ recalled_unverified: z3.number().int().nonnegative(),
859
+ expected_but_missed: z3.number().int().nonnegative(),
860
+ total_turns: z3.number().int().nonnegative(),
792
861
  // v2.0.0-rc.38 UX-8 (C, user-authorized): cite-policy COMPLIANCE rate —
793
862
  // the corrected G-CITE semantic. The legacy qualifying_cites/edits ratio
794
863
  // measured "how often an applicable KB id existed" (a function of corpus
@@ -797,38 +866,102 @@ var citeCoverageReportSchema = z2.object({
797
866
  // `KB: none [reason]` (the policy explicitly allows the none sentinel) —
798
867
  // over the turns where a cite was expected. null when no cite-expected
799
868
  // turns observed (avoids a misleading 0/0 → 0). Range [0,1].
800
- cite_compliance_rate: z2.number().min(0).max(1).nullable().optional(),
801
- compliant_cites: z2.number().int().nonnegative().optional(),
802
- noncompliant_cites: z2.number().int().nonnegative().optional(),
869
+ cite_compliance_rate: z3.number().min(0).max(1).nullable().optional(),
870
+ compliant_cites: z3.number().int().nonnegative().optional(),
871
+ noncompliant_cites: z3.number().int().nonnegative().optional(),
803
872
  // Edit signals lacking session_id → uncorrelatable, silently excluded from
804
873
  // expected_but_missed. >0 typically means a stale pre-session_id hook is
805
874
  // installed (run `fabric install`). Surfaced so the denominator gap is
806
875
  // visible rather than a silent 100% confound.
807
- uncorrelatable_edits: z2.number().int().nonnegative().optional()
876
+ uncorrelatable_edits: z3.number().int().nonnegative().optional(),
877
+ // v2.1 ⑤ cite-redesign (P5): recall-based coverage口径. The redesign infers
878
+ // a citation from real behavior — an in-session fab_recall
879
+ // (knowledge_context_planned) whose target_paths overlap a subsequently
880
+ // edited file IS the citation, no hand-written `KB:` line required.
881
+ // recall_backed_edits = correlatable edits preceded (within the recall
882
+ // window) by such an overlapping recall. recall_coverage_rate =
883
+ // recall_backed_edits / edits_touched (null when no edits). Additive — the
884
+ // legacy first-line-`KB:` metrics above are unchanged (back-compat).
885
+ recall_backed_edits: z3.number().int().nonnegative().optional(),
886
+ recall_coverage_rate: z3.number().min(0).max(1).nullable().optional(),
887
+ // v2.2.0-rc.1 W1-T3 (cite 诚实拆分 / lifecycle §3): exposed_and_mutated is a
888
+ // WEAK auxiliary signal — strictly SEPARATE from cite_compliance_rate (which
889
+ // is the true explicit-adherence rate, currently ~2.5%). It MUST NOT be
890
+ // merged into compliance: it estimates "a narrow PreToolUse-surfaced KB id
891
+ // whose contract-specific glob was subsequently edited (mutated) in the same
892
+ // session, and was not [dismissed] that round". It credits NOTHING toward the
893
+ // real `KB:`-line compliance — it is an observational hint that surfaced
894
+ // knowledge influenced an edit, surfaced ONLY as its own field so the renderer
895
+ // can label it "weak signal, NOT counted toward true adherence". Three
896
+ // conditions (all required): (1) id came from a `hook_surface_emitted` with
897
+ // hook_name === "knowledge-hint-narrow"; (2) the id's contract glob is
898
+ // SPECIFIC (excludes `**/*` wildcards and generic guideline-type entries);
899
+ // (3) the id was not [dismissed] in the same session. `count` = number of
900
+ // distinct (session_id, stable_id) pairs satisfying all three; `ids` =
901
+ // sorted distinct stable_ids (capped, diagnostics only). Always >= 0; null/
902
+ // absent on degraded/skipped reports.
903
+ exposed_and_mutated: z3.object({
904
+ count: z3.number().int().nonnegative(),
905
+ ids: z3.array(z3.string()).optional()
906
+ }).optional(),
907
+ // lifecycle-refactor W2-T4 (§5 row7 PostToolUse / §0 下沉 doctor): mutation
908
+ // funnel rebuilt offline from the new `file_mutated` PostToolUse marker —
909
+ // the权威 signal that a mutation actually completed (path + tool_call_id),
910
+ // distinct from the PreToolUse `edit_intent_checked` EDIT-INTENT signal that
911
+ // feeds `edits_touched`. mutations_observed.count = number of distinct
912
+ // `file_mutated` events in window (per-call tool_call_id dedup guards the
913
+ // PostToolUse parallel-fire race). Strictly ADDITIVE — never folded into
914
+ // cite_compliance_rate (honesty 铁律, mirrors exposed_and_mutated). Absent on
915
+ // degraded/skipped reports.
916
+ mutations_observed: z3.object({
917
+ count: z3.number().int().nonnegative()
918
+ }).optional(),
919
+ // lifecycle-refactor W2-T4 (§5 row7 mutation_pool + downgrade): low-confidence
920
+ // mutation attribution pool. A `file_mutated` event is `attributed` ONLY when
921
+ // its `source_event_id` links back to a `hook_surface_emitted` (surfaced/cited
922
+ // knowledge) in window — attribution key = store_id + stable_id +
923
+ // source_event_id (distinct dedup so multi-store never double-counts). Every
924
+ // other mutation (no source_event_id, or a source_event_id that does not
925
+ // resolve to a surfaced event) downgrades to `unattributed_workspace_dirty`.
926
+ // NOTE: this is the events.jsonl-only attribution. The §9 git-diff fallback
927
+ // (升 fallback via session shell event + baseline) is a SPECULATIVE
928
+ // implementation note — deliberately NOT run here (doctor stays read-only,
929
+ // no git diff / no disk write). Additive; absent on degraded/skipped reports.
930
+ mutation_pool: z3.object({
931
+ attributed: z3.number().int().nonnegative(),
932
+ unattributed_workspace_dirty: z3.number().int().nonnegative()
933
+ }).optional(),
934
+ // lifecycle-refactor W2-T4 (§5 row2 SessionEnd funnel 对账下沉 doctor): the
935
+ // SessionEnd hook only O(1)-appends a `session_ended` marker; this counts the
936
+ // distinct sessions that emitted one (funnel "closed" boundary). Purely an
937
+ // observability marker — not joined into any rate. Additive.
938
+ sessions_closed: z3.object({
939
+ count: z3.number().int().nonnegative()
940
+ }).optional()
808
941
  }),
809
- per_client: z2.record(
810
- z2.string(),
811
- z2.object({
812
- edits_touched: z2.number().int().nonnegative().optional(),
813
- qualifying_cites: z2.number().int().nonnegative().optional(),
814
- recalled_unverified: z2.number().int().nonnegative().optional(),
815
- expected_but_missed: z2.number().int().nonnegative().optional(),
816
- total_turns: z2.number().int().nonnegative().optional()
942
+ per_client: z3.record(
943
+ z3.string(),
944
+ z3.object({
945
+ edits_touched: z3.number().int().nonnegative().optional(),
946
+ qualifying_cites: z3.number().int().nonnegative().optional(),
947
+ recalled_unverified: z3.number().int().nonnegative().optional(),
948
+ expected_but_missed: z3.number().int().nonnegative().optional(),
949
+ total_turns: z3.number().int().nonnegative().optional()
817
950
  })
818
951
  ).optional(),
819
- dismissed_reason_histogram: z2.record(z2.string(), z2.number().int().nonnegative()).optional(),
820
- none_reason_histogram: z2.record(z2.string(), z2.number().int().nonnegative()).optional(),
952
+ dismissed_reason_histogram: z3.record(z3.string(), z3.number().int().nonnegative()).optional(),
953
+ none_reason_histogram: z3.record(z3.string(), z3.number().int().nonnegative()).optional(),
821
954
  // v2.0.0-rc.24 TASK-08: contract-policy audit metrics. Status discriminates
822
955
  // populated vs degraded modes. contract_metrics + per_layer_type are emitted
823
956
  // (zeroed) in degraded modes so the renderer iterates one stable shape.
824
- contract_metrics_status: z2.enum(["ok", "skipped:bootstrap_drift", "awaiting_marker"]).optional(),
957
+ contract_metrics_status: z3.enum(["ok", "skipped:bootstrap_drift", "awaiting_marker"]).optional(),
825
958
  contract_metrics: citeContractMetricsSchema.optional(),
826
959
  per_layer_type: citeLayerTypeBreakdownSchema.optional(),
827
- contract_marker_ts: z2.number().int().nonnegative().optional(),
828
- generated_at: z2.string()
960
+ contract_marker_ts: z3.number().int().nonnegative().optional(),
961
+ generated_at: z3.string()
829
962
  });
830
- var ledgerSourceSchema = z2.enum(["ai", "human"]);
831
- var timestampFilterSchema = z2.preprocess((value) => {
963
+ var ledgerSourceSchema = z3.enum(["ai", "human"]);
964
+ var timestampFilterSchema = z3.preprocess((value) => {
832
965
  if (value === void 0 || value === null || value === "") {
833
966
  return void 0;
834
967
  }
@@ -847,38 +980,38 @@ var timestampFilterSchema = z2.preprocess((value) => {
847
980
  return Number.isNaN(parsed) ? value : parsed;
848
981
  }
849
982
  return value;
850
- }, z2.number().int().nonnegative());
851
- var ledgerQuerySchema = z2.object({
983
+ }, z3.number().int().nonnegative());
984
+ var ledgerQuerySchema = z3.object({
852
985
  source: ledgerSourceSchema.optional(),
853
986
  since: timestampFilterSchema.optional()
854
987
  });
855
- var historyStateQuerySchema = z2.object({
856
- ledger_id: z2.string().trim().min(1).optional(),
988
+ var historyStateQuerySchema = z3.object({
989
+ ledger_id: z3.string().trim().min(1).optional(),
857
990
  ts: timestampFilterSchema.optional()
858
991
  }).superRefine((value, ctx) => {
859
992
  const provided = [value.ledger_id, value.ts].filter((entry) => entry !== void 0);
860
993
  if (provided.length !== 1) {
861
994
  ctx.addIssue({
862
- code: z2.ZodIssueCode.custom,
995
+ code: z3.ZodIssueCode.custom,
863
996
  message: "Provide exactly one of ledger_id or ts.",
864
997
  path: ["ledger_id"]
865
998
  });
866
999
  }
867
1000
  });
868
- var humanLockApproveRequestSchema = z2.object({
869
- file: z2.string().min(1),
870
- start_line: z2.number().int().positive(),
871
- end_line: z2.number().int().positive(),
872
- new_hash: z2.string().min(1)
1001
+ var humanLockApproveRequestSchema = z3.object({
1002
+ file: z3.string().min(1),
1003
+ start_line: z3.number().int().positive(),
1004
+ end_line: z3.number().int().positive(),
1005
+ new_hash: z3.string().min(1)
873
1006
  });
874
- var humanLockFileParamsSchema = z2.object({
875
- file: z2.string().min(1)
1007
+ var humanLockFileParamsSchema = z3.object({
1008
+ file: z3.string().min(1)
876
1009
  });
877
- var annotateIntentRequestSchema = z2.object({
878
- ledger_entry_id: z2.string().min(1),
879
- annotation: z2.string().trim().min(1)
1010
+ var annotateIntentRequestSchema = z3.object({
1011
+ ledger_entry_id: z3.string().min(1),
1012
+ annotation: z3.string().trim().min(1)
880
1013
  });
881
- var KnowledgeTypeSchema = z2.enum([
1014
+ var KnowledgeTypeSchema = z3.enum([
882
1015
  "models",
883
1016
  // entities, data structures, relationships
884
1017
  "decisions",
@@ -890,10 +1023,10 @@ var KnowledgeTypeSchema = z2.enum([
890
1023
  "processes"
891
1024
  // workflows, state machines, operational steps
892
1025
  ]);
893
- var MaturitySchema = z2.enum(["draft", "verified", "proven"]);
894
- var LayerSchema = z2.enum(["personal", "team"]);
895
- var StableIdSchema = z2.string().regex(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/);
896
- var KnowledgeEntryFrontmatterSchema = z2.object({
1026
+ var MaturitySchema = z3.enum(["draft", "verified", "proven"]);
1027
+ var LayerSchema = z3.enum(["personal", "team"]);
1028
+ var StableIdSchema = z3.string().regex(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/);
1029
+ var KnowledgeEntryFrontmatterSchema = z3.object({
897
1030
  id: StableIdSchema,
898
1031
  // e.g., "KT-DEC-0042"
899
1032
  type: KnowledgeTypeSchema,
@@ -902,9 +1035,9 @@ var KnowledgeEntryFrontmatterSchema = z2.object({
902
1035
  // draft | verified | proven
903
1036
  layer: LayerSchema,
904
1037
  // personal | team
905
- layer_reason: z2.string().optional(),
1038
+ layer_reason: z3.string().optional(),
906
1039
  // why this layer (for ambiguous cases)
907
- created_at: z2.string()
1040
+ created_at: z3.string()
908
1041
  // ISO 8601 timestamp
909
1042
  // Note: 'tags' and other fields can be added later but core schema is these 6
910
1043
  });
@@ -935,6 +1068,13 @@ export {
935
1068
  ONBOARD_SLOT_NAMES,
936
1069
  onboardSlotSchema,
937
1070
  ONBOARD_SLOT_TOTAL,
1071
+ PERSONAL_SCOPE,
1072
+ KNOWN_SCOPE_PREFIXES,
1073
+ SCOPE_COORDINATE_PATTERN,
1074
+ scopeCoordinateSchema,
1075
+ scopeRoot,
1076
+ isPersonalScope,
1077
+ entryScopeMetadataSchema,
938
1078
  structuredWarningSchema,
939
1079
  planContextInputSchema,
940
1080
  planContextOutputSchema,
@@ -951,7 +1091,7 @@ export {
951
1091
  archiveScanOutputSchema,
952
1092
  archiveScanAnnotations,
953
1093
  ProposedReasonSchema,
954
- PROPOSED_REASON_DESCRIPTIONS,
1094
+ PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE,
955
1095
  FabExtractKnowledgeInputSchema,
956
1096
  FabExtractKnowledgeInputShape,
957
1097
  FabExtractKnowledgeOutputSchema,