@dogfood-lab/findings 1.2.2 → 1.2.3
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/advise/advice-bundle.js +155 -155
- package/advise/index.js +5 -5
- package/advise/query.js +182 -182
- package/derive/dedupe.js +107 -107
- package/derive/derive-findings.js +187 -187
- package/derive/ids.js +48 -48
- package/derive/index.js +9 -9
- package/derive/load-records.js +153 -153
- package/derive/rules.js +415 -415
- package/derive/write-findings.js +63 -63
- package/index.js +11 -11
- package/lib/file-lock.js +359 -359
- package/package.json +2 -2
- package/reader.js +156 -156
- package/review/index.js +6 -6
- package/review/transitions.js +79 -79
- package/synthesis/doctrine-derivation.js +128 -128
- package/synthesis/index.js +8 -8
- package/synthesis/pattern-derivation.js +184 -184
- package/synthesis/recommendation-derivation.js +156 -156
- package/synthesis/validate-artifacts.js +46 -46
- package/synthesis/write-artifacts.js +75 -75
- package/validate.js +87 -87
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recommendation derivation from accepted patterns.
|
|
3
|
-
*
|
|
4
|
-
* Generates actionable guidance using constrained templates based on
|
|
5
|
-
* pattern kind, dimensions, and transfer scope.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
9
|
-
import { resolve, join } from 'node:path';
|
|
10
|
-
import yaml from 'js-yaml';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Derive recommendations from accepted patterns.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} rootDir - dogfood-labs repo root
|
|
16
|
-
* @returns {{ recommendations: Array, stats: { patternsConsidered: number, recommendationsEmitted: number } }}
|
|
17
|
-
*/
|
|
18
|
-
export function deriveRecommendations(rootDir) {
|
|
19
|
-
const patterns = loadAcceptedPatterns(rootDir);
|
|
20
|
-
const recommendations = [];
|
|
21
|
-
|
|
22
|
-
for (const pat of patterns) {
|
|
23
|
-
const rec = deriveFromPattern(pat);
|
|
24
|
-
if (rec) recommendations.push(rec);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
recommendations,
|
|
29
|
-
stats: {
|
|
30
|
-
patternsConsidered: patterns.length,
|
|
31
|
-
recommendationsEmitted: recommendations.length
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Derive a recommendation from a single accepted pattern.
|
|
38
|
-
*/
|
|
39
|
-
function deriveFromPattern(pattern) {
|
|
40
|
-
const now = new Date().toISOString();
|
|
41
|
-
const template = selectTemplate(pattern);
|
|
42
|
-
if (!template) return null;
|
|
43
|
-
|
|
44
|
-
const surfaces = pattern.dimensions?.product_surfaces || [];
|
|
45
|
-
const slug = `${surfaces[0] || 'general'}-${template.kind}`.replace(/_/g, '-');
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
schema_version: '1.0.0',
|
|
49
|
-
recommendation_id: `drec-${slug}-${pattern.pattern_id.replace('dpat-', '')}`,
|
|
50
|
-
title: template.titleFn(pattern),
|
|
51
|
-
status: 'candidate',
|
|
52
|
-
recommendation_kind: template.kind,
|
|
53
|
-
summary: template.summaryFn(pattern),
|
|
54
|
-
applies_to: {
|
|
55
|
-
product_surfaces: surfaces,
|
|
56
|
-
...(pattern.support?.execution_modes?.length ? { execution_modes: pattern.support.execution_modes } : {}),
|
|
57
|
-
transfer_scope: pattern.transfer_scope
|
|
58
|
-
},
|
|
59
|
-
based_on_pattern_ids: [pattern.pattern_id],
|
|
60
|
-
action: {
|
|
61
|
-
type: template.actionType,
|
|
62
|
-
target: template.target,
|
|
63
|
-
details: template.detailsFn(pattern)
|
|
64
|
-
},
|
|
65
|
-
confidence: pattern.pattern_strength === 'strong' || pattern.pattern_strength === 'portfolio_stable' ? 'strong' : 'emerging',
|
|
66
|
-
created_at: now,
|
|
67
|
-
updated_at: now
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Select the right recommendation template for a pattern.
|
|
73
|
-
*/
|
|
74
|
-
function selectTemplate(pattern) {
|
|
75
|
-
const kind = pattern.pattern_kind;
|
|
76
|
-
const issueKinds = pattern.dimensions?.issue_kinds || [];
|
|
77
|
-
|
|
78
|
-
if (issueKinds.some(k => /interface|surface|entrypoint|build/.test(k))) {
|
|
79
|
-
return {
|
|
80
|
-
kind: 'starter_check',
|
|
81
|
-
actionType: 'add_check',
|
|
82
|
-
target: 'rollout',
|
|
83
|
-
titleFn: (p) => `Add ${issueKinds[0].replace(/_/g, ' ')} verification to starter rollout for ${fmtSurfaces(p)}`,
|
|
84
|
-
summaryFn: (p) => `New ${fmtSurfaces(p)} repos should verify ${issueKinds[0].replace(/_/g, ' ')} before rollout assumptions are encoded into scenarios or docs. This recurs across ${p.support.repo_count} repo(s).`,
|
|
85
|
-
detailsFn: (p) => `Verify ${issueKinds[0].replace(/_/g, ' ')} contract and invocation shape before scenario authoring for ${fmtSurfaces(p)} repos.`
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (kind === 'evidence_calibration') {
|
|
90
|
-
return {
|
|
91
|
-
kind: 'evidence_expectation',
|
|
92
|
-
actionType: 'set_evidence',
|
|
93
|
-
target: 'policy',
|
|
94
|
-
titleFn: (p) => `Calibrate evidence requirements for ${fmtSurfaces(p)} based on recurring miscalibration`,
|
|
95
|
-
summaryFn: (p) => `Evidence requirements for ${fmtSurfaces(p)} have been repeatedly miscalibrated. Default to natural output types rather than forced artifact shapes.`,
|
|
96
|
-
detailsFn: (p) => `Review and adjust evidence_requirements in surface policy to match natural outputs for ${fmtSurfaces(p)} repos.`
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (kind === 'verification_seam') {
|
|
101
|
-
return {
|
|
102
|
-
kind: 'verification_rule',
|
|
103
|
-
actionType: 'set_verification',
|
|
104
|
-
target: 'verification',
|
|
105
|
-
titleFn: (p) => `Add verification guard for ${issueKinds[0]?.replace(/_/g, ' ') || 'seam'} on ${fmtSurfaces(p)}`,
|
|
106
|
-
summaryFn: (p) => `A verification seam recurs on ${fmtSurfaces(p)}. Add a guard to prevent this class of verification failure.`,
|
|
107
|
-
detailsFn: (p) => `Add verification step to detect ${issueKinds[0]?.replace(/_/g, ' ') || 'verification gap'} before acceptance.`
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (kind === 'calibration_signal') {
|
|
112
|
-
return {
|
|
113
|
-
kind: 'policy_seed',
|
|
114
|
-
actionType: 'set_policy',
|
|
115
|
-
target: 'policy',
|
|
116
|
-
titleFn: (p) => `Seed ${fmtSurfaces(p)} policy with calibrated defaults from recurring pattern`,
|
|
117
|
-
summaryFn: (p) => `Policy miscalibration recurs on ${fmtSurfaces(p)}. Seed new repo policies with proven defaults.`,
|
|
118
|
-
detailsFn: (p) => `Apply calibrated policy defaults for ${fmtSurfaces(p)} repos based on ${p.support.finding_count} proven findings.`
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Default: starter_check
|
|
123
|
-
return {
|
|
124
|
-
kind: 'starter_check',
|
|
125
|
-
actionType: 'add_check',
|
|
126
|
-
target: 'rollout',
|
|
127
|
-
titleFn: (p) => `Add check for ${issueKinds[0]?.replace(/_/g, ' ') || 'recurring issue'} on ${fmtSurfaces(p)}`,
|
|
128
|
-
summaryFn: (p) => `A recurring issue pattern was detected on ${fmtSurfaces(p)}. Add a rollout check to prevent future occurrences.`,
|
|
129
|
-
detailsFn: (p) => `Add a rollout verification step for ${issueKinds[0]?.replace(/_/g, ' ') || 'this issue class'} on ${fmtSurfaces(p)} repos.`
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function fmtSurfaces(pattern) {
|
|
134
|
-
const s = pattern.dimensions?.product_surfaces || [];
|
|
135
|
-
return s.length ? s.join(', ') : 'general';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Load accepted patterns from disk.
|
|
140
|
-
*/
|
|
141
|
-
function loadAcceptedPatterns(rootDir) {
|
|
142
|
-
const dir = resolve(rootDir, 'patterns');
|
|
143
|
-
if (!existsSync(dir)) return [];
|
|
144
|
-
|
|
145
|
-
const patterns = [];
|
|
146
|
-
for (const file of readdirSync(dir)) {
|
|
147
|
-
if (!file.endsWith('.yaml')) continue;
|
|
148
|
-
try {
|
|
149
|
-
const data = yaml.load(readFileSync(join(dir, file), 'utf-8'));
|
|
150
|
-
if (data?.status === 'accepted') patterns.push(data);
|
|
151
|
-
} catch { /* skip */ }
|
|
152
|
-
}
|
|
153
|
-
return patterns;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export { loadAcceptedPatterns };
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation derivation from accepted patterns.
|
|
3
|
+
*
|
|
4
|
+
* Generates actionable guidance using constrained templates based on
|
|
5
|
+
* pattern kind, dimensions, and transfer scope.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
9
|
+
import { resolve, join } from 'node:path';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Derive recommendations from accepted patterns.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} rootDir - dogfood-labs repo root
|
|
16
|
+
* @returns {{ recommendations: Array, stats: { patternsConsidered: number, recommendationsEmitted: number } }}
|
|
17
|
+
*/
|
|
18
|
+
export function deriveRecommendations(rootDir) {
|
|
19
|
+
const patterns = loadAcceptedPatterns(rootDir);
|
|
20
|
+
const recommendations = [];
|
|
21
|
+
|
|
22
|
+
for (const pat of patterns) {
|
|
23
|
+
const rec = deriveFromPattern(pat);
|
|
24
|
+
if (rec) recommendations.push(rec);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
recommendations,
|
|
29
|
+
stats: {
|
|
30
|
+
patternsConsidered: patterns.length,
|
|
31
|
+
recommendationsEmitted: recommendations.length
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Derive a recommendation from a single accepted pattern.
|
|
38
|
+
*/
|
|
39
|
+
function deriveFromPattern(pattern) {
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
const template = selectTemplate(pattern);
|
|
42
|
+
if (!template) return null;
|
|
43
|
+
|
|
44
|
+
const surfaces = pattern.dimensions?.product_surfaces || [];
|
|
45
|
+
const slug = `${surfaces[0] || 'general'}-${template.kind}`.replace(/_/g, '-');
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
schema_version: '1.0.0',
|
|
49
|
+
recommendation_id: `drec-${slug}-${pattern.pattern_id.replace('dpat-', '')}`,
|
|
50
|
+
title: template.titleFn(pattern),
|
|
51
|
+
status: 'candidate',
|
|
52
|
+
recommendation_kind: template.kind,
|
|
53
|
+
summary: template.summaryFn(pattern),
|
|
54
|
+
applies_to: {
|
|
55
|
+
product_surfaces: surfaces,
|
|
56
|
+
...(pattern.support?.execution_modes?.length ? { execution_modes: pattern.support.execution_modes } : {}),
|
|
57
|
+
transfer_scope: pattern.transfer_scope
|
|
58
|
+
},
|
|
59
|
+
based_on_pattern_ids: [pattern.pattern_id],
|
|
60
|
+
action: {
|
|
61
|
+
type: template.actionType,
|
|
62
|
+
target: template.target,
|
|
63
|
+
details: template.detailsFn(pattern)
|
|
64
|
+
},
|
|
65
|
+
confidence: pattern.pattern_strength === 'strong' || pattern.pattern_strength === 'portfolio_stable' ? 'strong' : 'emerging',
|
|
66
|
+
created_at: now,
|
|
67
|
+
updated_at: now
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Select the right recommendation template for a pattern.
|
|
73
|
+
*/
|
|
74
|
+
function selectTemplate(pattern) {
|
|
75
|
+
const kind = pattern.pattern_kind;
|
|
76
|
+
const issueKinds = pattern.dimensions?.issue_kinds || [];
|
|
77
|
+
|
|
78
|
+
if (issueKinds.some(k => /interface|surface|entrypoint|build/.test(k))) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'starter_check',
|
|
81
|
+
actionType: 'add_check',
|
|
82
|
+
target: 'rollout',
|
|
83
|
+
titleFn: (p) => `Add ${issueKinds[0].replace(/_/g, ' ')} verification to starter rollout for ${fmtSurfaces(p)}`,
|
|
84
|
+
summaryFn: (p) => `New ${fmtSurfaces(p)} repos should verify ${issueKinds[0].replace(/_/g, ' ')} before rollout assumptions are encoded into scenarios or docs. This recurs across ${p.support.repo_count} repo(s).`,
|
|
85
|
+
detailsFn: (p) => `Verify ${issueKinds[0].replace(/_/g, ' ')} contract and invocation shape before scenario authoring for ${fmtSurfaces(p)} repos.`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (kind === 'evidence_calibration') {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'evidence_expectation',
|
|
92
|
+
actionType: 'set_evidence',
|
|
93
|
+
target: 'policy',
|
|
94
|
+
titleFn: (p) => `Calibrate evidence requirements for ${fmtSurfaces(p)} based on recurring miscalibration`,
|
|
95
|
+
summaryFn: (p) => `Evidence requirements for ${fmtSurfaces(p)} have been repeatedly miscalibrated. Default to natural output types rather than forced artifact shapes.`,
|
|
96
|
+
detailsFn: (p) => `Review and adjust evidence_requirements in surface policy to match natural outputs for ${fmtSurfaces(p)} repos.`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (kind === 'verification_seam') {
|
|
101
|
+
return {
|
|
102
|
+
kind: 'verification_rule',
|
|
103
|
+
actionType: 'set_verification',
|
|
104
|
+
target: 'verification',
|
|
105
|
+
titleFn: (p) => `Add verification guard for ${issueKinds[0]?.replace(/_/g, ' ') || 'seam'} on ${fmtSurfaces(p)}`,
|
|
106
|
+
summaryFn: (p) => `A verification seam recurs on ${fmtSurfaces(p)}. Add a guard to prevent this class of verification failure.`,
|
|
107
|
+
detailsFn: (p) => `Add verification step to detect ${issueKinds[0]?.replace(/_/g, ' ') || 'verification gap'} before acceptance.`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (kind === 'calibration_signal') {
|
|
112
|
+
return {
|
|
113
|
+
kind: 'policy_seed',
|
|
114
|
+
actionType: 'set_policy',
|
|
115
|
+
target: 'policy',
|
|
116
|
+
titleFn: (p) => `Seed ${fmtSurfaces(p)} policy with calibrated defaults from recurring pattern`,
|
|
117
|
+
summaryFn: (p) => `Policy miscalibration recurs on ${fmtSurfaces(p)}. Seed new repo policies with proven defaults.`,
|
|
118
|
+
detailsFn: (p) => `Apply calibrated policy defaults for ${fmtSurfaces(p)} repos based on ${p.support.finding_count} proven findings.`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Default: starter_check
|
|
123
|
+
return {
|
|
124
|
+
kind: 'starter_check',
|
|
125
|
+
actionType: 'add_check',
|
|
126
|
+
target: 'rollout',
|
|
127
|
+
titleFn: (p) => `Add check for ${issueKinds[0]?.replace(/_/g, ' ') || 'recurring issue'} on ${fmtSurfaces(p)}`,
|
|
128
|
+
summaryFn: (p) => `A recurring issue pattern was detected on ${fmtSurfaces(p)}. Add a rollout check to prevent future occurrences.`,
|
|
129
|
+
detailsFn: (p) => `Add a rollout verification step for ${issueKinds[0]?.replace(/_/g, ' ') || 'this issue class'} on ${fmtSurfaces(p)} repos.`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fmtSurfaces(pattern) {
|
|
134
|
+
const s = pattern.dimensions?.product_surfaces || [];
|
|
135
|
+
return s.length ? s.join(', ') : 'general';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Load accepted patterns from disk.
|
|
140
|
+
*/
|
|
141
|
+
function loadAcceptedPatterns(rootDir) {
|
|
142
|
+
const dir = resolve(rootDir, 'patterns');
|
|
143
|
+
if (!existsSync(dir)) return [];
|
|
144
|
+
|
|
145
|
+
const patterns = [];
|
|
146
|
+
for (const file of readdirSync(dir)) {
|
|
147
|
+
if (!file.endsWith('.yaml')) continue;
|
|
148
|
+
try {
|
|
149
|
+
const data = yaml.load(readFileSync(join(dir, file), 'utf-8'));
|
|
150
|
+
if (data?.status === 'accepted') patterns.push(data);
|
|
151
|
+
} catch { /* skip */ }
|
|
152
|
+
}
|
|
153
|
+
return patterns;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { loadAcceptedPatterns };
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Schema validation for pattern, recommendation, and doctrine artifacts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { dirname } from 'node:path';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
9
|
-
import addFormats from 'ajv-formats';
|
|
10
|
-
import yaml from 'js-yaml';
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
// Resolve the schemas package's json directory via its subpath export.
|
|
14
|
-
const SCHEMAS_DIR = dirname(
|
|
15
|
-
require.resolve('@dogfood-lab/schemas/json/dogfood-pattern.schema.json')
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const _validators = {};
|
|
19
|
-
|
|
20
|
-
function getValidator(schemaFile) {
|
|
21
|
-
if (!_validators[schemaFile]) {
|
|
22
|
-
const schema = JSON.parse(readFileSync(`${SCHEMAS_DIR}/${schemaFile}`, 'utf-8'));
|
|
23
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
24
|
-
addFormats(ajv);
|
|
25
|
-
_validators[schemaFile] = ajv.compile(schema);
|
|
26
|
-
}
|
|
27
|
-
return _validators[schemaFile];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function validatePattern(data) {
|
|
31
|
-
const validate = getValidator('dogfood-pattern.schema.json');
|
|
32
|
-
const valid = validate(data);
|
|
33
|
-
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function validateRecommendation(data) {
|
|
37
|
-
const validate = getValidator('dogfood-recommendation.schema.json');
|
|
38
|
-
const valid = validate(data);
|
|
39
|
-
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function validateDoctrine(data) {
|
|
43
|
-
const validate = getValidator('dogfood-doctrine.schema.json');
|
|
44
|
-
const valid = validate(data);
|
|
45
|
-
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
46
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Schema validation for pattern, recommendation, and doctrine artifacts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
9
|
+
import addFormats from 'ajv-formats';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
// Resolve the schemas package's json directory via its subpath export.
|
|
14
|
+
const SCHEMAS_DIR = dirname(
|
|
15
|
+
require.resolve('@dogfood-lab/schemas/json/dogfood-pattern.schema.json')
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const _validators = {};
|
|
19
|
+
|
|
20
|
+
function getValidator(schemaFile) {
|
|
21
|
+
if (!_validators[schemaFile]) {
|
|
22
|
+
const schema = JSON.parse(readFileSync(`${SCHEMAS_DIR}/${schemaFile}`, 'utf-8'));
|
|
23
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
24
|
+
addFormats(ajv);
|
|
25
|
+
_validators[schemaFile] = ajv.compile(schema);
|
|
26
|
+
}
|
|
27
|
+
return _validators[schemaFile];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validatePattern(data) {
|
|
31
|
+
const validate = getValidator('dogfood-pattern.schema.json');
|
|
32
|
+
const valid = validate(data);
|
|
33
|
+
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateRecommendation(data) {
|
|
37
|
+
const validate = getValidator('dogfood-recommendation.schema.json');
|
|
38
|
+
const valid = validate(data);
|
|
39
|
+
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateDoctrine(data) {
|
|
43
|
+
const validate = getValidator('dogfood-doctrine.schema.json');
|
|
44
|
+
const valid = validate(data);
|
|
45
|
+
return { valid, errors: valid ? [] : (validate.errors || []).map(e => ({ path: e.instancePath || '/', message: e.message })) };
|
|
46
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Write pattern, recommendation, and doctrine artifacts to disk.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { mkdirSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { resolve, join } from 'node:path';
|
|
7
|
-
import yaml from 'js-yaml';
|
|
8
|
-
|
|
9
|
-
import { atomicWriteFileSync } from '../lib/atomic-write.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Write a pattern to disk.
|
|
13
|
-
*/
|
|
14
|
-
export function writePattern(rootDir, pattern) {
|
|
15
|
-
const dir = resolve(rootDir, 'patterns');
|
|
16
|
-
mkdirSync(dir, { recursive: true });
|
|
17
|
-
const path = resolve(dir, `${pattern.pattern_id}.yaml`);
|
|
18
|
-
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(pattern)), { lineWidth: 120, noRefs: true }));
|
|
19
|
-
return path;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Write a recommendation to disk.
|
|
24
|
-
*/
|
|
25
|
-
export function writeRecommendation(rootDir, rec) {
|
|
26
|
-
const dir = resolve(rootDir, 'recommendations');
|
|
27
|
-
mkdirSync(dir, { recursive: true });
|
|
28
|
-
const path = resolve(dir, `${rec.recommendation_id}.yaml`);
|
|
29
|
-
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(rec)), { lineWidth: 120, noRefs: true }));
|
|
30
|
-
return path;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Write a doctrine to disk.
|
|
35
|
-
*/
|
|
36
|
-
export function writeDoctrine(rootDir, doc) {
|
|
37
|
-
const dir = resolve(rootDir, 'doctrine');
|
|
38
|
-
mkdirSync(dir, { recursive: true });
|
|
39
|
-
const path = resolve(dir, `${doc.doctrine_id}.yaml`);
|
|
40
|
-
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(doc)), { lineWidth: 120, noRefs: true }));
|
|
41
|
-
return path;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Load all patterns from disk.
|
|
46
|
-
*/
|
|
47
|
-
export function loadPatterns(rootDir) {
|
|
48
|
-
return loadArtifacts(resolve(rootDir, 'patterns'));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Load all recommendations from disk.
|
|
53
|
-
*/
|
|
54
|
-
export function loadRecommendations(rootDir) {
|
|
55
|
-
return loadArtifacts(resolve(rootDir, 'recommendations'));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Load all doctrines from disk.
|
|
60
|
-
*/
|
|
61
|
-
export function loadDoctrines(rootDir) {
|
|
62
|
-
return loadArtifacts(resolve(rootDir, 'doctrine'));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function loadArtifacts(dir) {
|
|
66
|
-
if (!existsSync(dir)) return [];
|
|
67
|
-
return readdirSync(dir)
|
|
68
|
-
.filter(f => f.endsWith('.yaml'))
|
|
69
|
-
.map(f => {
|
|
70
|
-
try {
|
|
71
|
-
return yaml.load(readFileSync(join(dir, f), 'utf-8'));
|
|
72
|
-
} catch { return null; }
|
|
73
|
-
})
|
|
74
|
-
.filter(Boolean);
|
|
75
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Write pattern, recommendation, and doctrine artifacts to disk.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { resolve, join } from 'node:path';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
|
|
9
|
+
import { atomicWriteFileSync } from '../lib/atomic-write.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write a pattern to disk.
|
|
13
|
+
*/
|
|
14
|
+
export function writePattern(rootDir, pattern) {
|
|
15
|
+
const dir = resolve(rootDir, 'patterns');
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
const path = resolve(dir, `${pattern.pattern_id}.yaml`);
|
|
18
|
+
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(pattern)), { lineWidth: 120, noRefs: true }));
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write a recommendation to disk.
|
|
24
|
+
*/
|
|
25
|
+
export function writeRecommendation(rootDir, rec) {
|
|
26
|
+
const dir = resolve(rootDir, 'recommendations');
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
const path = resolve(dir, `${rec.recommendation_id}.yaml`);
|
|
29
|
+
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(rec)), { lineWidth: 120, noRefs: true }));
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Write a doctrine to disk.
|
|
35
|
+
*/
|
|
36
|
+
export function writeDoctrine(rootDir, doc) {
|
|
37
|
+
const dir = resolve(rootDir, 'doctrine');
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
const path = resolve(dir, `${doc.doctrine_id}.yaml`);
|
|
40
|
+
atomicWriteFileSync(path, yaml.dump(JSON.parse(JSON.stringify(doc)), { lineWidth: 120, noRefs: true }));
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load all patterns from disk.
|
|
46
|
+
*/
|
|
47
|
+
export function loadPatterns(rootDir) {
|
|
48
|
+
return loadArtifacts(resolve(rootDir, 'patterns'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load all recommendations from disk.
|
|
53
|
+
*/
|
|
54
|
+
export function loadRecommendations(rootDir) {
|
|
55
|
+
return loadArtifacts(resolve(rootDir, 'recommendations'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load all doctrines from disk.
|
|
60
|
+
*/
|
|
61
|
+
export function loadDoctrines(rootDir) {
|
|
62
|
+
return loadArtifacts(resolve(rootDir, 'doctrine'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadArtifacts(dir) {
|
|
66
|
+
if (!existsSync(dir)) return [];
|
|
67
|
+
return readdirSync(dir)
|
|
68
|
+
.filter(f => f.endsWith('.yaml'))
|
|
69
|
+
.map(f => {
|
|
70
|
+
try {
|
|
71
|
+
return yaml.load(readFileSync(join(dir, f), 'utf-8'));
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
})
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
}
|