@theglitchking/hit-em-with-the-docs 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +7 -0
  4. package/commands/audit-facts.md +25 -0
  5. package/commands/cite.md +17 -0
  6. package/commands/extract-facts.md +66 -0
  7. package/commands/find-citers.md +16 -0
  8. package/commands/help.md +86 -0
  9. package/commands/migrate-incident.md +18 -0
  10. package/dist/action/core/audit/auditor.d.ts +14 -0
  11. package/dist/action/core/audit/auditor.d.ts.map +1 -1
  12. package/dist/action/core/domains/classifier.d.ts +15 -1
  13. package/dist/action/core/domains/classifier.d.ts.map +1 -1
  14. package/dist/action/core/knowledge-base/audit.d.ts +67 -0
  15. package/dist/action/core/knowledge-base/audit.d.ts.map +1 -0
  16. package/dist/action/core/knowledge-base/cite.d.ts +41 -0
  17. package/dist/action/core/knowledge-base/cite.d.ts.map +1 -0
  18. package/dist/action/core/knowledge-base/citers.d.ts +119 -0
  19. package/dist/action/core/knowledge-base/citers.d.ts.map +1 -0
  20. package/dist/action/core/knowledge-base/extract.d.ts +61 -0
  21. package/dist/action/core/knowledge-base/extract.d.ts.map +1 -0
  22. package/dist/action/core/knowledge-base/fix-legacy-layout.d.ts +61 -0
  23. package/dist/action/core/knowledge-base/fix-legacy-layout.d.ts.map +1 -0
  24. package/dist/action/core/knowledge-base/migrate.d.ts +67 -0
  25. package/dist/action/core/knowledge-base/migrate.d.ts.map +1 -0
  26. package/dist/action/core/maintain/orchestrator.d.ts.map +1 -1
  27. package/dist/action/core/metadata/errors.d.ts +62 -0
  28. package/dist/action/core/metadata/errors.d.ts.map +1 -0
  29. package/dist/action/core/metadata/schema.d.ts +709 -58
  30. package/dist/action/core/metadata/schema.d.ts.map +1 -1
  31. package/dist/action/generators/facts-index.d.ts +27 -0
  32. package/dist/action/generators/facts-index.d.ts.map +1 -0
  33. package/dist/action/generators/incidents-index.d.ts +21 -0
  34. package/dist/action/generators/incidents-index.d.ts.map +1 -0
  35. package/dist/action/generators/symptoms-index.d.ts +20 -0
  36. package/dist/action/generators/symptoms-index.d.ts.map +1 -0
  37. package/dist/action/generators/table.d.ts +8 -0
  38. package/dist/action/generators/table.d.ts.map +1 -0
  39. package/dist/action/index.d.ts +7 -0
  40. package/dist/action/index.d.ts.map +1 -1
  41. package/dist/action/index.js +4 -4
  42. package/dist/action/utils/config.d.ts +99 -0
  43. package/dist/action/utils/config.d.ts.map +1 -0
  44. package/dist/cli/index.js +315 -1
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/core/audit/auditor.d.ts +14 -0
  47. package/dist/core/audit/auditor.d.ts.map +1 -1
  48. package/dist/core/audit/auditor.js +19 -3
  49. package/dist/core/audit/auditor.js.map +1 -1
  50. package/dist/core/domains/classifier.d.ts +15 -1
  51. package/dist/core/domains/classifier.d.ts.map +1 -1
  52. package/dist/core/domains/classifier.js +83 -1
  53. package/dist/core/domains/classifier.js.map +1 -1
  54. package/dist/core/knowledge-base/audit.d.ts +67 -0
  55. package/dist/core/knowledge-base/audit.d.ts.map +1 -0
  56. package/dist/core/knowledge-base/audit.js +158 -0
  57. package/dist/core/knowledge-base/audit.js.map +1 -0
  58. package/dist/core/knowledge-base/cite.d.ts +41 -0
  59. package/dist/core/knowledge-base/cite.d.ts.map +1 -0
  60. package/dist/core/knowledge-base/cite.js +82 -0
  61. package/dist/core/knowledge-base/cite.js.map +1 -0
  62. package/dist/core/knowledge-base/citers.d.ts +119 -0
  63. package/dist/core/knowledge-base/citers.d.ts.map +1 -0
  64. package/dist/core/knowledge-base/citers.js +249 -0
  65. package/dist/core/knowledge-base/citers.js.map +1 -0
  66. package/dist/core/knowledge-base/extract.d.ts +61 -0
  67. package/dist/core/knowledge-base/extract.d.ts.map +1 -0
  68. package/dist/core/knowledge-base/extract.js +132 -0
  69. package/dist/core/knowledge-base/extract.js.map +1 -0
  70. package/dist/core/knowledge-base/fix-legacy-layout.d.ts +61 -0
  71. package/dist/core/knowledge-base/fix-legacy-layout.d.ts.map +1 -0
  72. package/dist/core/knowledge-base/fix-legacy-layout.js +189 -0
  73. package/dist/core/knowledge-base/fix-legacy-layout.js.map +1 -0
  74. package/dist/core/knowledge-base/migrate.d.ts +67 -0
  75. package/dist/core/knowledge-base/migrate.d.ts.map +1 -0
  76. package/dist/core/knowledge-base/migrate.js +147 -0
  77. package/dist/core/knowledge-base/migrate.js.map +1 -0
  78. package/dist/core/maintain/orchestrator.d.ts.map +1 -1
  79. package/dist/core/maintain/orchestrator.js +48 -1
  80. package/dist/core/maintain/orchestrator.js.map +1 -1
  81. package/dist/core/metadata/errors.d.ts +62 -0
  82. package/dist/core/metadata/errors.d.ts.map +1 -0
  83. package/dist/core/metadata/errors.js +95 -0
  84. package/dist/core/metadata/errors.js.map +1 -0
  85. package/dist/core/metadata/schema.d.ts +709 -58
  86. package/dist/core/metadata/schema.d.ts.map +1 -1
  87. package/dist/core/metadata/schema.js +453 -79
  88. package/dist/core/metadata/schema.js.map +1 -1
  89. package/dist/generators/facts-index.d.ts +27 -0
  90. package/dist/generators/facts-index.d.ts.map +1 -0
  91. package/dist/generators/facts-index.js +111 -0
  92. package/dist/generators/facts-index.js.map +1 -0
  93. package/dist/generators/incidents-index.d.ts +21 -0
  94. package/dist/generators/incidents-index.d.ts.map +1 -0
  95. package/dist/generators/incidents-index.js +79 -0
  96. package/dist/generators/incidents-index.js.map +1 -0
  97. package/dist/generators/symptoms-index.d.ts +20 -0
  98. package/dist/generators/symptoms-index.d.ts.map +1 -0
  99. package/dist/generators/symptoms-index.js +123 -0
  100. package/dist/generators/symptoms-index.js.map +1 -0
  101. package/dist/generators/table.d.ts +8 -0
  102. package/dist/generators/table.d.ts.map +1 -0
  103. package/dist/generators/table.js +23 -0
  104. package/dist/generators/table.js.map +1 -0
  105. package/dist/index.d.ts +7 -0
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +8 -0
  108. package/dist/index.js.map +1 -1
  109. package/dist/utils/config.d.ts +99 -0
  110. package/dist/utils/config.d.ts.map +1 -0
  111. package/dist/utils/config.js +95 -0
  112. package/dist/utils/config.js.map +1 -0
  113. package/package.json +1 -1
  114. package/templates/knowledge-base/README.md +31 -0
  115. package/templates/knowledge-base/fact.template.md +44 -0
  116. package/templates/knowledge-base/incident-facts.template.md +31 -0
  117. package/templates/knowledge-base/incident-narrative.template.md +50 -0
  118. package/templates/knowledge-base/playbook-symptoms.template.md +74 -0
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/core/metadata/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgFxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBxB,CAAC;AAEJ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAA8B,CAAC;AACjE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE5E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,4EAOlB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,wBAAwB,EAAE,OAAO,CAAC,gBAAgB,CAI9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,+EAKxB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,mBAAmB,mTA4BtB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAYA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG;IACtD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,uBAAuB,CAAC;CAChC,CAYA;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,EAAE,CAMV;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAY5C;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,GACnB,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,gBAAgB,GAAG,MAAM,CAiB3E"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/core/metadata/schema.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,29 @@
1
1
  import { z } from 'zod';
