@theglitchking/hit-em-with-the-docs 2.1.1 → 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 +810 -49
- 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 +100 -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 +806 -45
- package/dist/core/metadata/schema.d.ts.map +1 -1
- package/dist/core/metadata/schema.js +472 -42
- 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":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,uBAAuB,EACvB,sBAAsB,EACtB,KAAK,IAAI,EACV,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAmC9E,QAAA,MAAM,iBAAiB,wDAAyD,CAAC;AAwDjF,QAAA,MAAM,sBAAsB,kDAAmD,CAAC;AAChF,QAAA,MAAM,wBAAwB,gDAAiD,CAAC;AAChF,QAAA,MAAM,0BAA0B,qDAAsD,CAAC;AAEvF,QAAA,MAAM,aAAa,QAA+B,CAAC;AACnD,QAAA,MAAM,cAAc,QAAiD,CAAC;AAyItE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCvB,CAAC;AAEL,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAA8B,CAAC;AACjE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE5E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,4EAOlB,CAAC;AAEX;;;;;GAKG;AACH,QAAA,MAAM,wBAAwB,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC,CAUtE,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,OAAO,CAAC,gBAAgB,CAI9D,CAAC;AAEF,eAAO,MAAM,qBAAqB,+EAKxB,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mTA4BtB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,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;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAwBD,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,iBAAiB,EAAE,CAyLrB;AAED,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;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,EAAE,CAwBV;AAED,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,CAU5C;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAMR;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,GACnB,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,gBAAgB,GAAG,MAAM,CAiB3E;AAGD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,aAAa,EACb,cAAc,EACd,wBAAwB,GACzB,CAAC"}
|
|
@@ -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,52 +31,230 @@ 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
|
-
|
|
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'));
|
|
16
49
|
const statusSchema = z.object({
|
|
17
|
-
status: z.
|
|
18
|
-
last_updated:
|
|
19
|
-
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Must be semver format (X.Y.Z)'),
|
|
50
|
+
status: z.string().min(1, 'status is required'),
|
|
51
|
+
last_updated: dateStringSchema,
|
|
52
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Must be semver format (X.Y.Z)').optional(),
|
|
20
53
|
});
|
|
21
|
-
// Discovery & Navigation Fields (Optional)
|
|
22
54
|
const discoverySchema = z.object({
|
|
23
55
|
purpose: z.string().optional(),
|
|
24
56
|
related_docs: z.array(z.string()).optional(),
|
|
25
57
|
load_priority: z.number().min(1).max(10).optional(),
|
|
26
58
|
});
|
|
27
|
-
// Ownership & Maintenance Fields (Optional)
|
|
28
59
|
const ownershipSchema = z.object({
|
|
29
60
|
author: z.string().optional(),
|
|
30
61
|
maintainer: z.string().optional(),
|
|
31
62
|
review_frequency: z.enum(['weekly', 'monthly', 'quarterly', 'annually']).optional(),
|
|
32
63
|
});
|
|
33
|
-
// Implementation Fields (Optional)
|
|
34
64
|
const implementationSchema = z.object({
|
|
35
65
|
implementation_status: z.enum(['planned', 'in_progress', 'complete', 'deprecated']).optional(),
|
|
36
66
|
tested: z.boolean().optional(),
|
|
37
67
|
production_ready: z.boolean().optional(),
|
|
38
68
|
});
|
|
39
|
-
// Auto-generated Fields
|
|
40
69
|
const autoGeneratedSchema = z.object({
|
|
41
70
|
estimated_read_time: z.string().optional(),
|
|
42
71
|
word_count: z.number().optional(),
|
|
43
|
-
last_validated:
|
|
72
|
+
last_validated: dateStringSchema.optional(),
|
|
44
73
|
backlinks: z.array(z.string()).optional(),
|
|
45
74
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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) -----
|
|
117
|
+
const _baseMergedSchema = coreIdentitySchema
|
|
50
118
|
.merge(statusSchema)
|
|
51
119
|
.merge(discoverySchema)
|
|
52
120
|
.merge(ownershipSchema)
|
|
53
121
|
.merge(implementationSchema)
|
|
54
|
-
.merge(autoGeneratedSchema)
|
|
122
|
+
.merge(autoGeneratedSchema)
|
|
123
|
+
.merge(knowledgeBaseSchema);
|
|
124
|
+
// ----- Conditional refinements -----
|
|
55
125
|
/**
|
|
56
|
-
*
|
|
126
|
+
* `version` is required for any tier NOT in LIFECYCLE_TRACKED_TIERS.
|
|
57
127
|
*/
|
|
58
|
-
|
|
128
|
+
const refineVersionRequired = (data) => isLifecycleTrackedTier(data.tier) || data.version !== undefined;
|
|
59
129
|
/**
|
|
60
|
-
*
|
|
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.
|
|
203
|
+
*
|
|
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)
|
|
214
|
+
*/
|
|
215
|
+
export const MetadataSchema = _baseMergedSchema
|
|
216
|
+
.refine(refineVersionRequired, {
|
|
217
|
+
message: 'version is required unless tier is plan, fact, incident-narrative, or incident-facts',
|
|
218
|
+
path: ['version'],
|
|
219
|
+
})
|
|
220
|
+
.refine(refineStatusEnum, {
|
|
221
|
+
message: `status must be one of ${DOC_STATUS_VALUES.join('|')} for non-lifecycle-tracked tiers`,
|
|
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'],
|
|
239
|
+
});
|
|
240
|
+
/**
|
|
241
|
+
* Partial schema for documents that may have incomplete metadata.
|
|
242
|
+
* Built from the un-refined base so every field is optional regardless of
|
|
243
|
+
* tier — used by `hewtd fix` to identify incomplete docs.
|
|
244
|
+
*/
|
|
245
|
+
export const PartialMetadataSchema = _baseMergedSchema.partial();
|
|
246
|
+
/**
|
|
247
|
+
* Required fields shared across all tiers.
|
|
248
|
+
*
|
|
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()`.
|
|
255
|
+
*
|
|
256
|
+
* Prefer `getMissingRequiredFields(data)` for tier-aware checks rather than
|
|
257
|
+
* iterating REQUIRED_FIELDS directly.
|
|
61
258
|
*/
|
|
62
259
|
export const REQUIRED_FIELDS = [
|
|
63
260
|
'title',
|
|
@@ -68,16 +265,27 @@ export const REQUIRED_FIELDS = [
|
|
|
68
265
|
'version',
|
|
69
266
|
];
|
|
70
267
|
/**
|
|
71
|
-
*
|
|
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.
|
|
72
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
|
+
};
|
|
73
284
|
export const OPTIONAL_FIELDS_DEFAULTS = {
|
|
74
285
|
audience: ['all'],
|
|
75
286
|
tags: [],
|
|
76
287
|
backlinks: [],
|
|
77
288
|
};
|
|
78
|
-
/**
|
|
79
|
-
* Auto-generated fields that should not be manually edited
|
|
80
|
-
*/
|
|
81
289
|
export const AUTO_GENERATED_FIELDS = [
|
|
82
290
|
'estimated_read_time',
|
|
83
291
|
'word_count',
|
|
@@ -85,7 +293,9 @@ export const AUTO_GENERATED_FIELDS = [
|
|
|
85
293
|
'backlinks',
|
|
86
294
|
];
|
|
87
295
|
/**
|
|
88
|
-
* 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.
|
|
89
299
|
*/
|
|
90
300
|
export const ALL_METADATA_FIELDS = [
|
|
91
301
|
// Core Identity (5)
|
|
@@ -116,9 +326,6 @@ export const ALL_METADATA_FIELDS = [
|
|
|
116
326
|
'last_validated',
|
|
117
327
|
'backlinks',
|
|
118
328
|
];
|
|
119
|
-
/**
|
|
120
|
-
* Validate metadata against the schema
|
|
121
|
-
*/
|
|
122
329
|
export function validateMetadata(data) {
|
|
123
330
|
const result = MetadataSchema.safeParse(data);
|
|
124
331
|
if (result.success) {
|
|
@@ -128,8 +335,212 @@ export function validateMetadata(data) {
|
|
|
128
335
|
return { valid: false, errors };
|
|
129
336
|
}
|
|
130
337
|
/**
|
|
131
|
-
*
|
|
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.
|
|
132
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.
|
|
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
|
+
}
|
|
133
544
|
export function validatePartialMetadata(data) {
|
|
134
545
|
const result = PartialMetadataSchema.safeParse(data);
|
|
135
546
|
if (result.success) {
|
|
@@ -139,30 +550,47 @@ export function validatePartialMetadata(data) {
|
|
|
139
550
|
return { valid: false, errors };
|
|
140
551
|
}
|
|
141
552
|
/**
|
|
142
|
-
* Get missing required fields
|
|
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.
|
|
143
559
|
*/
|
|
144
560
|
export function getMissingRequiredFields(data) {
|
|
145
|
-
|
|
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)
|
|
568
|
+
return false;
|
|
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;
|
|
582
|
+
});
|
|
146
583
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Get all missing fields (both required and optional)
|
|
149
|
-
*/
|
|
150
584
|
export function getMissingFields(data) {
|
|
151
|
-
const required =
|
|
585
|
+
const required = getMissingRequiredFields(data);
|
|
152
586
|
const optional = ALL_METADATA_FIELDS.filter((field) => !REQUIRED_FIELDS.includes(field) &&
|
|
153
587
|
!(field in data));
|
|
154
588
|
return { required, optional };
|
|
155
589
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Calculate metadata completeness percentage
|
|
158
|
-
*/
|
|
159
590
|
export function calculateMetadataCompleteness(data) {
|
|
160
591
|
const presentFields = ALL_METADATA_FIELDS.filter((field) => field in data && data[field] !== undefined && data[field] !== null);
|
|
161
592
|
return (presentFields.length / ALL_METADATA_FIELDS.length) * 100;
|
|
162
593
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Get field category
|
|
165
|
-
*/
|
|
166
594
|
export function getFieldCategory(field) {
|
|
167
595
|
if (['title', 'tier', 'domains', 'audience', 'tags'].includes(field)) {
|
|
168
596
|
return 'core';
|
|
@@ -181,4 +609,6 @@ export function getFieldCategory(field) {
|
|
|
181
609
|
}
|
|
182
610
|
return 'auto';
|
|
183
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, };
|
|
184
614
|
//# sourceMappingURL=schema.js.map
|