@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.
- package/advise/advice-bundle.js +155 -155
- package/advise/index.js +5 -5
- package/advise/query.js +182 -182
- package/cli.js +57 -12
- 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 +245 -153
- package/derive/rules.js +415 -415
- package/derive/write-findings.js +215 -63
- package/index.js +11 -11
- package/lib/file-lock.js +359 -359
- package/lib/rename-with-retry.js +29 -5
- package/lib/safe-yaml-load.js +169 -0
- package/package.json +3 -5
- package/reader.js +156 -156
- package/review/event-log.js +49 -23
- package/review/index.js +6 -6
- package/review/review-engine.js +22 -1
- package/review/transitions.js +79 -79
- package/synthesis/doctrine-derivation.js +137 -128
- package/synthesis/index.js +12 -8
- package/synthesis/pattern-derivation.js +184 -184
- package/synthesis/recommendation-derivation.js +187 -156
- package/synthesis/validate-artifacts.js +38 -46
- package/synthesis/write-artifacts.js +357 -75
- package/validate.js +69 -87
|
@@ -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 {
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Derive recommendations from accepted patterns.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 { 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|