2
- import { TIERS } from '../domains/classifier.js';
2
+ import { TIERS, LIFECYCLE_TRACKED_TIERS, isLifecycleTrackedTier, } from '../domains/classifier.js';
3
+ import { KB_ERROR_CODES, formatKbError } from './errors.js';
3
4
  /**
4
- * 22-field metadata schema for documentation files.
5
- * This schema validates the YAML frontmatter of documentation files.
5
+ * Metadata schema for documentation files (the YAML frontmatter contract).
6
+ *
7
+ * Universal fields: 22 across six groups (core / status / discovery / ownership
8
+ * / implementation / auto). Plus tier-specific extensions for the lifecycle-
9
+ * tracked tiers introduced in 2.2.0 (plan) and 2.3.0 (fact, incident-narrative,
10
+ * incident-facts), and an optional `symptoms:` field for playbook documents.
11
+ *
12
+ * Conditional rules (enforced by .refine() on the merged MetadataSchema):
13
+ *
14
+ * 1. `version` is optional in the base schema; required for tiers that
15
+ * AREN'T in LIFECYCLE_TRACKED_TIERS. Lifecycle-tracked tiers (plan, fact,
16
+ * incident-narrative, incident-facts) use status/last_verified, not semver.
17
+ * 2. `status` accepts any string in the base schema; conditionally restricted
18
+ * to the doc-tier enum (draft|active|deprecated|archived) for tiers that
19
+ * aren't lifecycle-tracked. Each lifecycle-tracked tier uses its own
20
+ * status enum.
21
+ * 3. Each lifecycle-tracked tier has its own required-fields contract
22
+ * (e.g. fact requires id+confidence+last_verified+provenance).
23
+ * 4. The optional `symptoms:` field, when present on any tier, must be an
24
+ * array of valid symptom entries.
6
25
  */
