@fenglimg/fabric-shared 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,952 @@
1
+ // src/schemas/api-contracts.ts
2
+ import { z as z2 } from "zod";
3
+
4
+ // src/onboard-slots.ts
5
+ import { z } from "zod";
6
+ var ONBOARD_SLOT_NAMES = [
7
+ "tech-stack-decision",
8
+ "architecture-pattern",
9
+ "code-style-tone",
10
+ "build-system-idiom",
11
+ "domain-vocabulary"
12
+ ];
13
+ var onboardSlotSchema = z.enum(ONBOARD_SLOT_NAMES);
14
+ var ONBOARD_SLOT_TOTAL = ONBOARD_SLOT_NAMES.length;
15
+
16
+ // 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()
22
+ });
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(),
33
+ // v2.0: optional knowledge-entry fields. Absent for v1.x rules; present for
34
+ // entries that declare frontmatter `id/type/maturity/layer`.
35
+ id: z2.string().optional(),
36
+ knowledge_type: _knowledgeTypeEnum.optional(),
37
+ maturity: _maturityEnum.optional(),
38
+ knowledge_layer: _layerEnum.optional(),
39
+ layer_reason: z2.string().optional(),
40
+ created_at: z2.string().optional(),
41
+ // v2.0.0-rc.38 UX-3 (D-MCP fold ③): these three were previously carried ONLY
42
+ // as top-level mirrors on the index item. With the mirrors removed,
43
+ // `description` becomes their canonical (and only) home, so the schema must
44
+ // validate them here. Optional + default-safe (tags/[]/broad) so legacy
45
+ // 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()
49
+ });
50
+ var _descriptionIndexItemSchema = z2.object({
51
+ stable_id: z2.string(),
52
+ description: _ruleDescriptionSchema
53
+ });
54
+ var _requirementProfileSchema = z2.object({
55
+ target_path: z2.string(),
56
+ known_tech: z2.array(z2.string()),
57
+ user_intent: z2.string(),
58
+ detected_entities: z2.array(z2.string())
59
+ });
60
+ var planContextInputSchema = z2.object({
61
+ paths: z2.array(z2.string()).min(1).describe("Candidate file paths to build neutral rule selection context for"),
62
+ intent: z2.string().optional().describe("User-stated requirement or implementation intent; used only to build a neutral requirement profile"),
63
+ known_tech: z2.array(z2.string()).optional().describe("Known technologies involved in the requirement profile"),
64
+ detected_entities: z2.record(z2.array(z2.string())).optional().describe("Optional path-keyed detected entities for the requirement profile"),
65
+ client_hash: z2.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection"),
66
+ correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
67
+ session_id: z2.string().optional().describe(
68
+ "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."
69
+ ),
70
+ // v2.0-rc.5 A3 (TASK-007): `include_deprecated` removed — it was a no-op
71
+ // placeholder (MaturitySchema has no `deprecated` value). When the maturity
72
+ // enum widens we re-introduce the flag as part of that protocol bump.
73
+ // v2/rc.2 (Q6): client-supplied layer scope. When omitted, the server
74
+ // falls back to fabric-config.default_layer_filter (TASK-002) so a single
75
+ // workspace policy controls the default. Explicit values override.
76
+ layer_filter: z2.enum(["team", "personal", "both"]).optional().describe(
77
+ "Restrict description_index to the named layer. Default: fabric-config.default_layer_filter (TASK-002)."
78
+ ),
79
+ // v2.0-rc.5 C3 (TASK-012): explicit path context for `narrow` relevance
80
+ // filtering. When omitted, the server falls back to `paths` so existing
81
+ // callers see narrowing against the requested paths. When the resolved
82
+ // list is empty, the narrow filter fails open (every narrow entry passes).
83
+ target_paths: z2.array(z2.string()).optional().describe(
84
+ "Path context for narrow-scope relevance filtering. Defaults to `paths`; empty = no filter."
85
+ )
86
+ });
87
+ var _preflightDiagnosticSchema = z2.object({
88
+ // v2.0.0-rc.38 UX-2: `empty_shell_suppressed` surfaces draft entries whose
89
+ // description carries no selection signal (summary === stable_id + empty
90
+ // intent_clues/tech_stack/impact). They are filtered out of `candidates` to
91
+ // cut noise; this diagnostic names them so `fabric doctor` /
92
+ // --enrich-descriptions can prompt enrichment.
93
+ code: z2.enum(["missing_description", "empty_shell_suppressed"]),
94
+ severity: z2.literal("warn"),
95
+ message: z2.string(),
96
+ stable_ids: z2.array(z2.string()).optional(),
97
+ path: z2.string().optional()
98
+ });
99
+ var planContextOutputSchema = z2.object({
100
+ revision_hash: z2.string(),
101
+ stale: z2.boolean(),
102
+ selection_token: z2.string(),
103
+ entries: z2.array(
104
+ z2.object({
105
+ path: z2.string(),
106
+ requirement_profile: _requirementProfileSchema
107
+ })
108
+ ),
109
+ candidates: z2.array(_descriptionIndexItemSchema),
110
+ preflight_diagnostics: z2.array(_preflightDiagnosticSchema),
111
+ warnings: z2.array(structuredWarningSchema).optional(),
112
+ // v2.0.0-rc.22 Scope D T-D2: optional auto-heal banner fields. Surfaced
113
+ // ONLY when the loadActiveMetaOrStale call detected drift and rebuilt the
114
+ // meta in-place. Downstream CLI / hint renderers use this pair to render a
115
+ // "knowledge meta auto-healed (was <prev>, now <curr>)" notice without
116
+ // having to query the event ledger.
117
+ auto_healed: z2.boolean().optional(),
118
+ previous_revision_hash: z2.string().optional(),
119
+ // v2.0.0-rc.37 NEW-24: stale-id redirect map. Populated when one or more
120
+ // recent fab_review modify-layer flips reassigned a canonical stable_id
121
+ // and the NEW id is in this response's description_index. Callers that
122
+ // cached the OLD id from a prior session look it up here and substitute
123
+ // the new id before issuing fab_get_knowledge_sections / fab_recall. Empty
124
+ // (field omitted) when no actionable redirects exist for the surfaced
125
+ // candidate set. See packages/server/src/services/id-redirect.ts.
126
+ redirects: z2.record(z2.string()).optional()
127
+ });
128
+ var planContextAnnotations = {
129
+ readOnlyHint: true,
130
+ idempotentHint: true,
131
+ destructiveHint: false,
132
+ openWorldHint: false,
133
+ title: "Plan rule context"
134
+ };
135
+ var planContextHintNarrowEntrySchema = z2.object({
136
+ id: z2.string(),
137
+ type: z2.string(),
138
+ maturity: z2.string(),
139
+ summary: z2.string()
140
+ });
141
+ var planContextHintOutputSchema = z2.object({
142
+ version: z2.literal(1),
143
+ revision_hash: z2.string(),
144
+ target_paths: z2.array(z2.string()),
145
+ narrow: z2.array(planContextHintNarrowEntrySchema),
146
+ broad_count: z2.number().int().nonnegative()
147
+ });
148
+ var knowledgeSectionsInputSchema = z2.object({
149
+ selection_token: z2.string().min(1).describe("Selection token returned by fab_plan_context"),
150
+ ai_selected_stable_ids: z2.array(z2.string()).describe(
151
+ "Stable ids picked from fab_plan_context candidates[].stable_id; choose 1..N to fetch bodies for"
152
+ ),
153
+ ai_selection_reasons: z2.record(z2.string().min(1)).describe("Reason for each AI-selected L1 stable_id"),
154
+ correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records"),
155
+ session_id: z2.string().optional().describe("Optional caller-provided session id for Event Ledger records"),
156
+ // v2.0 rc.5 TASK-014 (C5): optional client identity hash propagated into
157
+ // knowledge_consumed events. Falls back to empty string when unset — full
158
+ // client-identity propagation deferred to rc.6.
159
+ client_hash: z2.string().optional().describe("Optional caller-provided client hash propagated into knowledge_consumed events")
160
+ });
161
+ var knowledgeSectionsOutputSchema = z2.object({
162
+ revision_hash: z2.string(),
163
+ // v2.0.0-rc.38 UX-13 (D-MCP step-2 audit): the deprecated `precedence`
164
+ // L2/L1/L0 tuple (flagged "removed in rc.24" but still emitted) is gone — it
165
+ // was a constant 3-string field on every response read by no production
166
+ // consumer. Use rules[].level for ordering.
167
+ selected_stable_ids: z2.array(z2.string()),
168
+ rules: z2.array(
169
+ z2.object({
170
+ stable_id: z2.string(),
171
+ level: z2.enum(["L0", "L1", "L2"]),
172
+ path: z2.string(),
173
+ // v2.0.0-rc.23 TASK-013 (F8b): replaced the legacy
174
+ // `sections: Record<string,string>` (keyed by the 4-element A-set enum)
175
+ // with the full markdown body (frontmatter stripped). Callers scan the
176
+ // body for whichever B-set heading they need (Summary / Why proposed /
177
+ // Session context / Evidence) — section-name discipline is now a writer
178
+ // convention, not an API contract.
179
+ body: z2.string()
180
+ })
181
+ ),
182
+ diagnostics: z2.array(
183
+ // v2.0.0-rc.23 TASK-013 (F8b): `missing_section` was removed alongside the
184
+ // A-set enum. `missing_knowledge_metadata` stays as the warn-level signal
185
+ // for un-migrated v1.x entries (no knowledge_type AND no knowledge_layer
186
+ // in frontmatter). Does NOT block selection.
187
+ z2.object({
188
+ code: z2.literal("missing_knowledge_metadata"),
189
+ severity: z2.literal("warn"),
190
+ stable_id: z2.string(),
191
+ message: z2.string()
192
+ })
193
+ ),
194
+ // v2/rc.3 (Q6) + v2.0.0-rc.37 NEW-24: present iff at least one stable_id in
195
+ // the caller-supplied ai_selected_stable_ids was rewritten by the layer-flip
196
+ // redirect resolver. Pre-rc.37: this was a single { stable_id } object set
197
+ // only on the rare token-mint-vs-flip race. rc.37+: also accepts a map of
198
+ // (old_id → new_id) when multiple rewrites fire in one fetch. Both shapes
199
+ // are accepted for forward-compat; readers should branch on shape and
200
+ // refresh their cached ids accordingly.
201
+ redirect_to: z2.union([
202
+ z2.object({ stable_id: z2.string() }),
203
+ z2.record(z2.string())
204
+ ]).optional().describe(
205
+ "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."
206
+ ),
207
+ warnings: z2.array(structuredWarningSchema).optional()
208
+ });
209
+ var knowledgeSectionsAnnotations = {
210
+ readOnlyHint: true,
211
+ idempotentHint: true,
212
+ destructiveHint: false,
213
+ openWorldHint: false,
214
+ title: "Filter rule sections"
215
+ };
216
+ var recallInputSchema = z2.object({
217
+ paths: z2.array(z2.string()).min(1).describe(
218
+ "Candidate file paths to recall Fabric rules for. Same semantics as fab_plan_context.paths."
219
+ ),
220
+ intent: z2.string().optional().describe("User-stated requirement or implementation intent; used to build a neutral requirement profile."),
221
+ known_tech: z2.array(z2.string()).optional().describe("Known technologies involved."),
222
+ detected_entities: z2.record(z2.array(z2.string())).optional().describe("Optional path-keyed detected entities."),
223
+ client_hash: z2.string().optional().describe("Revision hash from a prior call; enables stale detection."),
224
+ correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
225
+ session_id: z2.string().optional().describe(
226
+ "Current client session id (Claude Code: $session_id; Codex: corresponding identifier). Enables cross-session debt tracking. Falls back gracefully if omitted."
227
+ ),
228
+ layer_filter: z2.enum(["team", "personal", "both"]).optional().describe(
229
+ "Restrict recall to the named layer. Default: fabric-config.default_layer_filter."
230
+ ),
231
+ target_paths: z2.array(z2.string()).optional().describe(
232
+ "Path context for narrow-scope relevance filtering. Defaults to `paths`; empty = no filter."
233
+ ),
234
+ ids: z2.array(z2.string()).optional().describe(
235
+ "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."
236
+ )
237
+ });
238
+ var recallOutputSchema = z2.object({
239
+ revision_hash: z2.string(),
240
+ stale: z2.boolean(),
241
+ // Selection token surfaced for callers who want to continue the conversation
242
+ // with fab_get_knowledge_sections (e.g. fetch additional ids later) — every
243
+ // recall response is still token-backed internally.
244
+ selection_token: z2.string(),
245
+ // v2.0.0-rc.38 UX-1/UX-4: mirrors planContextOutputSchema fold ① — per-path
246
+ // description_index collapsed into a single top-level `candidates`, and
247
+ // `preflight_diagnostics` lifted out of the removed `shared` wrapper.
248
+ entries: z2.array(
249
+ z2.object({
250
+ path: z2.string(),
251
+ requirement_profile: _requirementProfileSchema
252
+ })
253
+ ),
254
+ candidates: z2.array(_descriptionIndexItemSchema),
255
+ preflight_diagnostics: z2.array(_preflightDiagnosticSchema),
256
+ // Same shape as knowledgeSectionsOutputSchema.rules — full body keyed by stable_id.
257
+ rules: z2.array(
258
+ z2.object({
259
+ stable_id: z2.string(),
260
+ level: z2.enum(["L0", "L1", "L2"]),
261
+ path: z2.string(),
262
+ body: z2.string()
263
+ })
264
+ ),
265
+ selected_stable_ids: z2.array(z2.string()),
266
+ diagnostics: z2.array(
267
+ z2.object({
268
+ code: z2.literal("missing_knowledge_metadata"),
269
+ severity: z2.literal("warn"),
270
+ stable_id: z2.string(),
271
+ message: z2.string()
272
+ })
273
+ ),
274
+ warnings: z2.array(structuredWarningSchema).optional(),
275
+ auto_healed: z2.boolean().optional(),
276
+ previous_revision_hash: z2.string().optional(),
277
+ // v2.0.0-rc.37 NEW-24: parallel to planContextOutputSchema.redirects — see
278
+ // that field for semantics. fab_recall transparently rewrites any old ids
279
+ // passed via `ids` before fetching bodies; the surfaced map still exposes
280
+ // the substitution to callers that want to refresh their cached state.
281
+ redirects: z2.record(z2.string()).optional()
282
+ });
283
+ var recallAnnotations = {
284
+ readOnlyHint: true,
285
+ idempotentHint: true,
286
+ destructiveHint: false,
287
+ openWorldHint: false,
288
+ title: "Recall Fabric knowledge (one-call)"
289
+ };
290
+ var archiveScanInputSchema = z2.object({
291
+ range: z2.union([z2.array(z2.string()).min(1), z2.literal("all")]).optional().describe(
292
+ "Phase 0 scope: explicit session_id[] to constrain the scan, or the 'all' sentinel. Omitted = scan everything since the last knowledge_proposed anchor."
293
+ ),
294
+ now_ms: z2.number().int().nonnegative().optional().describe("Override for the anti-loop cooldown clock (testing). Defaults to Date.now()."),
295
+ correlation_id: z2.string().optional().describe("Optional caller-provided correlation id for Event Ledger records."),
296
+ session_id: z2.string().optional().describe("Current client session id; recorded for cross-session debt tracking.")
297
+ });
298
+ var archiveScanOutputSchema = z2.object({
299
+ // ts of the most recent knowledge_proposed event (the lower bound), or null
300
+ // when the workspace has never archived (scan everything).
301
+ anchor_ts: z2.number().nullable(),
302
+ // Distinct session_ids since the anchor that survived the outcome filter,
303
+ // in first-seen order — ready for the Skill to load digests + stitch.
304
+ session_ids: z2.array(z2.string()),
305
+ // Sessions dropped by the filter, with the rule that fired (audit/debug).
306
+ dropped: z2.array(
307
+ z2.object({
308
+ session_id: z2.string(),
309
+ reason: z2.enum(["user_dismissed", "cooldown", "no_new_signal"])
310
+ })
311
+ ),
312
+ // max ts examined across the scan — becomes the next covered_through_ts.
313
+ covered_through_ts: z2.number().nullable(),
314
+ // Idempotency keys already proposed by prior archive runs but not yet
315
+ // reviewed (Phase 4.5 cross-session pending dedupe). Drop matching candidates.
316
+ already_proposed_keys: z2.array(z2.string()),
317
+ warnings: z2.array(structuredWarningSchema).optional()
318
+ });
319
+ var archiveScanAnnotations = {
320
+ readOnlyHint: true,
321
+ idempotentHint: true,
322
+ destructiveHint: false,
323
+ openWorldHint: false,
324
+ title: "Scan event ledger for archive candidates (deterministic)"
325
+ };
326
+ var ProposedReasonSchema = z2.enum([
327
+ "explicit-user-mark",
328
+ "diagnostic-then-fix",
329
+ "decision-confirmation",
330
+ "wrong-turn-revert",
331
+ "new-dependency-or-pattern",
332
+ "dismissal-with-reason"
333
+ ]);
334
+ var PROPOSED_REASON_DESCRIPTIONS = {
335
+ "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",
336
+ "diagnostic-then-fix": "\u8BCA\u65AD\u8FC7\u7A0B\u53D1\u73B0\u65B0\u6A21\u5F0F\u6216\u8E29\u5751\uFF0C\u4FEE\u590D\u540E\u503C\u5F97\u6C89\u6DC0\u3002",
337
+ "decision-confirmation": "\u22652 \u5019\u9009\u65B9\u6848\u7ECF\u6743\u8861\u540E\u786E\u8BA4\u9009\u578B\uFF0C\u9700\u4FDD\u7559 rationale\u3002",
338
+ "wrong-turn-revert": "\u5C1D\u8BD5\u67D0\u8DEF\u5F84\u540E\u56DE\u9000\uFF0C\u9519\u8BEF\u8DEF\u5F84\u672C\u8EAB\u662F\u503C\u5F97\u8BB0\u5F55\u7684 pitfall\u3002",
339
+ "new-dependency-or-pattern": "\u5F15\u5165\u65B0\u4F9D\u8D56 / \u65B0\u6A21\u5F0F / \u65B0\u547D\u540D\u7EA6\u5B9A\u3002",
340
+ "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"
341
+ };
342
+ var _sourceSessionsField = z2.array(z2.string().min(1)).min(1);
343
+ var _FabExtractKnowledgeInputBaseSchema = z2.object({
344
+ // v2.0.0-rc.7 T5: array form. rc.23 dropped the legacy single-string alias.
345
+ source_sessions: _sourceSessionsField.optional().describe(
346
+ "Originating session ids; correlates with Event Ledger records. Array form (T5+, rc.23 made it the sole accepted shape)."
347
+ ),
348
+ recent_paths: z2.array(z2.string()).describe("Workspace paths recently touched in the source session \u2014 used as scope hints"),
349
+ user_messages_summary: z2.string().describe("Skill-side summary of the user's intent/messages, kept compact"),
350
+ type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).describe("Knowledge type bucket (plural form, mirrors directory layout)"),
351
+ slug: z2.string().describe("URL-safe short identifier proposed by the Skill; server may sanitize"),
352
+ // rc.5 B1: dual pending root. When 'personal', the server writes to
353
+ // ~/.fabric/knowledge/pending/<type>/; otherwise to .fabric/knowledge/pending/<type>/.
354
+ // Defaults to 'team' to preserve existing call sites (Skill bumps as needed).
355
+ layer: z2.enum(["team", "personal"]).optional().describe(
356
+ "Storage layer for the pending entry. 'team' writes under the workspace; 'personal' writes under the user's home. Defaults to 'team'."
357
+ ),
358
+ // v2.0.0-rc.7 T6: proposed_reason — required enum that drives `## Why
359
+ // proposed` rendering. Skills (archive / import / review) infer the
360
+ // appropriate reason per their semantics (see each SKILL.md).
361
+ proposed_reason: ProposedReasonSchema.describe(
362
+ "Why this entry is being proposed. Drives `## Why proposed` rendering and enables future maturity-promotion scoring."
363
+ ),
364
+ // v2.0.0-rc.7 T6: session_context — required 3-5 line markdown blob that
365
+ // captures the session goal + key turning point. Future-self review reads
366
+ // this without conversation transcript access. Min length guards against
367
+ // empty placeholders; cap is soft (no max), Skill caps at ~600 chars.
368
+ session_context: z2.string().min(20, { message: "session_context must be \u226520 chars (3-5 lines describing goal + turning point)" }).describe(
369
+ "3-5 line markdown blob \u2014 session goal + key turning point. Reviewed by future-self without transcript access."
370
+ ),
371
+ // v2.0.0-rc.8 A1 (skill-contract-fix): relevance scope/paths on the
372
+ // creation surface. Mirrors `_fabReviewModifyChangesSchema.relevance_*`
373
+ // (L518-533) verbatim so callers can declare scope at archive time
374
+ // instead of waiting for a fab_review.modify follow-up. Both fields are
375
+ // optional — when omitted, the pending file omits the YAML lines entirely
376
+ // (knowledge-meta-builder defaults to broad + [] at parse time, see
377
+ // L1007-1021). Personal + narrow is silently degraded to broad + [] at
378
+ // service entry, mirroring the rc.5 review.ts:725-739 behaviour, and emits
379
+ // a `knowledge_scope_degraded` event keyed by `pending:<idempotency_key>`.
380
+ // NOTE: these fields MUST NOT be part of the idempotency hash inputs at
381
+ // extract-knowledge.ts:78 — preserves rc.5→rc.7 collision detection.
382
+ relevance_scope: z2.enum(["narrow", "broad"]).optional().describe(
383
+ "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 + []."
384
+ ),
385
+ relevance_paths: z2.array(z2.string()).optional().describe(
386
+ "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)."
387
+ ),
388
+ // v2.0.0-rc.23 TASK-006 (a-C1): four optional structured fields that the
389
+ // skill-side LLM populates from raw observations. The same information
390
+ // historically lived only in `## Session context` prose, forcing future-self
391
+ // reviewers / plan-context retrievers to re-read the entire body to decide
392
+ // relevance. Lifting them into structured frontmatter lets downstream
393
+ // surfaces (description_index, scoring, relevance triage) consume them
394
+ // directly. ALL FOUR ARE STRICTLY OPTIONAL — skills that cannot infer them
395
+ // confidently must omit, not guess.
396
+ //
397
+ // IMPORTANT: these fields MUST NOT participate in the idempotency_key hash
398
+ // (see rc.8 A1 convention at extract-knowledge.ts — relevance_scope /
399
+ // relevance_paths follow the same rule). Including them would let an LLM
400
+ // re-roll of the same observation create a second pending file just because
401
+ // its inferred metadata wording drifted.
402
+ intent_clues: z2.array(z2.string()).optional().describe(
403
+ "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."
404
+ ),
405
+ tech_stack: z2.array(z2.string()).optional().describe(
406
+ "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."
407
+ ),
408
+ impact: z2.array(z2.string()).optional().describe(
409
+ "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."
410
+ ),
411
+ must_read_if: z2.string().optional().describe(
412
+ "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."
413
+ ),
414
+ // v2.0.0-rc.37 NEW-37 (werewolf dogfood remediation): optional tags array.
415
+ // Werewolf实测发现 100% canonical entries 的 `tags: []` 为空,主题聚类与
416
+ // 跨条目检索退化。Skills (fabric-archive / fabric-import) 应每个 entry 产
417
+ // 2-4 个 kebab-case 主题词。Server 写入时直接落 frontmatter `tags: [...]`;
418
+ // empty array 仍然合法(skill 无法 confident 推断时显式空)。
419
+ // IDEMPOTENCY: tags MUST NOT 参与 idempotency_key hash(同 relevance_*
420
+ // / intent_clues 等可变字段一致),re-extract 时 tags 调整不应产生重复
421
+ // pending file。
422
+ tags: z2.array(z2.string()).optional().describe(
423
+ "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)."
424
+ ),
425
+ // v2.0.0-rc.23 TASK-014 (F8c): optional onboard-slot tag. The S5 slot
426
+ // mechanism reintroduces a Skill-orchestrated "project tone" capture
427
+ // surface after F8a deleted the auto-`fabric scan` baseline pipeline.
428
+ // fabric-archive's first-run phase reads `fabric onboard-coverage` to
429
+ // discover unclaimed slots, then propagates the chosen slot label here
430
+ // so the resulting pending entry counts toward coverage.
431
+ //
432
+ // STRICT optionality: every non-onboard fab_extract_knowledge call MUST
433
+ // omit this field. The skill is the only producer; downstream consumers
434
+ // (plan_context retrieval, doctor lints) treat missing as a steady-state
435
+ // signal that the entry was NOT part of an onboard pass.
436
+ //
437
+ // IDEMPOTENCY: like the four a-C1 fields and the rc.8 A1 relevance pair,
438
+ // `onboard_slot` MUST NOT participate in the idempotency_key hash at
439
+ // extract-knowledge.ts:100-106. An LLM that re-rolls the same observation
440
+ // with a different (or absent) slot must still collapse onto the same
441
+ // pending file — otherwise the slot mechanic itself could spawn
442
+ // duplicate entries.
443
+ onboard_slot: onboardSlotSchema.optional().describe(
444
+ "Optional slot tag from the S5 onboarding set (tech-stack-decision / architecture-pattern / code-style-tone / build-system-idiom / domain-vocabulary); lets fabric-archive's first-run phase claim a project-tone slot. Skill propose-time only; never required."
445
+ ),
446
+ // v2.0.0-rc.37 NEW-7: read-only evidence paths lifted from the legacy
447
+ // body `## Evidence` markdown block into structured frontmatter. These are
448
+ // paths the agent CONSULTED while building this knowledge but never
449
+ // modified — they document context without participating in the
450
+ // activation gate (relevance_paths does that). Splitting evidence into a
451
+ // first-class frontmatter array lets future plan-context retrieval read
452
+ // it as data (intersect with current paths to surface high-recall hits)
453
+ // instead of re-parsing markdown. Optional; omit when no read-only
454
+ // signal was captured. Like relevance_paths it MUST NOT participate in
455
+ // the idempotency_key hash (an idempotent re-extract may surface a
456
+ // slightly different read set without spawning a duplicate pending).
457
+ evidence_paths: z2.array(z2.string()).optional().describe(
458
+ "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."
459
+ )
460
+ });
461
+ var FabExtractKnowledgeInputSchema = _FabExtractKnowledgeInputBaseSchema.superRefine(
462
+ (value, ctx) => {
463
+ const hasArray = Array.isArray(value.source_sessions) && value.source_sessions.length > 0;
464
+ if (!hasArray) {
465
+ ctx.addIssue({
466
+ code: z2.ZodIssueCode.custom,
467
+ message: "source_sessions (non-empty string array) is required",
468
+ path: ["source_sessions"]
469
+ });
470
+ }
471
+ }
472
+ );
473
+ var FabExtractKnowledgeInputShape = _FabExtractKnowledgeInputBaseSchema.shape;
474
+ var FabExtractKnowledgeOutputSchema = z2.object({
475
+ pending_path: z2.string().describe("Workspace-relative path to the persisted pending entry"),
476
+ idempotency_key: z2.string().describe("Stable key derived from inputs; identical inputs yield identical key"),
477
+ // v2.0.0-rc.23 TASK-009 (d): optional warnings surface for the first-reconcile
478
+ // gate (`meta_stale` / `reconcile_failed`). Absent on the steady-state path.
479
+ warnings: z2.array(structuredWarningSchema).optional()
480
+ });
481
+ var fabExtractKnowledgeAnnotations = {
482
+ readOnlyHint: false,
483
+ idempotentHint: true,
484
+ destructiveHint: false,
485
+ openWorldHint: false,
486
+ title: "Extract pending knowledge entry"
487
+ };
488
+ var _fabReviewFiltersSchema = z2.object({
489
+ type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]).optional(),
490
+ layer: z2.enum(["team", "personal", "both"]).optional(),
491
+ maturity: z2.enum(["draft", "verified", "proven"]).optional(),
492
+ tags: z2.array(z2.string()).optional(),
493
+ // rc.4 TASK-006 fix (c): ISO-8601 lower bound on entry created_at; entries
494
+ // strictly older than this threshold are excluded from list / search
495
+ // results. Additive optional field — existing callers unaffected.
496
+ created_after: z2.string().datetime().optional(),
497
+ // v2.0.0-rc.27 TASK-001 (§2.2/§2.3): opt-in surfacing of lifecycle-filtered
498
+ // entries. Default (omit both) hides rejected entries and deferred entries
499
+ // whose deferred_until is in the future. Pass true to include them — e.g.
500
+ // for vacuum tooling, audit dashboards, or "show me what I parked" UX.
501
+ include_rejected: z2.boolean().optional(),
502
+ include_deferred: z2.boolean().optional(),
503
+ // v2.0.0-rc.27 TASK-006 (audit §2.23): opt-in body inspection. Default
504
+ // list/search return only frontmatter-derived fields — a malicious
505
+ // pending entry could hide a prompt-injection payload under `## Evidence`
506
+ // body content that frontmatter inspection never surfaces. Setting
507
+ // `include_body: true` attaches the full post-frontmatter content to
508
+ // each item, and (for search) extends the haystack to body text. The
509
+ // default-off design keeps the wire payload small for routine list
510
+ // calls; reviewer workflows pass `true` before approving so the body
511
+ // is rendered into the reviewer's UI for visual scan.
512
+ include_body: z2.boolean().optional()
513
+ }).optional();
514
+ var _fabReviewModifyChangesSchema = z2.object({
515
+ title: z2.string().optional(),
516
+ summary: z2.string().optional(),
517
+ // Q7: writing `layer` here triggers a layer-flip; downstream callers may
518
+ // observe a redirect_to in fab_get_knowledge_sections if stable_id changes.
519
+ layer: z2.enum(["team", "personal"]).optional(),
520
+ maturity: z2.enum(["draft", "verified", "proven"]).optional(),
521
+ tags: z2.array(z2.string()).optional(),
522
+ // v2.0-rc.5 C3 (TASK-012): relevance scope/paths patches. Applied to
523
+ // pending AND canonical entries. When an explicit team→personal layer flip
524
+ // arrives on a narrow entry, the server auto-degrades to broad + [] and
525
+ // emits a `knowledge_scope_degraded` event regardless of what the caller
526
+ // sent in these fields (personal-implies-broad).
527
+ relevance_scope: z2.enum(["narrow", "broad"]).optional(),
528
+ relevance_paths: z2.array(z2.string()).optional()
529
+ });
530
+ var FabReviewInputSchema = z2.discriminatedUnion("action", [
531
+ z2.object({
532
+ action: z2.literal("list"),
533
+ filters: _fabReviewFiltersSchema
534
+ }),
535
+ z2.object({
536
+ action: z2.literal("approve"),
537
+ pending_paths: z2.array(z2.string()).min(1)
538
+ }),
539
+ z2.object({
540
+ action: z2.literal("reject"),
541
+ pending_paths: z2.array(z2.string()).min(1),
542
+ reason: z2.string().min(1)
543
+ }),
544
+ z2.object({
545
+ action: z2.literal("modify"),
546
+ pending_path: z2.string().min(1),
547
+ changes: _fabReviewModifyChangesSchema
548
+ }),
549
+ // v2.0.0-rc.37 NEW-12: explicit modify split. `modify-content` edits scalar
550
+ // frontmatter/body fields (title/summary/maturity/tags/relevance_*) and MUST
551
+ // NOT carry a layer change. `modify-layer` is the dedicated layer-flip path
552
+ // (changes.layer REQUIRED) which may reallocate the stable_id + emit an
553
+ // id-redirect (rc.37 NEW-24). Legacy `modify` stays for back-compat and
554
+ // routes by whether changes.layer is present.
555
+ z2.object({
556
+ action: z2.literal("modify-content"),
557
+ pending_path: z2.string().min(1),
558
+ changes: _fabReviewModifyChangesSchema
559
+ }),
560
+ z2.object({
561
+ action: z2.literal("modify-layer"),
562
+ pending_path: z2.string().min(1),
563
+ changes: _fabReviewModifyChangesSchema.extend({
564
+ layer: z2.enum(["team", "personal"])
565
+ })
566
+ }),
567
+ z2.object({
568
+ action: z2.literal("search"),
569
+ query: z2.string().min(1),
570
+ filters: _fabReviewFiltersSchema
571
+ }),
572
+ z2.object({
573
+ action: z2.literal("defer"),
574
+ pending_paths: z2.array(z2.string()).min(1),
575
+ until: z2.string().datetime().optional(),
576
+ reason: z2.string().optional()
577
+ })
578
+ ]);
579
+ var FabReviewInputShape = {
580
+ action: z2.enum(["list", "approve", "reject", "modify", "modify-content", "modify-layer", "search", "defer"]).describe(
581
+ "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."
582
+ ),
583
+ filters: _fabReviewFiltersSchema.describe(
584
+ "Optional filters (type/layer/maturity/tags/created_after). Used by action=list and action=search."
585
+ ),
586
+ pending_paths: z2.array(z2.string()).min(1).optional().describe(
587
+ "Workspace-relative pending entry paths. Required when action=approve|reject|defer (non-empty array)."
588
+ ),
589
+ pending_path: z2.string().min(1).optional().describe(
590
+ "Workspace-relative pending OR canonical entry path. Required when action=modify."
591
+ ),
592
+ reason: z2.string().optional().describe(
593
+ "Reason string. Required (non-empty) when action=reject; optional when action=defer."
594
+ ),
595
+ changes: _fabReviewModifyChangesSchema.optional().describe(
596
+ "Frontmatter scalar patches (title/summary/layer/maturity/tags/relevance_*). Required when action=modify."
597
+ ),
598
+ query: z2.string().min(1).optional().describe(
599
+ "Substring query against title/summary/tags/path. Required (non-empty) when action=search."
600
+ ),
601
+ until: z2.string().datetime().optional().describe(
602
+ "ISO-8601 datetime upper bound for the deferral. Optional; used only when action=defer."
603
+ )
604
+ };
605
+ var _fabReviewListItemSchema = z2.object({
606
+ pending_path: z2.string(),
607
+ // v2.0.0-rc.27 TASK-001 (§2.12): for personal-layer entries `pending_path`
608
+ // carries the human-friendly `~/...` form (legacy contract) while
609
+ // `pending_path_absolute` carries the os-expanded absolute path. Programmatic
610
+ // consumers (Read tool, fs.readFile, downstream MCP servers) should prefer
611
+ // the absolute variant — the `~` is a shell-only sigil that breaks every
612
+ // non-shell consumer. Team entries omit this field because their
613
+ // `pending_path` is already project-relative and unambiguous.
614
+ pending_path_absolute: z2.string().optional(),
615
+ type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
616
+ layer: z2.enum(["team", "personal"]),
617
+ maturity: z2.enum(["draft", "verified", "proven"]),
618
+ tags: z2.array(z2.string()).optional(),
619
+ title: z2.string().optional(),
620
+ summary: z2.string().optional(),
621
+ // rc.5 B1: dual pending root. 'team' = workspace .fabric/knowledge/pending,
622
+ // 'personal' = ~/.fabric/knowledge/pending. Distinct from `layer` (frontmatter):
623
+ // origin reflects where the pending file actually lives on disk; layer reflects
624
+ // the declared classification that will drive the approve destination.
625
+ origin: z2.enum(["team", "personal"]).optional(),
626
+ // v2.0.0-rc.27 TASK-001 (§2.2/§2.3): frontmatter status markers. Default
627
+ // "active" (or absent). `rejected` entries are excluded from list/search
628
+ // unless filters.include_rejected=true; `deferred` entries are excluded
629
+ // when deferred_until is in the future. Authored by reject/defer write
630
+ // paths — never by extract or approve.
631
+ status: z2.enum(["active", "rejected", "deferred"]).optional(),
632
+ deferred_until: z2.string().datetime().optional(),
633
+ // v2.0.0-rc.27 TASK-006 (audit §2.23): full body content (everything
634
+ // after the closing `---` of frontmatter). Surfaced only when caller
635
+ // passes `filters.include_body: true`. Default-omitted to keep payload
636
+ // small for routine list calls.
637
+ body: z2.string().optional()
638
+ });
639
+ var _fabReviewSearchItemSchema = z2.object({
640
+ // Search hits live in one of two trees:
641
+ // - "pending" → .fabric/knowledge/pending/ (or ~/.fabric/knowledge/pending/)
642
+ // - "canonical" → .fabric/knowledge/{decisions,pitfalls,...} (or personal mirror)
643
+ area: z2.enum(["pending", "canonical"]),
644
+ path: z2.string(),
645
+ path_absolute: z2.string().optional(),
646
+ type: z2.enum(["decisions", "pitfalls", "guidelines", "models", "processes"]),
647
+ layer: z2.enum(["team", "personal"]),
648
+ maturity: z2.enum(["draft", "verified", "proven"]),
649
+ tags: z2.array(z2.string()).optional(),
650
+ title: z2.string().optional(),
651
+ summary: z2.string().optional(),
652
+ origin: z2.enum(["team", "personal"]).optional(),
653
+ status: z2.enum(["active", "rejected", "deferred"]).optional(),
654
+ deferred_until: z2.string().datetime().optional(),
655
+ body: z2.string().optional(),
656
+ // For pending hits the upstream stable_id may still be unassigned — keep it
657
+ // optional so canonical hits (which always have one) parse alongside pending
658
+ // hits in the same array.
659
+ stable_id: z2.string().optional()
660
+ });
661
+ var FabReviewOutputSchema = z2.discriminatedUnion("action", [
662
+ z2.object({
663
+ action: z2.literal("list"),
664
+ items: z2.array(_fabReviewListItemSchema),
665
+ warnings: z2.array(structuredWarningSchema).optional()
666
+ }),
667
+ z2.object({
668
+ action: z2.literal("approve"),
669
+ approved: z2.array(z2.object({ pending_path: z2.string(), stable_id: z2.string() })),
670
+ warnings: z2.array(structuredWarningSchema).optional()
671
+ }),
672
+ z2.object({
673
+ action: z2.literal("reject"),
674
+ rejected: z2.array(z2.string()),
675
+ warnings: z2.array(structuredWarningSchema).optional()
676
+ }),
677
+ z2.object({
678
+ action: z2.literal("modify"),
679
+ pending_path: z2.string(),
680
+ // When a layer-flip occurred, prior_stable_id and new_stable_id differ.
681
+ prior_stable_id: z2.string().optional(),
682
+ new_stable_id: z2.string().optional(),
683
+ warnings: z2.array(structuredWarningSchema).optional()
684
+ }),
685
+ z2.object({
686
+ action: z2.literal("search"),
687
+ // v2.0.0-rc.29 TASK-007 (BUG-M4): search returns the new search-item
688
+ // shape with `area` discriminator + neutrally-named `path` field.
689
+ items: z2.array(_fabReviewSearchItemSchema),
690
+ warnings: z2.array(structuredWarningSchema).optional()
691
+ }),
692
+ z2.object({
693
+ action: z2.literal("defer"),
694
+ deferred: z2.array(z2.string()),
695
+ warnings: z2.array(structuredWarningSchema).optional()
696
+ })
697
+ ]);
698
+ var FabReviewOutputShape = {
699
+ action: z2.enum(["list", "approve", "reject", "modify", "search", "defer"]).describe(
700
+ "Echoes the input action; clients can switch on it for per-variant fields below."
701
+ ),
702
+ items: z2.array(z2.union([_fabReviewListItemSchema, _fabReviewSearchItemSchema])).optional().describe(
703
+ "Pending entries (action=list, `pending_path` shape) or pending+canonical entries (action=search, `area`+`path` shape)."
704
+ ),
705
+ approved: z2.array(z2.object({ pending_path: z2.string(), stable_id: z2.string() })).optional().describe(
706
+ "Allocated stable ids paired with their original pending paths. Present when action=approve."
707
+ ),
708
+ rejected: z2.array(z2.string()).optional().describe(
709
+ "Pending paths that were rejected (files retained on disk; doctor owns vacuum). Present when action=reject."
710
+ ),
711
+ pending_path: z2.string().optional().describe(
712
+ "Echoed target path for the modification. Present when action=modify."
713
+ ),
714
+ prior_stable_id: z2.string().optional().describe(
715
+ "Prior stable id. Present when action=modify AND a layer-flip reallocated the id."
716
+ ),
717
+ new_stable_id: z2.string().optional().describe(
718
+ "New stable id after reallocation. Present when action=modify AND a layer-flip reallocated the id."
719
+ ),
720
+ deferred: z2.array(z2.string()).optional().describe(
721
+ "Pending paths that were deferred (files retained on disk). Present when action=defer."
722
+ ),
723
+ // v2.0.0-rc.23 TASK-009 (d): optional warnings surface for the first-reconcile
724
+ // gate (`meta_stale` / `reconcile_failed`). Absent on the steady-state path.
725
+ warnings: z2.array(structuredWarningSchema).optional()
726
+ };
727
+ var fabReviewAnnotations = {
728
+ readOnlyHint: false,
729
+ idempotentHint: false,
730
+ destructiveHint: false,
731
+ openWorldHint: false,
732
+ title: "Review pending knowledge entries"
733
+ };
734
+ var citeContractMetricsSchema = z2.object({
735
+ decisions_cited: z2.number().int().nonnegative(),
736
+ pitfalls_cited: z2.number().int().nonnegative(),
737
+ contract_with: z2.number().int().nonnegative(),
738
+ contract_missing: z2.number().int().nonnegative(),
739
+ hard_violated: z2.number().int().nonnegative(),
740
+ cite_id_unresolved: z2.number().int().nonnegative(),
741
+ skip_count: z2.record(z2.string(), z2.number().int().nonnegative())
742
+ });
743
+ var citeLayerTypeBreakdownSchema = z2.object({
744
+ team: z2.record(z2.string(), z2.number().int().nonnegative()),
745
+ personal: z2.record(z2.string(), z2.number().int().nonnegative())
746
+ });
747
+ var citeCoverageReportSchema = z2.object({
748
+ status: z2.enum(["ok", "skipped"]),
749
+ marker_ts: z2.number().int().nonnegative(),
750
+ marker_emitted_now: z2.boolean(),
751
+ since_ts: z2.number().int().nonnegative(),
752
+ client_filter: z2.enum(["cc", "codex", "cursor", "all"]),
753
+ // v2.0.0-rc.24 TASK-08: layer filter discriminator. Optional so pre-TASK-10
754
+ // CLI callers (which never set the flag) still parse. Defaults to "all" at
755
+ // the service layer.
756
+ layer_filter: z2.enum(["team", "personal", "all"]).optional(),
757
+ metrics: z2.object({
758
+ edits_touched: z2.number().int().nonnegative(),
759
+ qualifying_cites: z2.number().int().nonnegative(),
760
+ recalled_unverified: z2.number().int().nonnegative(),
761
+ expected_but_missed: z2.number().int().nonnegative(),
762
+ total_turns: z2.number().int().nonnegative(),
763
+ // v2.0.0-rc.38 UX-8 (C, user-authorized): cite-policy COMPLIANCE rate —
764
+ // the corrected G-CITE semantic. The legacy qualifying_cites/edits ratio
765
+ // measured "how often an applicable KB id existed" (a function of corpus
766
+ // density / soak), NOT "did the AI follow the cite policy". Compliance
767
+ // credits every valid cite line — `KB: <id> [applied|dismissed]` AND
768
+ // `KB: none [reason]` (the policy explicitly allows the none sentinel) —
769
+ // over the turns where a cite was expected. null when no cite-expected
770
+ // turns observed (avoids a misleading 0/0 → 0). Range [0,1].
771
+ cite_compliance_rate: z2.number().min(0).max(1).nullable().optional(),
772
+ compliant_cites: z2.number().int().nonnegative().optional(),
773
+ noncompliant_cites: z2.number().int().nonnegative().optional(),
774
+ // Edit signals lacking session_id → uncorrelatable, silently excluded from
775
+ // expected_but_missed. >0 typically means a stale pre-session_id hook is
776
+ // installed (run `fabric install`). Surfaced so the denominator gap is
777
+ // visible rather than a silent 100% confound.
778
+ uncorrelatable_edits: z2.number().int().nonnegative().optional()
779
+ }),
780
+ per_client: z2.record(
781
+ z2.string(),
782
+ z2.object({
783
+ edits_touched: z2.number().int().nonnegative().optional(),
784
+ qualifying_cites: z2.number().int().nonnegative().optional(),
785
+ recalled_unverified: z2.number().int().nonnegative().optional(),
786
+ expected_but_missed: z2.number().int().nonnegative().optional(),
787
+ total_turns: z2.number().int().nonnegative().optional()
788
+ })
789
+ ).optional(),
790
+ dismissed_reason_histogram: z2.record(z2.string(), z2.number().int().nonnegative()).optional(),
791
+ none_reason_histogram: z2.record(z2.string(), z2.number().int().nonnegative()).optional(),
792
+ // v2.0.0-rc.24 TASK-08: contract-policy audit metrics. Status discriminates
793
+ // populated vs degraded modes. contract_metrics + per_layer_type are emitted
794
+ // (zeroed) in degraded modes so the renderer iterates one stable shape.
795
+ contract_metrics_status: z2.enum(["ok", "skipped:bootstrap_drift", "awaiting_marker"]).optional(),
796
+ contract_metrics: citeContractMetricsSchema.optional(),
797
+ per_layer_type: citeLayerTypeBreakdownSchema.optional(),
798
+ contract_marker_ts: z2.number().int().nonnegative().optional(),
799
+ generated_at: z2.string()
800
+ });
801
+ var ledgerSourceSchema = z2.enum(["ai", "human"]);
802
+ var timestampFilterSchema = z2.preprocess((value) => {
803
+ if (value === void 0 || value === null || value === "") {
804
+ return void 0;
805
+ }
806
+ if (typeof value === "number") {
807
+ return value;
808
+ }
809
+ if (typeof value === "string") {
810
+ const trimmed = value.trim();
811
+ if (trimmed.length === 0) {
812
+ return void 0;
813
+ }
814
+ if (/^\d+$/.test(trimmed)) {
815
+ return Number.parseInt(trimmed, 10);
816
+ }
817
+ const parsed = Date.parse(trimmed);
818
+ return Number.isNaN(parsed) ? value : parsed;
819
+ }
820
+ return value;
821
+ }, z2.number().int().nonnegative());
822
+ var ledgerQuerySchema = z2.object({
823
+ source: ledgerSourceSchema.optional(),
824
+ since: timestampFilterSchema.optional()
825
+ });
826
+ var historyStateQuerySchema = z2.object({
827
+ ledger_id: z2.string().trim().min(1).optional(),
828
+ ts: timestampFilterSchema.optional()
829
+ }).superRefine((value, ctx) => {
830
+ const provided = [value.ledger_id, value.ts].filter((entry) => entry !== void 0);
831
+ if (provided.length !== 1) {
832
+ ctx.addIssue({
833
+ code: z2.ZodIssueCode.custom,
834
+ message: "Provide exactly one of ledger_id or ts.",
835
+ path: ["ledger_id"]
836
+ });
837
+ }
838
+ });
839
+ var humanLockApproveRequestSchema = z2.object({
840
+ file: z2.string().min(1),
841
+ start_line: z2.number().int().positive(),
842
+ end_line: z2.number().int().positive(),
843
+ new_hash: z2.string().min(1)
844
+ });
845
+ var humanLockFileParamsSchema = z2.object({
846
+ file: z2.string().min(1)
847
+ });
848
+ var annotateIntentRequestSchema = z2.object({
849
+ ledger_entry_id: z2.string().min(1),
850
+ annotation: z2.string().trim().min(1)
851
+ });
852
+ var KnowledgeTypeSchema = z2.enum([
853
+ "models",
854
+ // entities, data structures, relationships
855
+ "decisions",
856
+ // architectural/technical choices with rationale
857
+ "guidelines",
858
+ // recommended practices (recommend) or anti-patterns (avoid)
859
+ "pitfalls",
860
+ // known risks, failure modes, troubleshooting
861
+ "processes"
862
+ // workflows, state machines, operational steps
863
+ ]);
864
+ var MaturitySchema = z2.enum(["draft", "verified", "proven"]);
865
+ var LayerSchema = z2.enum(["personal", "team"]);
866
+ var StableIdSchema = z2.string().regex(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/);
867
+ var KnowledgeEntryFrontmatterSchema = z2.object({
868
+ id: StableIdSchema,
869
+ // e.g., "KT-DEC-0042"
870
+ type: KnowledgeTypeSchema,
871
+ // one of 5 types
872
+ maturity: MaturitySchema,
873
+ // draft | verified | proven
874
+ layer: LayerSchema,
875
+ // personal | team
876
+ layer_reason: z2.string().optional(),
877
+ // why this layer (for ambiguous cases)
878
+ created_at: z2.string()
879
+ // ISO 8601 timestamp
880
+ // Note: 'tags' and other fields can be added later but core schema is these 6
881
+ });
882
+ var KNOWLEDGE_TYPE_CODES = {
883
+ models: "MOD",
884
+ decisions: "DEC",
885
+ guidelines: "GLD",
886
+ pitfalls: "PIT",
887
+ processes: "PRO"
888
+ };
889
+ function formatKnowledgeId(layer, type, counter) {
890
+ const layerPrefix = layer === "personal" ? "KP" : "KT";
891
+ const typeCode = KNOWLEDGE_TYPE_CODES[type];
892
+ return `${layerPrefix}-${typeCode}-${String(counter).padStart(4, "0")}`;
893
+ }
894
+ function parseKnowledgeId(id) {
895
+ const match = id.match(/^(KP|KT)-(MOD|DEC|GLD|PIT|PRO)-(\d+)$/);
896
+ if (!match) return null;
897
+ const layer = match[1] === "KP" ? "personal" : "team";
898
+ const typeCode = match[2];
899
+ const entry = Object.entries(KNOWLEDGE_TYPE_CODES).find(([, code]) => code === typeCode);
900
+ if (!entry) return null;
901
+ const type = entry[0];
902
+ return { layer, type, counter: parseInt(match[3], 10) };
903
+ }
904
+
905
+ export {
906
+ ONBOARD_SLOT_NAMES,
907
+ onboardSlotSchema,
908
+ ONBOARD_SLOT_TOTAL,
909
+ structuredWarningSchema,
910
+ planContextInputSchema,
911
+ planContextOutputSchema,
912
+ planContextAnnotations,
913
+ planContextHintNarrowEntrySchema,
914
+ planContextHintOutputSchema,
915
+ knowledgeSectionsInputSchema,
916
+ knowledgeSectionsOutputSchema,
917
+ knowledgeSectionsAnnotations,
918
+ recallInputSchema,
919
+ recallOutputSchema,
920
+ recallAnnotations,
921
+ archiveScanInputSchema,
922
+ archiveScanOutputSchema,
923
+ archiveScanAnnotations,
924
+ ProposedReasonSchema,
925
+ PROPOSED_REASON_DESCRIPTIONS,
926
+ FabExtractKnowledgeInputSchema,
927
+ FabExtractKnowledgeInputShape,
928
+ FabExtractKnowledgeOutputSchema,
929
+ fabExtractKnowledgeAnnotations,
930
+ FabReviewInputSchema,
931
+ FabReviewInputShape,
932
+ FabReviewOutputSchema,
933
+ FabReviewOutputShape,
934
+ fabReviewAnnotations,
935
+ citeContractMetricsSchema,
936
+ citeLayerTypeBreakdownSchema,
937
+ citeCoverageReportSchema,
938
+ ledgerSourceSchema,
939
+ ledgerQuerySchema,
940
+ historyStateQuerySchema,
941
+ humanLockApproveRequestSchema,
942
+ humanLockFileParamsSchema,
943
+ annotateIntentRequestSchema,
944
+ KnowledgeTypeSchema,
945
+ MaturitySchema,
946
+ LayerSchema,
947
+ StableIdSchema,
948
+ KnowledgeEntryFrontmatterSchema,
949
+ KNOWLEDGE_TYPE_CODES,
950
+ formatKnowledgeId,
951
+ parseKnowledgeId
952
+ };