@dogfood-lab/findings 1.2.2 → 1.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.
@@ -1,156 +1,187 @@
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 { resolve } from 'node:path';
9
+
10
+ import { loadYamlDir } from '../lib/safe-yaml-load.js';
11
+
12
+ /**
13
+ * Derive recommendations from accepted patterns.
14
+ *
15
+ * D2B-001 — return shape carries a `skipped: [{path, error}]` field so the
16
+ * silent-loader signal that Wave A1 wired through `loadYamlDir` now propagates
17
+ * through the derive API to the operator surface (CLI). Legacy callers that
18
+ * only read `recommendations` / `stats` are unaffected — the field is
19
+ * additive. A torn pattern that WOULD have been considered for recommendation
20
+ * synthesis no longer disappears with no signal.
21
+ *
22
+ * @param {string} rootDir - dogfood-labs repo root
23
+ * @returns {{ recommendations: Array, skipped: Array<{path: string, error: string}>, stats: { patternsConsidered: number, recommendationsEmitted: number, patternsSkipped: number } }}
24
+ */
25
+ export function deriveRecommendations(rootDir) {
26
+ const { entries, skipped } = loadAcceptedPatternsWithSkips(rootDir);
27
+ const patterns = entries.map(e => e.data);
28
+ const recommendations = [];
29
+
30
+ for (const pat of patterns) {
31
+ const rec = deriveFromPattern(pat);
32
+ if (rec) recommendations.push(rec);
33
+ }
34
+
35
+ return {
36
+ recommendations,
37
+ skipped, // D2B-001: structured skip list for operator legibility
38
+ stats: {
39
+ patternsConsidered: patterns.length,
40
+ recommendationsEmitted: recommendations.length,
41
+ patternsSkipped: skipped.length
42
+ }
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Derive a recommendation from a single accepted pattern.
48
+ */
49
+ function deriveFromPattern(pattern) {
50
+ const now = new Date().toISOString();
51
+ const template = selectTemplate(pattern);
52
+ if (!template) return null;
53
+
54
+ const surfaces = pattern.dimensions?.product_surfaces || [];
55
+ const slug = `${surfaces[0] || 'general'}-${template.kind}`.replace(/_/g, '-');
56
+
57
+ return {
58
+ schema_version: '1.0.0',
59
+ recommendation_id: `drec-${slug}-${pattern.pattern_id.replace('dpat-', '')}`,
60
+ title: template.titleFn(pattern),
61
+ status: 'candidate',
62
+ recommendation_kind: template.kind,
63
+ summary: template.summaryFn(pattern),
64
+ applies_to: {
65
+ product_surfaces: surfaces,
66
+ ...(pattern.support?.execution_modes?.length ? { execution_modes: pattern.support.execution_modes } : {}),
67
+ transfer_scope: pattern.transfer_scope
68
+ },
69
+ based_on_pattern_ids: [pattern.pattern_id],
70
+ action: {
71
+ type: template.actionType,
72
+ target: template.target,
73
+ details: template.detailsFn(pattern)
74
+ },
75
+ confidence: pattern.pattern_strength === 'strong' || pattern.pattern_strength === 'portfolio_stable' ? 'strong' : 'emerging',
76
+ created_at: now,
77
+ updated_at: now
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Select the right recommendation template for a pattern.
83
+ */
84
+ function selectTemplate(pattern) {
85
+ const kind = pattern.pattern_kind;
86
+ const issueKinds = pattern.dimensions?.issue_kinds || [];
87
+
88
+ if (issueKinds.some(k => /interface|surface|entrypoint|build/.test(k))) {
89
+ return {
90
+ kind: 'starter_check',
91
+ actionType: 'add_check',
92
+ target: 'rollout',
93
+ titleFn: (p) => `Add ${issueKinds[0].replace(/_/g, ' ')} verification to starter rollout for ${fmtSurfaces(p)}`,
94
+ 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).`,
95
+ detailsFn: (p) => `Verify ${issueKinds[0].replace(/_/g, ' ')} contract and invocation shape before scenario authoring for ${fmtSurfaces(p)} repos.`
96
+ };
97
+ }
98
+
99
+ if (kind === 'evidence_calibration') {
100
+ return {
101
+ kind: 'evidence_expectation',
102
+ actionType: 'set_evidence',
103
+ target: 'policy',
104
+ titleFn: (p) => `Calibrate evidence requirements for ${fmtSurfaces(p)} based on recurring miscalibration`,
105
+ summaryFn: (p) => `Evidence requirements for ${fmtSurfaces(p)} have been repeatedly miscalibrated. Default to natural output types rather than forced artifact shapes.`,
106
+ detailsFn: (p) => `Review and adjust evidence_requirements in surface policy to match natural outputs for ${fmtSurfaces(p)} repos.`
107
+ };
108
+ }
109
+
110
+ if (kind === 'verification_seam') {
111
+ return {
112
+ kind: 'verification_rule',
113
+ actionType: 'set_verification',
114
+ target: 'verification',
115
+ titleFn: (p) => `Add verification guard for ${issueKinds[0]?.replace(/_/g, ' ') || 'seam'} on ${fmtSurfaces(p)}`,
116
+ summaryFn: (p) => `A verification seam recurs on ${fmtSurfaces(p)}. Add a guard to prevent this class of verification failure.`,
117
+ detailsFn: (p) => `Add verification step to detect ${issueKinds[0]?.replace(/_/g, ' ') || 'verification gap'} before acceptance.`
118
+ };
119
+ }
120
+
121
+ if (kind === 'calibration_signal') {
122
+ return {
123
+ kind: 'policy_seed',
124
+ actionType: 'set_policy',
125
+ target: 'policy',
126
+ titleFn: (p) => `Seed ${fmtSurfaces(p)} policy with calibrated defaults from recurring pattern`,
127
+ summaryFn: (p) => `Policy miscalibration recurs on ${fmtSurfaces(p)}. Seed new repo policies with proven defaults.`,
128
+ detailsFn: (p) => `Apply calibrated policy defaults for ${fmtSurfaces(p)} repos based on ${p.support.finding_count} proven findings.`
129
+ };
130
+ }
131
+
132
+ // Default: starter_check
133
+ return {
134
+ kind: 'starter_check',
135
+ actionType: 'add_check',
136
+ target: 'rollout',
137
+ titleFn: (p) => `Add check for ${issueKinds[0]?.replace(/_/g, ' ') || 'recurring issue'} on ${fmtSurfaces(p)}`,
138
+ summaryFn: (p) => `A recurring issue pattern was detected on ${fmtSurfaces(p)}. Add a rollout check to prevent future occurrences.`,
139
+ detailsFn: (p) => `Add a rollout verification step for ${issueKinds[0]?.replace(/_/g, ' ') || 'this issue class'} on ${fmtSurfaces(p)} repos.`
140
+ };
141
+ }
142
+
143
+ function fmtSurfaces(pattern) {
144
+ const s = pattern.dimensions?.product_surfaces || [];
145
+ return s.length ? s.join(', ') : 'general';
146
+ }
147
+
148
+ /**
149
+ * Load accepted patterns from disk.
150
+ *
151
+ * Legacy array shape: returns only accepted patterns. Torn pattern YAML
152
+ * files are NO LONGER silently dropped — they surface via the sibling
153
+ * structured API `loadAcceptedPatternsWithSkips`. This call delegates to
154
+ * that one and discards the skipped list for callers that don't yet care
155
+ * about pipeline honesty.
156
+ *
157
+ * H2 / F-721047-010 — silent-loader closure: the previous implementation
158
+ * wrapped `yaml.load(readFileSync(...))` in a bare `try { ... } catch {}`,
159
+ * silently dropping every torn pattern. Now delegated to the shared
160
+ * `loadYamlDir` helper which returns structured skip records.
161
+ */
162
+ function loadAcceptedPatterns(rootDir) {
163
+ return loadAcceptedPatternsWithSkips(rootDir).entries.map(e => e.data);
164
+ }
165
+
166
+ /**
167
+ * Load accepted patterns from disk, surfacing structured skip records for
168
+ * any torn pattern file. The audit-honesty API.
169
+ *
170
+ * Only `entries[].data` whose `status === 'accepted'` are returned in
171
+ * entries — the legacy `loadAcceptedPatterns` filter is preserved. The
172
+ * `skipped` list captures every torn file (status unknowable) so a torn
173
+ * pattern that WOULD have been accepted does not silently vanish.
174
+ *
175
+ * @param {string} rootDir
176
+ * @returns {{ entries: Array<{ path: string, data: object }>, skipped: Array<{ path: string, error: string }> }}
177
+ */
178
+ function loadAcceptedPatternsWithSkips(rootDir) {
179
+ const dir = resolve(rootDir, 'patterns');
180
+ const { entries, skipped } = loadYamlDir(dir, { recursive: false });
181
+ return {
182
+ entries: entries.filter(e => e.data?.status === 'accepted'),
183
+ skipped,
184
+ };
185
+ }
186
+
187
+ export { loadAcceptedPatterns, loadAcceptedPatternsWithSkips };
@@ -1,46 +1,38 @@
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
+ * H3 hop 1: delegates to the canonical {@link validatePayload} from
5
+ * `@dogfood-lab/schemas`. Pre-H3 this module compiled its own
6
+ * Ajv2020 + ajv-formats instance per schema; that duplicated the
7
+ * verifier's compile path and created the C1 two-Ajv structural gap
8
+ * (same JSON Schema → two distinct compiled validators in two
9
+ * sibling packages). The migration collapses pattern, recommendation,
10
+ * and doctrine to the single cached validator the canonical seam
11
+ * shares with the rest of the workspace.
12
+ *
13
+ * Return contract preserved: `{ valid, errors: [{ path, message }] }`.
14
+ * Synthesis callers ignored `params` and `keyword` historically — we
15
+ * project the canonical ValidationError down to the narrower shape
16
+ * the synthesis layer actually uses.
17
+ */
18
+
19
+ import { validatePayload } from '@dogfood-lab/schemas';
20
+
21
+ function projectErrors(errors) {
22
+ return errors.map(e => ({ path: e.path, message: e.message }));
23
+ }
24
+
25
+ export function validatePattern(data) {
26
+ const result = validatePayload('pattern', data);
27
+ return { valid: result.valid, errors: projectErrors(result.errors) };
28
+ }
29
+
30
+ export function validateRecommendation(data) {
31
+ const result = validatePayload('recommendation', data);
32
+ return { valid: result.valid, errors: projectErrors(result.errors) };
33
+ }
34
+
35
+ export function validateDoctrine(data) {
36
+ const result = validatePayload('doctrine', data);
37
+ return { valid: result.valid, errors: projectErrors(result.errors) };
38
+ }