@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,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Doctrine derivation from strong accepted patterns.
|
|
3
|
-
*
|
|
4
|
-
* Doctrine is the most conservative artifact in the system.
|
|
5
|
-
* Requirements:
|
|
6
|
-
* - At least 1 accepted pattern (2+ for org_wide scope)
|
|
7
|
-
* - Pattern strength must be 'strong' or 'portfolio_stable'
|
|
8
|
-
* - Statement must be rule-like, not advisory
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
-
import { resolve, join } from 'node:path';
|
|
13
|
-
import yaml from 'js-yaml';
|
|
14
|
-
import { loadAcceptedPatterns } from './recommendation-derivation.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Derive doctrine from strong accepted patterns.
|
|
18
|
-
*
|
|
19
|
-
* @param {string} rootDir - dogfood-labs repo root
|
|
20
|
-
* @returns {{ doctrines: Array, stats: { patternsConsidered: number, doctrinesEmitted: number, belowThreshold: number } }}
|
|
21
|
-
*/
|
|
22
|
-
export function deriveDoctrine(rootDir) {
|
|
23
|
-
const patterns = loadAcceptedPatterns(rootDir);
|
|
24
|
-
const strong = patterns.filter(p =>
|
|
25
|
-
p.pattern_strength === 'strong' || p.pattern_strength === 'portfolio_stable'
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const doctrines = [];
|
|
29
|
-
let belowThreshold = 0;
|
|
30
|
-
|
|
31
|
-
// Group strong patterns by shared doctrine theme
|
|
32
|
-
const themes = groupByDoctrineTheme(strong);
|
|
33
|
-
|
|
34
|
-
for (const [theme, themePatterns] of themes) {
|
|
35
|
-
// org_wide doctrine requires 2+ patterns
|
|
36
|
-
const maxScope = widestScope(themePatterns);
|
|
37
|
-
if (maxScope === 'org_wide' && themePatterns.length < 2) {
|
|
38
|
-
belowThreshold++;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
doctrines.push(buildDoctrineCandidate(theme, themePatterns));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
doctrines,
|
|
47
|
-
stats: {
|
|
48
|
-
patternsConsidered: patterns.length,
|
|
49
|
-
doctrinesEmitted: doctrines.length,
|
|
50
|
-
belowThreshold
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Group patterns by doctrine theme (shared root cause family).
|
|
57
|
-
*/
|
|
58
|
-
function groupByDoctrineTheme(patterns) {
|
|
59
|
-
const themes = new Map();
|
|
60
|
-
for (const p of patterns) {
|
|
61
|
-
const rootCauses = p.dimensions?.root_cause_kinds || [];
|
|
62
|
-
const theme = rootCauses[0] || 'general';
|
|
63
|
-
if (!themes.has(theme)) themes.set(theme, []);
|
|
64
|
-
themes.get(theme).push(p);
|
|
65
|
-
}
|
|
66
|
-
return themes;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function widestScope(patterns) {
|
|
70
|
-
const order = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
|
|
71
|
-
let widest = 0;
|
|
72
|
-
for (const p of patterns) {
|
|
73
|
-
const idx = order.indexOf(p.transfer_scope);
|
|
74
|
-
if (idx > widest) widest = idx;
|
|
75
|
-
}
|
|
76
|
-
return order[widest];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function buildDoctrineCandidate(theme, patterns) {
|
|
80
|
-
const now = new Date().toISOString();
|
|
81
|
-
const issueKinds = [...new Set(patterns.flatMap(p => p.dimensions?.issue_kinds || []))];
|
|
82
|
-
const surfaces = [...new Set(patterns.flatMap(p => p.dimensions?.product_surfaces || []))];
|
|
83
|
-
const scope = widestScope(patterns);
|
|
84
|
-
|
|
85
|
-
const kind = classifyDoctrineKind(issueKinds, theme);
|
|
86
|
-
const slug = `${theme}-${kind}`.replace(/_/g, '-');
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
schema_version: '1.0.0',
|
|
90
|
-
doctrine_id: `ddoc-${slug}`,
|
|
91
|
-
title: buildDoctrineTitle(theme, issueKinds, surfaces),
|
|
92
|
-
status: 'candidate',
|
|
93
|
-
doctrine_kind: kind,
|
|
94
|
-
statement: buildDoctrineStatement(theme, issueKinds, surfaces),
|
|
95
|
-
rationale: buildDoctrineRationale(patterns, theme),
|
|
96
|
-
based_on_pattern_ids: patterns.map(p => p.pattern_id),
|
|
97
|
-
transfer_scope: scope === 'repo_local' || scope === 'surface_local' ? 'surface_archetype' : scope,
|
|
98
|
-
strength: patterns.length >= 3 ? 'foundational' : 'proven',
|
|
99
|
-
created_at: now,
|
|
100
|
-
updated_at: now
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function classifyDoctrineKind(issueKinds, theme) {
|
|
105
|
-
if (/evidence/.test(theme) || issueKinds.some(k => /evidence/.test(k))) return 'evidence_law';
|
|
106
|
-
if (/surface|interface/.test(theme)) return 'surface_law';
|
|
107
|
-
if (/policy|calibration/.test(theme)) return 'calibration_law';
|
|
108
|
-
if (/verification|provenance/.test(theme)) return 'verification_law';
|
|
109
|
-
return 'rollout_law';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildDoctrineTitle(theme, issueKinds, surfaces) {
|
|
113
|
-
const label = theme.replace(/_/g, ' ');
|
|
114
|
-
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all surfaces';
|
|
115
|
-
return `${label}: verified rule for ${surfaceStr}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildDoctrineStatement(theme, issueKinds, surfaces) {
|
|
119
|
-
const issueLabel = issueKinds.map(k => k.replace(/_/g, ' ')).join(' and ');
|
|
120
|
-
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all product surfaces';
|
|
121
|
-
return `Verify ${issueLabel} truth before authoring rollout assumptions for ${surfaceStr}. This is a proven recurring failure class — do not skip this step.`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function buildDoctrineRationale(patterns, theme) {
|
|
125
|
-
const findingCount = patterns.reduce((sum, p) => sum + (p.support?.finding_count || 0), 0);
|
|
126
|
-
const repoCount = patterns.reduce((sum, p) => sum + (p.support?.repo_count || 0), 0);
|
|
127
|
-
return `Backed by ${patterns.length} accepted pattern(s) covering ${findingCount} findings across ${repoCount} repo(s). The ${theme.replace(/_/g, ' ')} root cause recurs independently across multiple contexts, confirming this is structural, not incidental.`;
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Doctrine derivation from strong accepted patterns.
|
|
3
|
+
*
|
|
4
|
+
* Doctrine is the most conservative artifact in the system.
|
|
5
|
+
* Requirements:
|
|
6
|
+
* - At least 1 accepted pattern (2+ for org_wide scope)
|
|
7
|
+
* - Pattern strength must be 'strong' or 'portfolio_stable'
|
|
8
|
+
* - Statement must be rule-like, not advisory
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { resolve, join } from 'node:path';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
import { loadAcceptedPatterns } from './recommendation-derivation.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive doctrine from strong accepted patterns.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} rootDir - dogfood-labs repo root
|
|
20
|
+
* @returns {{ doctrines: Array, stats: { patternsConsidered: number, doctrinesEmitted: number, belowThreshold: number } }}
|
|
21
|
+
*/
|
|
22
|
+
export function deriveDoctrine(rootDir) {
|
|
23
|
+
const patterns = loadAcceptedPatterns(rootDir);
|
|
24
|
+
const strong = patterns.filter(p =>
|
|
25
|
+
p.pattern_strength === 'strong' || p.pattern_strength === 'portfolio_stable'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const doctrines = [];
|
|
29
|
+
let belowThreshold = 0;
|
|
30
|
+
|
|
31
|
+
// Group strong patterns by shared doctrine theme
|
|
32
|
+
const themes = groupByDoctrineTheme(strong);
|
|
33
|
+
|
|
34
|
+
for (const [theme, themePatterns] of themes) {
|
|
35
|
+
// org_wide doctrine requires 2+ patterns
|
|
36
|
+
const maxScope = widestScope(themePatterns);
|
|
37
|
+
if (maxScope === 'org_wide' && themePatterns.length < 2) {
|
|
38
|
+
belowThreshold++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
doctrines.push(buildDoctrineCandidate(theme, themePatterns));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
doctrines,
|
|
47
|
+
stats: {
|
|
48
|
+
patternsConsidered: patterns.length,
|
|
49
|
+
doctrinesEmitted: doctrines.length,
|
|
50
|
+
belowThreshold
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Group patterns by doctrine theme (shared root cause family).
|
|
57
|
+
*/
|
|
58
|
+
function groupByDoctrineTheme(patterns) {
|
|
59
|
+
const themes = new Map();
|
|
60
|
+
for (const p of patterns) {
|
|
61
|
+
const rootCauses = p.dimensions?.root_cause_kinds || [];
|
|
62
|
+
const theme = rootCauses[0] || 'general';
|
|
63
|
+
if (!themes.has(theme)) themes.set(theme, []);
|
|
64
|
+
themes.get(theme).push(p);
|
|
65
|
+
}
|
|
66
|
+
return themes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function widestScope(patterns) {
|
|
70
|
+
const order = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
|
|
71
|
+
let widest = 0;
|
|
72
|
+
for (const p of patterns) {
|
|
73
|
+
const idx = order.indexOf(p.transfer_scope);
|
|
74
|
+
if (idx > widest) widest = idx;
|
|
75
|
+
}
|
|
76
|
+
return order[widest];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildDoctrineCandidate(theme, patterns) {
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const issueKinds = [...new Set(patterns.flatMap(p => p.dimensions?.issue_kinds || []))];
|
|
82
|
+
const surfaces = [...new Set(patterns.flatMap(p => p.dimensions?.product_surfaces || []))];
|
|
83
|
+
const scope = widestScope(patterns);
|
|
84
|
+
|
|
85
|
+
const kind = classifyDoctrineKind(issueKinds, theme);
|
|
86
|
+
const slug = `${theme}-${kind}`.replace(/_/g, '-');
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
schema_version: '1.0.0',
|
|
90
|
+
doctrine_id: `ddoc-${slug}`,
|
|
91
|
+
title: buildDoctrineTitle(theme, issueKinds, surfaces),
|
|
92
|
+
status: 'candidate',
|
|
93
|
+
doctrine_kind: kind,
|
|
94
|
+
statement: buildDoctrineStatement(theme, issueKinds, surfaces),
|
|
95
|
+
rationale: buildDoctrineRationale(patterns, theme),
|
|
96
|
+
based_on_pattern_ids: patterns.map(p => p.pattern_id),
|
|
97
|
+
transfer_scope: scope === 'repo_local' || scope === 'surface_local' ? 'surface_archetype' : scope,
|
|
98
|
+
strength: patterns.length >= 3 ? 'foundational' : 'proven',
|
|
99
|
+
created_at: now,
|
|
100
|
+
updated_at: now
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function classifyDoctrineKind(issueKinds, theme) {
|
|
105
|
+
if (/evidence/.test(theme) || issueKinds.some(k => /evidence/.test(k))) return 'evidence_law';
|
|
106
|
+
if (/surface|interface/.test(theme)) return 'surface_law';
|
|
107
|
+
if (/policy|calibration/.test(theme)) return 'calibration_law';
|
|
108
|
+
if (/verification|provenance/.test(theme)) return 'verification_law';
|
|
109
|
+
return 'rollout_law';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildDoctrineTitle(theme, issueKinds, surfaces) {
|
|
113
|
+
const label = theme.replace(/_/g, ' ');
|
|
114
|
+
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all surfaces';
|
|
115
|
+
return `${label}: verified rule for ${surfaceStr}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildDoctrineStatement(theme, issueKinds, surfaces) {
|
|
119
|
+
const issueLabel = issueKinds.map(k => k.replace(/_/g, ' ')).join(' and ');
|
|
120
|
+
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all product surfaces';
|
|
121
|
+
return `Verify ${issueLabel} truth before authoring rollout assumptions for ${surfaceStr}. This is a proven recurring failure class — do not skip this step.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildDoctrineRationale(patterns, theme) {
|
|
125
|
+
const findingCount = patterns.reduce((sum, p) => sum + (p.support?.finding_count || 0), 0);
|
|
126
|
+
const repoCount = patterns.reduce((sum, p) => sum + (p.support?.repo_count || 0), 0);
|
|
127
|
+
return `Backed by ${patterns.length} accepted pattern(s) covering ${findingCount} findings across ${repoCount} repo(s). The ${theme.replace(/_/g, ' ')} root cause recurs independently across multiple contexts, confirming this is structural, not incidental.`;
|
|
128
|
+
}
|
package/synthesis/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Synthesis layer exports.
|
|
3
|
-
*/
|
|
4
|
-
export { derivePatterns } from './pattern-derivation.js';
|
|
5
|
-
export { deriveRecommendations, loadAcceptedPatterns } from './recommendation-derivation.js';
|
|
6
|
-
export { deriveDoctrine } from './doctrine-derivation.js';
|
|
7
|
-
export { validatePattern, validateRecommendation, validateDoctrine } from './validate-artifacts.js';
|
|
8
|
-
export { writePattern, writeRecommendation, writeDoctrine, loadPatterns, loadRecommendations, loadDoctrines } from './write-artifacts.js';
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis layer exports.
|
|
3
|
+
*/
|
|
4
|
+
export { derivePatterns } from './pattern-derivation.js';
|
|
5
|
+
export { deriveRecommendations, loadAcceptedPatterns } from './recommendation-derivation.js';
|
|
6
|
+
export { deriveDoctrine } from './doctrine-derivation.js';
|
|
7
|
+
export { validatePattern, validateRecommendation, validateDoctrine } from './validate-artifacts.js';
|
|
8
|
+
export { writePattern, writeRecommendation, writeDoctrine, loadPatterns, loadRecommendations, loadDoctrines } from './write-artifacts.js';
|
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic pattern derivation from accepted findings.
|
|
3
|
-
*
|
|
4
|
-
* Clusters accepted, non-invalidated findings by shared dimensions:
|
|
5
|
-
* issue_kind + root_cause_kind + (optionally) product_surface
|
|
6
|
-
*
|
|
7
|
-
* A pattern candidate forms when 2+ accepted findings share these dimensions.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { loadFindings } from '../reader.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Derive candidate patterns from accepted findings.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} rootDir - dogfood-labs repo root
|
|
16
|
-
* @param {{ includeFixtures?: boolean }} opts
|
|
17
|
-
* @returns {{ patterns: Array, stats: { findingsConsidered: number, clustersFound: number, belowThreshold: number } }}
|
|
18
|
-
*/
|
|
19
|
-
export function derivePatterns(rootDir, opts = {}) {
|
|
20
|
-
// Load only accepted, non-invalidated findings
|
|
21
|
-
const allFindings = loadFindings(rootDir);
|
|
22
|
-
if (opts.includeFixtures) {
|
|
23
|
-
allFindings.push(...loadFindings(rootDir, { fixtures: true, fixtureKind: 'valid' }));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const accepted = allFindings.filter(f =>
|
|
27
|
-
f.valid &&
|
|
28
|
-
f.data?.status === 'accepted' &&
|
|
29
|
-
!f.data?.invalidation?.is_invalidated
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// Cluster by shared dimensions
|
|
33
|
-
const clusters = new Map();
|
|
34
|
-
|
|
35
|
-
for (const f of accepted) {
|
|
36
|
-
const d = f.data;
|
|
37
|
-
const key = buildClusterKey(d);
|
|
38
|
-
|
|
39
|
-
if (!clusters.has(key)) {
|
|
40
|
-
clusters.set(key, {
|
|
41
|
-
issue_kind: d.issue_kind,
|
|
42
|
-
root_cause_kind: d.root_cause_kind,
|
|
43
|
-
remediation_kind: d.remediation_kind,
|
|
44
|
-
findings: []
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
clusters.get(key).findings.push(d);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Filter clusters to those meeting threshold (2+ findings)
|
|
51
|
-
const patterns = [];
|
|
52
|
-
let belowThreshold = 0;
|
|
53
|
-
|
|
54
|
-
for (const [key, cluster] of clusters) {
|
|
55
|
-
if (cluster.findings.length < 2) {
|
|
56
|
-
belowThreshold++;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check for false recurrence: all from same repo AND same source record
|
|
61
|
-
if (isFalseRecurrence(cluster.findings)) {
|
|
62
|
-
belowThreshold++;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
patterns.push(buildPatternCandidate(cluster));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
patterns,
|
|
71
|
-
stats: {
|
|
72
|
-
findingsConsidered: accepted.length,
|
|
73
|
-
clustersFound: clusters.size,
|
|
74
|
-
belowThreshold
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Build a cluster key from shared dimensions.
|
|
81
|
-
*/
|
|
82
|
-
function buildClusterKey(finding) {
|
|
83
|
-
return `${finding.issue_kind}::${finding.root_cause_kind}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check for false recurrence: all findings from same root incident.
|
|
88
|
-
*/
|
|
89
|
-
function isFalseRecurrence(findings) {
|
|
90
|
-
// If all findings share exact same source_record_ids set, it's likely one incident split by extraction
|
|
91
|
-
if (findings.length < 2) return true;
|
|
92
|
-
|
|
93
|
-
const recordSets = findings.map(f => (f.source_record_ids || []).sort().join(','));
|
|
94
|
-
const unique = new Set(recordSets);
|
|
95
|
-
return unique.size === 1; // All from same records = false recurrence
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Build a pattern candidate from a cluster.
|
|
100
|
-
*/
|
|
101
|
-
function buildPatternCandidate(cluster) {
|
|
102
|
-
const { findings, issue_kind, root_cause_kind, remediation_kind } = cluster;
|
|
103
|
-
const now = new Date().toISOString();
|
|
104
|
-
|
|
105
|
-
// Compute support dimensions
|
|
106
|
-
const repos = new Set(findings.map(f => f.repo));
|
|
107
|
-
const surfaces = new Set(findings.map(f => f.product_surface));
|
|
108
|
-
const modes = new Set(findings.flatMap(f => f.execution_mode ? [f.execution_mode] : []));
|
|
109
|
-
const remediations = new Set(findings.map(f => f.remediation_kind));
|
|
110
|
-
|
|
111
|
-
// Determine transfer scope (widest from findings)
|
|
112
|
-
const scopes = findings.map(f => f.transfer_scope);
|
|
113
|
-
const transfer_scope = widenScope(scopes);
|
|
114
|
-
|
|
115
|
-
// Determine pattern kind
|
|
116
|
-
const pattern_kind = classifyPatternKind(issue_kind);
|
|
117
|
-
|
|
118
|
-
// Build slug — must include every dimension in the cluster key (issue_kind + root_cause_kind),
|
|
119
|
-
// otherwise two clusters that differ only by root_cause_kind collide on pattern_id and the
|
|
120
|
-
// second writePattern() silently overwrites the first on disk. Surface is added for readability,
|
|
121
|
-
// not for uniqueness.
|
|
122
|
-
const surfaceStr = surfaces.size === 1 ? [...surfaces][0] : 'multi-surface';
|
|
123
|
-
const slug = `${surfaceStr}-${issue_kind}-${root_cause_kind}`.replace(/_/g, '-');
|
|
124
|
-
|
|
125
|
-
// Determine strength
|
|
126
|
-
const strength = repos.size >= 3 ? 'strong' : repos.size >= 2 ? 'emerging' : 'emerging';
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
schema_version: '1.0.0',
|
|
130
|
-
pattern_id: `dpat-${slug}`,
|
|
131
|
-
title: buildPatternTitle(issue_kind, surfaces, repos.size),
|
|
132
|
-
status: 'candidate',
|
|
133
|
-
pattern_kind,
|
|
134
|
-
summary: buildPatternSummary(findings, issue_kind, root_cause_kind),
|
|
135
|
-
source_finding_ids: findings.map(f => f.finding_id),
|
|
136
|
-
support: {
|
|
137
|
-
finding_count: findings.length,
|
|
138
|
-
repo_count: repos.size,
|
|
139
|
-
surface_count: surfaces.size,
|
|
140
|
-
...(modes.size > 0 ? { execution_modes: [...modes] } : {})
|
|
141
|
-
},
|
|
142
|
-
dimensions: {
|
|
143
|
-
product_surfaces: [...surfaces],
|
|
144
|
-
issue_kinds: [issue_kind],
|
|
145
|
-
root_cause_kinds: [root_cause_kind],
|
|
146
|
-
remediation_kinds: [...remediations]
|
|
147
|
-
},
|
|
148
|
-
transfer_scope,
|
|
149
|
-
pattern_strength: strength,
|
|
150
|
-
lineage_note: `Derived from ${findings.length} accepted findings across ${repos.size} repo(s).`,
|
|
151
|
-
created_at: now,
|
|
152
|
-
updated_at: now
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function classifyPatternKind(issueKind) {
|
|
157
|
-
if (/evidence/.test(issueKind)) return 'evidence_calibration';
|
|
158
|
-
if (/verification|provenance/.test(issueKind)) return 'verification_seam';
|
|
159
|
-
if (/policy|freshness/.test(issueKind)) return 'calibration_signal';
|
|
160
|
-
if (/build|entrypoint|flag|interface|surface/.test(issueKind)) return 'recurring_failure';
|
|
161
|
-
return 'recurring_failure';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function widenScope(scopes) {
|
|
165
|
-
const order = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
|
|
166
|
-
let widest = 0;
|
|
167
|
-
for (const s of scopes) {
|
|
168
|
-
const idx = order.indexOf(s);
|
|
169
|
-
if (idx > widest) widest = idx;
|
|
170
|
-
}
|
|
171
|
-
return order[widest];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function buildPatternTitle(issueKind, surfaces, repoCount) {
|
|
175
|
-
const surfaceStr = surfaces.size === 1 ? `${[...surfaces][0]}` : `${surfaces.size} surfaces`;
|
|
176
|
-
const label = issueKind.replace(/_/g, ' ');
|
|
177
|
-
return `${label} recurs across ${repoCount} repo(s) on ${surfaceStr}`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function buildPatternSummary(findings, issueKind, rootCause) {
|
|
181
|
-
const repos = [...new Set(findings.map(f => f.repo))];
|
|
182
|
-
const repoNames = repos.map(r => r.split('/').pop()).join(', ');
|
|
183
|
-
return `Multiple accepted findings show ${issueKind.replace(/_/g, ' ')} caused by ${rootCause.replace(/_/g, ' ')} across repos: ${repoNames}. This recurrence indicates a structural pattern, not isolated incidents.`;
|
|
184
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pattern derivation from accepted findings.
|
|
3
|
+
*
|
|
4
|
+
* Clusters accepted, non-invalidated findings by shared dimensions:
|
|
5
|
+
* issue_kind + root_cause_kind + (optionally) product_surface
|
|
6
|
+
*
|
|
7
|
+
* A pattern candidate forms when 2+ accepted findings share these dimensions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { loadFindings } from '../reader.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Derive candidate patterns from accepted findings.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} rootDir - dogfood-labs repo root
|
|
16
|
+
* @param {{ includeFixtures?: boolean }} opts
|
|
17
|
+
* @returns {{ patterns: Array, stats: { findingsConsidered: number, clustersFound: number, belowThreshold: number } }}
|
|
18
|
+
*/
|
|
19
|
+
export function derivePatterns(rootDir, opts = {}) {
|
|
20
|
+
// Load only accepted, non-invalidated findings
|
|
21
|
+
const allFindings = loadFindings(rootDir);
|
|
22
|
+
if (opts.includeFixtures) {
|
|
23
|
+
allFindings.push(...loadFindings(rootDir, { fixtures: true, fixtureKind: 'valid' }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const accepted = allFindings.filter(f =>
|
|
27
|
+
f.valid &&
|
|
28
|
+
f.data?.status === 'accepted' &&
|
|
29
|
+
!f.data?.invalidation?.is_invalidated
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Cluster by shared dimensions
|
|
33
|
+
const clusters = new Map();
|
|
34
|
+
|
|
35
|
+
for (const f of accepted) {
|
|
36
|
+
const d = f.data;
|
|
37
|
+
const key = buildClusterKey(d);
|
|
38
|
+
|
|
39
|
+
if (!clusters.has(key)) {
|
|
40
|
+
clusters.set(key, {
|
|
41
|
+
issue_kind: d.issue_kind,
|
|
42
|
+
root_cause_kind: d.root_cause_kind,
|
|
43
|
+
remediation_kind: d.remediation_kind,
|
|
44
|
+
findings: []
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
clusters.get(key).findings.push(d);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Filter clusters to those meeting threshold (2+ findings)
|
|
51
|
+
const patterns = [];
|
|
52
|
+
let belowThreshold = 0;
|
|
53
|
+
|
|
54
|
+
for (const [key, cluster] of clusters) {
|
|
55
|
+
if (cluster.findings.length < 2) {
|
|
56
|
+
belowThreshold++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for false recurrence: all from same repo AND same source record
|
|
61
|
+
if (isFalseRecurrence(cluster.findings)) {
|
|
62
|
+
belowThreshold++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
patterns.push(buildPatternCandidate(cluster));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
patterns,
|
|
71
|
+
stats: {
|
|
72
|
+
findingsConsidered: accepted.length,
|
|
73
|
+
clustersFound: clusters.size,
|
|
74
|
+
belowThreshold
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a cluster key from shared dimensions.
|
|
81
|
+
*/
|
|
82
|
+
function buildClusterKey(finding) {
|
|
83
|
+
return `${finding.issue_kind}::${finding.root_cause_kind}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check for false recurrence: all findings from same root incident.
|
|
88
|
+
*/
|
|
89
|
+
function isFalseRecurrence(findings) {
|
|
90
|
+
// If all findings share exact same source_record_ids set, it's likely one incident split by extraction
|
|
91
|
+
if (findings.length < 2) return true;
|
|
92
|
+
|
|
93
|
+
const recordSets = findings.map(f => (f.source_record_ids || []).sort().join(','));
|
|
94
|
+
const unique = new Set(recordSets);
|
|
95
|
+
return unique.size === 1; // All from same records = false recurrence
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a pattern candidate from a cluster.
|
|
100
|
+
*/
|
|
101
|
+
function buildPatternCandidate(cluster) {
|
|
102
|
+
const { findings, issue_kind, root_cause_kind, remediation_kind } = cluster;
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
|
|
105
|
+
// Compute support dimensions
|
|
106
|
+
const repos = new Set(findings.map(f => f.repo));
|
|
107
|
+
const surfaces = new Set(findings.map(f => f.product_surface));
|
|
108
|
+
const modes = new Set(findings.flatMap(f => f.execution_mode ? [f.execution_mode] : []));
|
|
109
|
+
const remediations = new Set(findings.map(f => f.remediation_kind));
|
|
110
|
+
|
|
111
|
+
// Determine transfer scope (widest from findings)
|
|
112
|
+
const scopes = findings.map(f => f.transfer_scope);
|
|
113
|
+
const transfer_scope = widenScope(scopes);
|
|
114
|
+
|
|
115
|
+
// Determine pattern kind
|
|
116
|
+
const pattern_kind = classifyPatternKind(issue_kind);
|
|
117
|
+
|
|
118
|
+
// Build slug — must include every dimension in the cluster key (issue_kind + root_cause_kind),
|
|
119
|
+
// otherwise two clusters that differ only by root_cause_kind collide on pattern_id and the
|
|
120
|
+
// second writePattern() silently overwrites the first on disk. Surface is added for readability,
|
|
121
|
+
// not for uniqueness.
|
|
122
|
+
const surfaceStr = surfaces.size === 1 ? [...surfaces][0] : 'multi-surface';
|
|
123
|
+
const slug = `${surfaceStr}-${issue_kind}-${root_cause_kind}`.replace(/_/g, '-');
|
|
124
|
+
|
|
125
|
+
// Determine strength
|
|
126
|
+
const strength = repos.size >= 3 ? 'strong' : repos.size >= 2 ? 'emerging' : 'emerging';
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
schema_version: '1.0.0',
|
|
130
|
+
pattern_id: `dpat-${slug}`,
|
|
131
|
+
title: buildPatternTitle(issue_kind, surfaces, repos.size),
|
|
132
|
+
status: 'candidate',
|
|
133
|
+
pattern_kind,
|
|
134
|
+
summary: buildPatternSummary(findings, issue_kind, root_cause_kind),
|
|
135
|
+
source_finding_ids: findings.map(f => f.finding_id),
|
|
136
|
+
support: {
|
|
137
|
+
finding_count: findings.length,
|
|
138
|
+
repo_count: repos.size,
|
|
139
|
+
surface_count: surfaces.size,
|
|
140
|
+
...(modes.size > 0 ? { execution_modes: [...modes] } : {})
|
|
141
|
+
},
|
|
142
|
+
dimensions: {
|
|
143
|
+
product_surfaces: [...surfaces],
|
|
144
|
+
issue_kinds: [issue_kind],
|
|
145
|
+
root_cause_kinds: [root_cause_kind],
|
|
146
|
+
remediation_kinds: [...remediations]
|
|
147
|
+
},
|
|
148
|
+
transfer_scope,
|
|
149
|
+
pattern_strength: strength,
|
|
150
|
+
lineage_note: `Derived from ${findings.length} accepted findings across ${repos.size} repo(s).`,
|
|
151
|
+
created_at: now,
|
|
152
|
+
updated_at: now
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function classifyPatternKind(issueKind) {
|
|
157
|
+
if (/evidence/.test(issueKind)) return 'evidence_calibration';
|
|
158
|
+
if (/verification|provenance/.test(issueKind)) return 'verification_seam';
|
|
159
|
+
if (/policy|freshness/.test(issueKind)) return 'calibration_signal';
|
|
160
|
+
if (/build|entrypoint|flag|interface|surface/.test(issueKind)) return 'recurring_failure';
|
|
161
|
+
return 'recurring_failure';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function widenScope(scopes) {
|
|
165
|
+
const order = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
|
|
166
|
+
let widest = 0;
|
|
167
|
+
for (const s of scopes) {
|
|
168
|
+
const idx = order.indexOf(s);
|
|
169
|
+
if (idx > widest) widest = idx;
|
|
170
|
+
}
|
|
171
|
+
return order[widest];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildPatternTitle(issueKind, surfaces, repoCount) {
|
|
175
|
+
const surfaceStr = surfaces.size === 1 ? `${[...surfaces][0]}` : `${surfaces.size} surfaces`;
|
|
176
|
+
const label = issueKind.replace(/_/g, ' ');
|
|
177
|
+
return `${label} recurs across ${repoCount} repo(s) on ${surfaceStr}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildPatternSummary(findings, issueKind, rootCause) {
|
|
181
|
+
const repos = [...new Set(findings.map(f => f.repo))];
|
|
182
|
+
const repoNames = repos.map(r => r.split('/').pop()).join(', ');
|
|
183
|
+
return `Multiple accepted findings show ${issueKind.replace(/_/g, ' ')} caused by ${rootCause.replace(/_/g, ' ')} across repos: ${repoNames}. This recurrence indicates a structural pattern, not isolated incidents.`;
|
|
184
|
+
}
|