@theglitchking/hit-em-with-the-docs 2.2.0 → 2.3.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -0
- package/commands/audit-facts.md +25 -0
- package/commands/cite.md +17 -0
- package/commands/extract-facts.md +66 -0
- package/commands/find-citers.md +16 -0
- package/commands/help.md +86 -0
- package/commands/migrate-incident.md +18 -0
- package/dist/action/core/audit/auditor.d.ts +14 -0
- package/dist/action/core/audit/auditor.d.ts.map +1 -1
- package/dist/action/core/domains/classifier.d.ts +15 -1
- package/dist/action/core/domains/classifier.d.ts.map +1 -1
- package/dist/action/core/knowledge-base/audit.d.ts +67 -0
- package/dist/action/core/knowledge-base/audit.d.ts.map +1 -0
- package/dist/action/core/knowledge-base/cite.d.ts +41 -0
- package/dist/action/core/knowledge-base/cite.d.ts.map +1 -0
- package/dist/action/core/knowledge-base/citers.d.ts +119 -0
- package/dist/action/core/knowledge-base/citers.d.ts.map +1 -0
- package/dist/action/core/knowledge-base/extract.d.ts +61 -0
- package/dist/action/core/knowledge-base/extract.d.ts.map +1 -0
- package/dist/action/core/knowledge-base/migrate.d.ts +33 -0
- package/dist/action/core/knowledge-base/migrate.d.ts.map +1 -0
- package/dist/action/core/maintain/orchestrator.d.ts.map +1 -1
- package/dist/action/core/metadata/errors.d.ts +62 -0
- package/dist/action/core/metadata/errors.d.ts.map +1 -0
- package/dist/action/core/metadata/schema.d.ts +709 -58
- package/dist/action/core/metadata/schema.d.ts.map +1 -1
- package/dist/action/generators/facts-index.d.ts +27 -0
- package/dist/action/generators/facts-index.d.ts.map +1 -0
- package/dist/action/generators/incidents-index.d.ts +21 -0
- package/dist/action/generators/incidents-index.d.ts.map +1 -0
- package/dist/action/generators/symptoms-index.d.ts +20 -0
- package/dist/action/generators/symptoms-index.d.ts.map +1 -0
- package/dist/action/generators/table.d.ts +8 -0
- package/dist/action/generators/table.d.ts.map +1 -0
- package/dist/action/index.d.ts +7 -0
- package/dist/action/index.d.ts.map +1 -1
- package/dist/action/index.js +4 -4
- package/dist/action/utils/config.d.ts +99 -0
- package/dist/action/utils/config.d.ts.map +1 -0
- package/dist/cli/index.js +248 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit/auditor.d.ts +14 -0
- package/dist/core/audit/auditor.d.ts.map +1 -1
- package/dist/core/audit/auditor.js +19 -3
- package/dist/core/audit/auditor.js.map +1 -1
- package/dist/core/domains/classifier.d.ts +15 -1
- package/dist/core/domains/classifier.d.ts.map +1 -1
- package/dist/core/domains/classifier.js +83 -1
- package/dist/core/domains/classifier.js.map +1 -1
- package/dist/core/knowledge-base/audit.d.ts +67 -0
- package/dist/core/knowledge-base/audit.d.ts.map +1 -0
- package/dist/core/knowledge-base/audit.js +158 -0
- package/dist/core/knowledge-base/audit.js.map +1 -0
- package/dist/core/knowledge-base/cite.d.ts +41 -0
- package/dist/core/knowledge-base/cite.d.ts.map +1 -0
- package/dist/core/knowledge-base/cite.js +82 -0
- package/dist/core/knowledge-base/cite.js.map +1 -0
- package/dist/core/knowledge-base/citers.d.ts +119 -0
- package/dist/core/knowledge-base/citers.d.ts.map +1 -0
- package/dist/core/knowledge-base/citers.js +249 -0
- package/dist/core/knowledge-base/citers.js.map +1 -0
- package/dist/core/knowledge-base/extract.d.ts +61 -0
- package/dist/core/knowledge-base/extract.d.ts.map +1 -0
- package/dist/core/knowledge-base/extract.js +132 -0
- package/dist/core/knowledge-base/extract.js.map +1 -0
- package/dist/core/knowledge-base/migrate.d.ts +33 -0
- package/dist/core/knowledge-base/migrate.d.ts.map +1 -0
- package/dist/core/knowledge-base/migrate.js +112 -0
- package/dist/core/knowledge-base/migrate.js.map +1 -0
- package/dist/core/maintain/orchestrator.d.ts.map +1 -1
- package/dist/core/maintain/orchestrator.js +48 -1
- package/dist/core/maintain/orchestrator.js.map +1 -1
- package/dist/core/metadata/errors.d.ts +62 -0
- package/dist/core/metadata/errors.d.ts.map +1 -0
- package/dist/core/metadata/errors.js +95 -0
- package/dist/core/metadata/errors.js.map +1 -0
- package/dist/core/metadata/schema.d.ts +709 -58
- package/dist/core/metadata/schema.d.ts.map +1 -1
- package/dist/core/metadata/schema.js +453 -79
- package/dist/core/metadata/schema.js.map +1 -1
- package/dist/generators/facts-index.d.ts +27 -0
- package/dist/generators/facts-index.d.ts.map +1 -0
- package/dist/generators/facts-index.js +111 -0
- package/dist/generators/facts-index.js.map +1 -0
- package/dist/generators/incidents-index.d.ts +21 -0
- package/dist/generators/incidents-index.d.ts.map +1 -0
- package/dist/generators/incidents-index.js +79 -0
- package/dist/generators/incidents-index.js.map +1 -0
- package/dist/generators/symptoms-index.d.ts +20 -0
- package/dist/generators/symptoms-index.d.ts.map +1 -0
- package/dist/generators/symptoms-index.js +123 -0
- package/dist/generators/symptoms-index.js.map +1 -0
- package/dist/generators/table.d.ts +8 -0
- package/dist/generators/table.d.ts.map +1 -0
- package/dist/generators/table.js +23 -0
- package/dist/generators/table.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +99 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +95 -0
- package/dist/utils/config.js.map +1 -0
- package/package.json +1 -1
- package/templates/knowledge-base/README.md +31 -0
- package/templates/knowledge-base/fact.template.md +44 -0
- package/templates/knowledge-base/incident-facts.template.md +31 -0
- package/templates/knowledge-base/incident-narrative.template.md +50 -0
- 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;
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
72
|
+
last_validated: dateStringSchema.optional(),
|
|
57
73
|
backlinks: z.array(z.string()).optional(),
|
|
58
74
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
78
|
-
* lifecycle
|
|
79
|
-
*
|
|
80
|
-
* -
|
|
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(
|
|
84
|
-
message: 'version is required
|
|
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(
|
|
88
|
-
DOC_STATUS_VALUES.
|
|
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
|
|
95
|
-
*
|
|
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
|
|
247
|
+
* Required fields shared across all tiers.
|
|
101
248
|
*
|
|
102
|
-
* Note: `version` is conditionally required — it must be present for
|
|
103
|
-
* tiers
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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 =
|
|
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
|