7
- // Core Identity Fields (Required)
26
+ // ----- Core schema groups (existing 22-field surface) -----
8
27
  const coreIdentitySchema = z.object({
9
28
  title: z.string().min(1, 'Title is required'),
10
29
  tier: z.enum(TIERS),
@@ -12,100 +31,229 @@ const coreIdentitySchema = z.object({
12
31
  audience: z.array(z.enum(['all', 'developers', 'devops', 'admin'])).default(['all']),
13
32
  tags: z.array(z.string()).default([]),
14
33
  });
15
- // Status & Lifecycle Fields
16
- //
17
- // Two conditional rules apply for plan-tier docs (introduced in 2.2.0):
18
- //
19
- // 1. `version` is optional in the base schema; conditionally required for
20
- // tier !== 'plan' (plans use lifecycle status, not semver).
21
- // 2. `status` accepts any string in the base schema; conditionally
22
- // restricted to the doc-tier enum for tier !== 'plan'. Plans use
23
- // their own lifecycle enums (e.g. atom: ready|in_progress|done|blocked,
24
- // phase/task: draft|active|paused|done|archived) which would conflict
25
- // with the doc-tier enum.
26
- //
27
- // Both rules are enforced by .refine() on the merged MetadataSchema below.
28
34
  const DOC_STATUS_VALUES = ['draft', 'active', 'deprecated', 'archived'];
35
+ /**
36
+ * Date-shaped value preprocessor: js-yaml auto-converts unquoted `2026-05-14`
37
+ * style fields into Date objects. Real users writing markdown rarely quote
38
+ * their dates, so we accept both `Date` and string-formatted YYYY-MM-DD,
39
+ * normalizing to the latter. Added in 2.3.0 alongside the knowledge-base
40
+ * primitives; applied universally so existing tiers (with unquoted
41
+ * `last_updated:` fields) also benefit.
42
+ */
43
+ const dateStringSchema = z.preprocess((v) => {
44
+ if (v instanceof Date && !Number.isNaN(v.getTime())) {
45
+ return v.toISOString().slice(0, 10);
46
+ }
47
+ return v;
48
+ }, z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD format'));
29
49
  const statusSchema = z.object({
30
50
  status: z.string().min(1, 'status is required'),
31
- last_updated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD format'),
51
+ last_updated: dateStringSchema,
32
52
  version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Must be semver format (X.Y.Z)').optional(),
33
53
  });
34
- // Discovery & Navigation Fields (Optional)
35
54
  const discoverySchema = z.object({
36
55
  purpose: z.string().optional(),
37
56
  related_docs: z.array(z.string()).optional(),
38
57
  load_priority: z.number().min(1).max(10).optional(),
39
58
  });
40
- // Ownership & Maintenance Fields (Optional)
41
59
  const ownershipSchema = z.object({
42
60
  author: z.string().optional(),
43
61
  maintainer: z.string().optional(),
44
62
  review_frequency: z.enum(['weekly', 'monthly', 'quarterly', 'annually']).optional(),
45
63
  });
46
- // Implementation Fields (Optional)
47
64
  const implementationSchema = z.object({
48
65
  implementation_status: z.enum(['planned', 'in_progress', 'complete', 'deprecated']).optional(),
49
66
  tested: z.boolean().optional(),
50
67
  production_ready: z.boolean().optional(),
51
68
  });
52
- // Auto-generated Fields
53
69
  const autoGeneratedSchema = z.object({
54
70
  estimated_read_time: z.string().optional(),
55
71
  word_count: z.number().optional(),
56
- last_validated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
72
+ last_validated: dateStringSchema.optional(),
57
73
  backlinks: z.array(z.string()).optional(),
58
74
  });
59
- /**
60
- * Internal merged ZodObject — kept un-refined so .partial() and other
61
- * ZodObject methods remain available downstream. The exported MetadataSchema
62
- * wraps this with a .refine() that enforces the conditional `version`
63
- * requirement.
64
- */
75
+ // ----- Knowledge-base extension fields (2.3.0) -----
76
+ //
77
+ // Every field below is optional at the type level — the conditional
78
+ // .refine() rules on the merged MetadataSchema enforce per-tier requirements.
79
+ const FACT_CONFIDENCE_VALUES = ['high', 'medium', 'low', 'hypothesis'];
80
+ const INCIDENT_SEVERITY_VALUES = ['low', 'medium', 'high', 'critical'];
81
+ const INCIDENT_RESOLUTION_VALUES = ['resolved', 'partial', 'open', 'planned'];
82
+ const KEBAB_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
83
+ const INCIDENT_ID_RE = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
84
+ const knowledgeBaseSchema = z.object({
85
+ // Fact fields
86
+ id: z.string().min(1).optional(),
87
+ confidence: z.string().optional(),
88
+ last_verified: dateStringSchema.optional(),
89
+ verify_command: z.string().optional(),
90
+ provenance: z.array(z.string()).optional(),
91
+ sources: z.array(z.string()).optional(),
92
+ invalidated_by: z.array(z.string()).optional(),
93
+ working_dir: z.string().optional(),
94
+ // Incident-narrative fields
95
+ date: dateStringSchema.optional(),
96
+ severity: z.string().optional(),
97
+ resolution_status: z.string().optional(),
98
+ components: z.array(z.string()).optional(),
99
+ // Incident-facts fields
100
+ incident_id: z.string().optional(),
101
+ produced: z.array(z.string()).optional(),
102
+ strengthened: z.array(z.string()).optional(),
103
+ weakened: z.array(z.string()).optional(),
104
+ // Playbook symptoms (any tier may carry this)
105
+ symptoms: z
106
+ .array(z.object({
107
+ alert_name: z.string().optional(),
108
+ user_phrase: z.union([z.string(), z.array(z.string())]).optional(),
109
+ error_pattern: z.string().optional(),
110
+ severity: z.enum(INCIDENT_SEVERITY_VALUES).optional(),
111
+ target: z.string().min(1).optional(),
112
+ cites: z.array(z.string()).optional(),
113
+ }))
114
+ .optional(),
115
+ });
116
+ // ----- Merged base (no refinements yet) -----
65
117
  const _baseMergedSchema = coreIdentitySchema
66
118
  .merge(statusSchema)
67
119
  .merge(discoverySchema)
68
120
  .merge(ownershipSchema)
69
121
  .merge(implementationSchema)
70
- .merge(autoGeneratedSchema);
122
+ .merge(autoGeneratedSchema)
123
+ .merge(knowledgeBaseSchema);
124
+ // ----- Conditional refinements -----
71
125
  /**
72
- * Complete metadata schema (22 fields total).
73
- *
74
- * Required fields: title, tier, domains, status, last_updated.
75
- * Plus `version` — required for ALL tiers EXCEPT `tier: "plan"`.
126
+ * `version` is required for any tier NOT in LIFECYCLE_TRACKED_TIERS.
127
+ */
128
+ const refineVersionRequired = (data) => isLifecycleTrackedTier(data.tier) || data.version !== undefined;
129
+ /**
130
+ * `status` must be one of DOC_STATUS_VALUES for non-lifecycle-tracked tiers.
131
+ * Lifecycle-tracked tiers use their own status enums (free-form at this layer).
132
+ */
133
+ const refineStatusEnum = (data) => isLifecycleTrackedTier(data.tier) ||
134
+ DOC_STATUS_VALUES.includes(data.status);
135
+ /**
136
+ * Fact-tier required fields: id, confidence (enum), last_verified, provenance (≥1).
137
+ */
138
+ const refineFactRequirements = (data) => {
139
+ if (data.tier !== 'fact')
140
+ return true;
141
+ const checks = [
142
+ typeof data.id === 'string' && data.id.length > 0,
143
+ typeof data.confidence === 'string' &&
144
+ FACT_CONFIDENCE_VALUES.includes(data.confidence),
145
+ typeof data.last_verified === 'string',
146
+ Array.isArray(data.provenance) && data.provenance.length > 0,
147
+ ];
148
+ return checks.every(Boolean);
149
+ };
150
+ /**
151
+ * Incident-narrative required fields: id, date, severity (enum),
152
+ * resolution_status (enum), components (array).
153
+ */
154
+ const refineIncidentNarrativeRequirements = (data) => {
155
+ if (data.tier !== 'incident-narrative')
156
+ return true;
157
+ const checks = [
158
+ typeof data.id === 'string' && data.id.length > 0,
159
+ typeof data.date === 'string',
160
+ typeof data.severity === 'string' &&
161
+ INCIDENT_SEVERITY_VALUES.includes(data.severity),
162
+ typeof data.resolution_status === 'string' &&
163
+ INCIDENT_RESOLUTION_VALUES.includes(data.resolution_status),
164
+ Array.isArray(data.components),
165
+ ];
166
+ return checks.every(Boolean);
167
+ };
168
+ /**
169
+ * Incident-facts required fields: incident_id, produced (array, may be empty).
170
+ */
171
+ const refineIncidentFactsRequirements = (data) => {
172
+ if (data.tier !== 'incident-facts')
173
+ return true;
174
+ const checks = [
175
+ typeof data.incident_id === 'string' && data.incident_id.length > 0,
176
+ Array.isArray(data.produced),
177
+ ];
178
+ return checks.every(Boolean);
179
+ };
180
+ /**
181
+ * Symptoms entries (any tier): each must have one of alert_name/user_phrase/
182
+ * error_pattern AND target AND non-empty cites array.
183
+ */
184
+ const refineSymptomsEntries = (data) => {
185
+ if (!Array.isArray(data.symptoms))
186
+ return true;
187
+ for (const s of data.symptoms) {
188
+ const hasKey = s.alert_name !== undefined ||
189
+ s.user_phrase !== undefined ||
190
+ s.error_pattern !== undefined;
191
+ if (!hasKey)
192
+ return false;
193
+ if (typeof s.target !== 'string' || s.target.length === 0)
194
+ return false;
195
+ if (!Array.isArray(s.cites) || s.cites.length === 0)
196
+ return false;
197
+ }
198
+ return true;
199
+ };
200
+ // ----- Exported schema -----
201
+ /**
202
+ * Complete metadata schema with all conditional refinements applied.
76
203
  *
77
- * Plans (introduced in 2.2.0 for persistent-planning lg-mode interop) use
78
- * lifecycle status instead of semver. The .refine() below enforces:
79
- * - tier !== 'plan' version is required
80
- * - tier === 'plan' version is optional (may be absent or supplied)
204
+ * Required fields by tier:
205
+ * - Non-lifecycle-tracked (guide/standard/example/reference/admin):
206
+ * title, tier, domains, status (enum), last_updated, version
207
+ * - plan: title, tier, domains, status, last_updated (no version)
208
+ * - fact: title, tier, domains, status, last_updated, id, confidence,
209
+ * last_verified, provenance (no version)
210
+ * - incident-narrative: title, tier, domains, status, last_updated,
211
+ * id, date, severity, resolution_status, components (no version)
212
+ * - incident-facts: title, tier, domains, status, last_updated,
213
+ * incident_id, produced (no version)
81
214
  */
82
215
  export const MetadataSchema = _baseMergedSchema
83
- .refine((data) => data.tier === 'plan' || data.version !== undefined, {
84
- message: 'version is required when tier is not "plan"',
216
+ .refine(refineVersionRequired, {
217
+ message: 'version is required unless tier is plan, fact, incident-narrative, or incident-facts',
85
218
  path: ['version'],
86
219
  })
87
- .refine((data) => data.tier === 'plan' ||
88
- DOC_STATUS_VALUES.includes(data.status), {
89
- message: `status must be one of ${DOC_STATUS_VALUES.join('|')} when tier is not "plan"`,
220
+ .refine(refineStatusEnum, {
221
+ message: `status must be one of ${DOC_STATUS_VALUES.join('|')} for non-lifecycle-tracked tiers`,
90
222
  path: ['status'],
223
+ })
224
+ .refine(refineFactRequirements, {
225
+ message: formatKbError(KB_ERROR_CODES.FACT_MISSING_ID, 'fact tier requires id (kebab-case), confidence (high|medium|low|hypothesis), last_verified (YYYY-MM-DD), and provenance (≥1 entry)'),
226
+ path: ['fact'],
227
+ })
228
+ .refine(refineIncidentNarrativeRequirements, {
229
+ message: formatKbError(KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_DATE, 'incident-narrative tier requires id, date (YYYY-MM-DD), severity (low|medium|high|critical), resolution_status (resolved|partial|open|planned), and components (array)'),
230
+ path: ['incident-narrative'],
231
+ })
232
+ .refine(refineIncidentFactsRequirements, {
233
+ message: formatKbError(KB_ERROR_CODES.INCIDENT_FACTS_MISSING_INCIDENT_ID, 'incident-facts tier requires incident_id and produced (array, may be empty)'),
234
+ path: ['incident-facts'],
235
+ })
236
+ .refine(refineSymptomsEntries, {
237
+ message: formatKbError(KB_ERROR_CODES.PLAYBOOK_SYMPTOM_MISSING_KEY, 'each symptoms[] entry requires one of alert_name|user_phrase|error_pattern AND a target AND a non-empty cites[] array'),
238
+ path: ['symptoms'],
91
239
  });
92
240
  /**
93
241
  * Partial schema for documents that may have incomplete metadata.
94
- * Built from the un-refined base so all fields (including version) are
95
- * optional, regardless of tier — this is the schema used by `hewtd fix`
96
- * to identify incomplete docs.
242
+ * Built from the un-refined base so every field is optional regardless of
243
+ * tier — used by `hewtd fix` to identify incomplete docs.
97
244
  */
98
245
  export const PartialMetadataSchema = _baseMergedSchema.partial();
99
246
  /**
100
- * Required fields that must always be present.
247
+ * Required fields shared across all tiers.
101
248
  *
102
- * Note: `version` is conditionally required — it must be present for ALL
103
- * tiers EXCEPT `tier: "plan"`, where it may be absent (plans use lifecycle
104
- * status instead of semver). The list below preserves the historical
105
- * 6-required-field contract for non-plan docs; for plan-tier docs, the
106
- * effective required list is the first 5 entries (without `version`).
249
+ * Note: `version` is conditionally required — it must be present for all
250
+ * tiers NOT in LIFECYCLE_TRACKED_TIERS. The list below preserves the
251
+ * historical 6-required-field contract for non-lifecycle docs; for lifecycle-
252
+ * tracked docs (plan/fact/incident-narrative/incident-facts), `version` is
253
+ * dropped and the per-tier extension fields are added by
254
+ * `getRequiredFieldsForTier()`.
107
255
  *
108
- * Use `getMissingRequiredFields(data)` for tier-aware checks rather than
256
+ * Prefer `getMissingRequiredFields(data)` for tier-aware checks rather than
109
257
  * iterating REQUIRED_FIELDS directly.
110
258
  */
111
259
  export const REQUIRED_FIELDS = [
@@ -117,16 +265,27 @@ export const REQUIRED_FIELDS = [
117
265
  'version',
118
266
  ];
119
267
  /**
120
- * Optional fields with their default values
268
+ * Tier-specific required-field extensions for lifecycle-tracked tiers.
269
+ *
270
+ * For each tier, these fields are required IN ADDITION to the universal
271
+ * five (title, tier, domains, status, last_updated). `version` is excluded.
121
272
  */
273
+ const TIER_REQUIRED_EXTENSIONS = {
274
+ fact: ['id', 'confidence', 'last_verified', 'provenance'],
275
+ 'incident-narrative': [
276
+ 'id',
277
+ 'date',
278
+ 'severity',
279
+ 'resolution_status',
280
+ 'components',
281
+ ],
282
+ 'incident-facts': ['incident_id', 'produced'],
283
+ };
122
284
  export const OPTIONAL_FIELDS_DEFAULTS = {
123
285
  audience: ['all'],
124
286
  tags: [],
125
287
  backlinks: [],
126
288
  };
127
- /**
128
- * Auto-generated fields that should not be manually edited
129
- */
130
289
  export const AUTO_GENERATED_FIELDS = [
131
290
  'estimated_read_time',
132
291
  'word_count',
@@ -134,7 +293,9 @@ export const AUTO_GENERATED_FIELDS = [
134
293
  'backlinks',
135
294
  ];
136
295
  /**
137
- * All 22 field names
296
+ * All 22 universal field names (unchanged from 2.2.0). The 2.3.0 knowledge-
297
+ * base extension fields (id, confidence, etc.) are intentionally NOT in this
298
+ * list — they apply per-tier and would skew completeness calc on non-KB docs.
138
299
  */
139
300
  export const ALL_METADATA_FIELDS = [
140
301
  // Core Identity (5)
@@ -165,9 +326,6 @@ export const ALL_METADATA_FIELDS = [
165
326
  'last_validated',
166
327
  'backlinks',
167
328
  ];
168
- /**
169
- * Validate metadata against the schema
170
- */
171
329
  export function validateMetadata(data) {
172
330
  const result = MetadataSchema.safeParse(data);
173
331
  if (result.success) {
@@ -177,8 +335,212 @@ export function validateMetadata(data) {
177
335
  return { valid: false, errors };
178
336
  }
179
337
  /**
180
- * Validate partial metadata (for documents being fixed)
338
+ * Run all KB-specific validation predicates against a partial metadata object
339
+ * and return a typed error list. This is the entrypoint the audit layer uses
340
+ * for the knowledge-base primitives (fact / incident-narrative / incident-facts
341
+ * / symptoms).
342
+ *
343
+ * Unlike `validateMetadata()`, this does NOT require the base schema to pass
344
+ * first — it can run on partial / in-progress docs and surface ONLY the KB
345
+ * coded errors, which lets the auditor combine KB checks with its existing
346
+ * missing-required-fields reporting without duplicate output.
347
+ */
348
+ /**
349
+ * YAML dates (e.g. `date: 2026-05-14` unquoted) are auto-parsed by js-yaml
350
+ * into Date objects. We accept both `string` (YYYY-MM-DD) and `Date` as
351
+ * valid date-shaped values, so users don't have to quote dates manually.
181
352
  */
353
+ function isDateLike(value) {
354
+ if (typeof value === 'string')
355
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
356
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
357
+ return true;
358
+ return false;
359
+ }
360
+ export function validateKnowledgeBaseFields(data) {
361
+ const errors = [];
362
+ const tier = typeof data.tier === 'string' ? data.tier : undefined;
363
+ // ----- Fact tier -----
364
+ if (tier === 'fact') {
365
+ if (typeof data.id !== 'string' || data.id.length === 0) {
366
+ errors.push({
367
+ code: KB_ERROR_CODES.FACT_MISSING_ID,
368
+ message: 'fact tier requires an id (kebab-case slug)',
369
+ path: 'id',
370
+ });
371
+ }
372
+ else if (!KEBAB_SLUG_RE.test(data.id)) {
373
+ errors.push({
374
+ code: KB_ERROR_CODES.FACT_MISSING_ID,
375
+ message: `fact id must be kebab-case (got: ${data.id})`,
376
+ path: 'id',
377
+ });
378
+ }
379
+ if (typeof data.confidence !== 'string') {
380
+ errors.push({
381
+ code: KB_ERROR_CODES.FACT_INVALID_CONFIDENCE,
382
+ message: `fact tier requires confidence (one of ${FACT_CONFIDENCE_VALUES.join('|')})`,
383
+ path: 'confidence',
384
+ });
385
+ }
386
+ else if (!FACT_CONFIDENCE_VALUES.includes(data.confidence)) {
387
+ errors.push({
388
+ code: KB_ERROR_CODES.FACT_INVALID_CONFIDENCE,
389
+ message: `confidence must be one of ${FACT_CONFIDENCE_VALUES.join('|')} (got: ${data.confidence})`,
390
+ path: 'confidence',
391
+ });
392
+ }
393
+ if (data.last_verified === undefined || data.last_verified === null) {
394
+ errors.push({
395
+ code: KB_ERROR_CODES.FACT_MISSING_LAST_VERIFIED,
396
+ message: 'fact tier requires last_verified (YYYY-MM-DD)',
397
+ path: 'last_verified',
398
+ });
399
+ }
400
+ else if (!isDateLike(data.last_verified)) {
401
+ errors.push({
402
+ code: KB_ERROR_CODES.FACT_MISSING_LAST_VERIFIED,
403
+ message: `last_verified must be YYYY-MM-DD (got: ${String(data.last_verified)})`,
404
+ path: 'last_verified',
405
+ });
406
+ }
407
+ if (!Array.isArray(data.provenance) || data.provenance.length === 0) {
408
+ errors.push({
409
+ code: KB_ERROR_CODES.FACT_MISSING_PROVENANCE,
410
+ message: 'fact tier requires at least one provenance entry',
411
+ path: 'provenance',
412
+ });
413
+ }
414
+ // Warning: verify_command with TRULY multi-line content (multiple commands)
415
+ // but missing shebang. YAML's `|` block scalar leaves a trailing newline
416
+ // on single-line commands, so we trim first to avoid false positives.
417
+ if (typeof data.verify_command === 'string') {
418
+ const trimmed = data.verify_command.trim();
419
+ if (trimmed.includes('\n') && !trimmed.startsWith('#!')) {
420
+ errors.push({
421
+ code: KB_ERROR_CODES.FACT_VERIFY_COMMAND_MULTILINE_SHEBANG,
422
+ message: 'multi-line verify_command without a shebang line — fine, but consider adding `#!/usr/bin/env bash` for clarity',
423
+ path: 'verify_command',
424
+ });
425
+ }
426
+ }
427
+ }
428
+ // ----- Incident-narrative tier -----
429
+ if (tier === 'incident-narrative') {
430
+ if (typeof data.id !== 'string' || data.id.length === 0) {
431
+ errors.push({
432
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_DATE,
433
+ message: 'incident-narrative tier requires an id (YYYY-MM-DD-slug)',
434
+ path: 'id',
435
+ });
436
+ }
437
+ else if (!INCIDENT_ID_RE.test(data.id)) {
438
+ errors.push({
439
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_DATE,
440
+ message: `incident id must match YYYY-MM-DD-<slug> (got: ${data.id})`,
441
+ path: 'id',
442
+ });
443
+ }
444
+ if (data.date === undefined || data.date === null) {
445
+ errors.push({
446
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_DATE,
447
+ message: 'incident-narrative tier requires date (YYYY-MM-DD)',
448
+ path: 'date',
449
+ });
450
+ }
451
+ else if (!isDateLike(data.date)) {
452
+ errors.push({
453
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_DATE,
454
+ message: `date must be YYYY-MM-DD (got: ${String(data.date)})`,
455
+ path: 'date',
456
+ });
457
+ }
458
+ if (typeof data.severity !== 'string') {
459
+ errors.push({
460
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_SEVERITY,
461
+ message: `incident-narrative tier requires severity (${INCIDENT_SEVERITY_VALUES.join('|')})`,
462
+ path: 'severity',
463
+ });
464
+ }
465
+ else if (!INCIDENT_SEVERITY_VALUES.includes(data.severity)) {
466
+ errors.push({
467
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_INVALID_SEVERITY,
468
+ message: `severity must be one of ${INCIDENT_SEVERITY_VALUES.join('|')} (got: ${data.severity})`,
469
+ path: 'severity',
470
+ });
471
+ }
472
+ if (typeof data.resolution_status !== 'string') {
473
+ errors.push({
474
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_RESOLUTION_STATUS,
475
+ message: `incident-narrative tier requires resolution_status (${INCIDENT_RESOLUTION_VALUES.join('|')})`,
476
+ path: 'resolution_status',
477
+ });
478
+ }
479
+ else if (!INCIDENT_RESOLUTION_VALUES.includes(data.resolution_status)) {
480
+ errors.push({
481
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_INVALID_RESOLUTION_STATUS,
482
+ message: `resolution_status must be one of ${INCIDENT_RESOLUTION_VALUES.join('|')} (got: ${data.resolution_status})`,
483
+ path: 'resolution_status',
484
+ });
485
+ }
486
+ if (!Array.isArray(data.components) || data.components.length === 0) {
487
+ errors.push({
488
+ code: KB_ERROR_CODES.INCIDENT_NARRATIVE_MISSING_COMPONENTS,
489
+ message: 'incident-narrative tier requires at least one component',
490
+ path: 'components',
491
+ });
492
+ }
493
+ }
494
+ // ----- Incident-facts tier -----
495
+ if (tier === 'incident-facts') {
496
+ if (typeof data.incident_id !== 'string' || data.incident_id.length === 0) {
497
+ errors.push({
498
+ code: KB_ERROR_CODES.INCIDENT_FACTS_MISSING_INCIDENT_ID,
499
+ message: 'incident-facts tier requires incident_id',
500
+ path: 'incident_id',
501
+ });
502
+ }
503
+ if (!Array.isArray(data.produced)) {
504
+ errors.push({
505
+ code: KB_ERROR_CODES.INCIDENT_FACTS_MISSING_PRODUCED,
506
+ message: 'incident-facts tier requires produced (array, may be empty)',
507
+ path: 'produced',
508
+ });
509
+ }
510
+ }
511
+ // ----- Symptoms entries (any tier) -----
512
+ if (Array.isArray(data.symptoms)) {
513
+ for (let i = 0; i < data.symptoms.length; i++) {
514
+ const s = data.symptoms[i];
515
+ const hasKey = typeof s.alert_name === 'string' ||
516
+ typeof s.user_phrase === 'string' ||
517
+ Array.isArray(s.user_phrase) ||
518
+ typeof s.error_pattern === 'string';
519
+ if (!hasKey) {
520
+ errors.push({
521
+ code: KB_ERROR_CODES.PLAYBOOK_SYMPTOM_MISSING_KEY,
522
+ message: `symptoms[${i}] must have one of alert_name, user_phrase, or error_pattern`,
523
+ path: `symptoms[${i}]`,
524
+ });
525
+ }
526
+ if (typeof s.target !== 'string' || s.target.length === 0) {
527
+ errors.push({
528
+ code: KB_ERROR_CODES.PLAYBOOK_SYMPTOM_MISSING_TARGET,
529
+ message: `symptoms[${i}] requires a target (anchor or path)`,
530
+ path: `symptoms[${i}].target`,
531
+ });
532
+ }
533
+ if (!Array.isArray(s.cites) || s.cites.length === 0) {
534
+ errors.push({
535
+ code: KB_ERROR_CODES.PLAYBOOK_SYMPTOM_MISSING_CITES,
536
+ message: `symptoms[${i}] requires at least one cite (fact id)`,
537
+ path: `symptoms[${i}].cites`,
538
+ });
539
+ }
540
+ }
541
+ }
542
+ return errors;
543
+ }
182
544
  export function validatePartialMetadata(data) {
183
545
  const result = PartialMetadataSchema.safeParse(data);
184
546
  if (result.success) {
@@ -188,37 +550,47 @@ export function validatePartialMetadata(data) {
188
550
  return { valid: false, errors };
189
551
  }
190
552
  /**
191
- * Get missing required fields, respecting the tier-conditional `version`
192
- * exception. For `tier: "plan"`, missing `version` is NOT reported as
193
- * missing — plans use lifecycle status, not semver.
553
+ * Get missing required fields, respecting both the version-exemption for
554
+ * lifecycle-tracked tiers AND the tier-specific extension requirements.
555
+ *
556
+ * - For non-lifecycle tiers: REQUIRED_FIELDS (all six, including version).
557
+ * - For lifecycle-tracked tiers: REQUIRED_FIELDS minus 'version', plus
558
+ * any tier-specific extensions from TIER_REQUIRED_EXTENSIONS.
194
559
  */
195
560
  export function getMissingRequiredFields(data) {
196
- return REQUIRED_FIELDS.filter((field) => {
197
- // `version` is conditionally required — exempt for plan tier.
198
- if (field === 'version' && data.tier === 'plan')
561
+ const tier = typeof data.tier === 'string' ? data.tier : undefined;
562
+ const isLifecycle = isLifecycleTrackedTier(tier);
563
+ const extensions = tier
564
+ ? (TIER_REQUIRED_EXTENSIONS[tier] ?? [])
565
+ : [];
566
+ const baseRequired = REQUIRED_FIELDS.filter((field) => {
567
+ if (field === 'version' && isLifecycle)
199
568
  return false;
200
- return !(field in data) || data[field] === undefined || data[field] === null;
569
+ return true;
570
+ });
571
+ const allRequired = [...baseRequired, ...extensions];
572
+ return allRequired.filter((field) => {
573
+ const value = data[field];
574
+ if (value === undefined || value === null)
575
+ return true;
576
+ if (Array.isArray(value) && value.length === 0) {
577
+ // provenance and components are required to be non-empty arrays;
578
+ // produced (incident-facts) is allowed to be empty.
579
+ return field === 'provenance' || field === 'components';
580
+ }
581
+ return false;
201
582
  });
202
583
  }
203
- /**
204
- * Get all missing fields (both required and optional)
205
- */
206
584
  export function getMissingFields(data) {
207
- const required = REQUIRED_FIELDS.filter((field) => !(field in data) || data[field] === undefined);
585
+ const required = getMissingRequiredFields(data);
208
586
  const optional = ALL_METADATA_FIELDS.filter((field) => !REQUIRED_FIELDS.includes(field) &&
209
587
  !(field in data));
210
588
  return { required, optional };
211
589
  }
212
- /**
213
- * Calculate metadata completeness percentage
214
- */
215
590
  export function calculateMetadataCompleteness(data) {
216
591
  const presentFields = ALL_METADATA_FIELDS.filter((field) => field in data && data[field] !== undefined && data[field] !== null);
217
592
  return (presentFields.length / ALL_METADATA_FIELDS.length) * 100;
218
593
  }
219
- /**
220
- * Get field category
221
- */
222
594
  export function getFieldCategory(field) {
223
595
  if (['title', 'tier', 'domains', 'audience', 'tags'].includes(field)) {
224
596
  return 'core';
@@ -237,4 +609,6 @@ export function getFieldCategory(field) {
237
609
  }
238
610
  return 'auto';
239
611
  }
612
+ // Re-export tier-related symbols that consumers (and the auditor) need.
613
+ export { LIFECYCLE_TRACKED_TIERS, isLifecycleTrackedTier, DOC_STATUS_VALUES, FACT_CONFIDENCE_VALUES, INCIDENT_SEVERITY_VALUES, INCIDENT_RESOLUTION_VALUES, KEBAB_SLUG_RE, INCIDENT_ID_RE, TIER_REQUIRED_EXTENSIONS, };
240
614
  //# sourceMappingURL=schema.js.map