@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/query.js CHANGED
@@ -1,182 +1,182 @@
1
- /**
2
- * Query layer over accepted dogfood learning artifacts.
3
- *
4
- * Retrieves findings, patterns, recommendations, and doctrine
5
- * filtered by surface, execution mode, journey stage, issue kind,
6
- * and transfer scope. Respects ranking and caps.
7
- */
8
-
9
- import { loadFindings } from '../reader.js';
10
- import { loadPatterns, loadRecommendations, loadDoctrines } from '../synthesis/write-artifacts.js';
11
-
12
- /**
13
- * Query accepted findings by scope.
14
- *
15
- * @param {string} rootDir
16
- * @param {object} scope
17
- * @param {string} [scope.surface]
18
- * @param {string} [scope.executionMode]
19
- * @param {string} [scope.journeyStage]
20
- * @param {string} [scope.issueKind]
21
- * @param {string} [scope.repo]
22
- * @param {number} [scope.limit=8]
23
- * @returns {Array}
24
- */
25
- export function queryFindings(rootDir, scope = {}) {
26
- const all = loadFindings(rootDir);
27
- const limit = scope.limit || 8;
28
-
29
- const accepted = all.filter(f =>
30
- f.valid &&
31
- f.data?.status === 'accepted' &&
32
- !f.data?.invalidation?.is_invalidated
33
- );
34
-
35
- let results = accepted.map(f => f.data);
36
-
37
- if (scope.surface) results = results.filter(f => f.product_surface === scope.surface);
38
- if (scope.executionMode) results = results.filter(f => f.execution_mode === scope.executionMode);
39
- if (scope.journeyStage) results = results.filter(f => f.journey_stage === scope.journeyStage);
40
- if (scope.issueKind) results = results.filter(f => f.issue_kind === scope.issueKind);
41
- if (scope.repo) results = results.filter(f => f.repo === scope.repo);
42
-
43
- // Rank: more specific transfer_scope first, then by surface match
44
- results = rankByRelevance(results, scope);
45
-
46
- return results.slice(0, limit);
47
- }
48
-
49
- /**
50
- * Query accepted patterns by scope.
51
- */
52
- export function queryPatterns(rootDir, scope = {}) {
53
- const all = loadPatterns(rootDir);
54
- const limit = scope.limit || 5;
55
-
56
- let results = all.filter(p =>
57
- p.status === 'accepted' &&
58
- !(p.review?.last_action === 'invalidate')
59
- );
60
-
61
- if (scope.surface) {
62
- results = results.filter(p =>
63
- (p.dimensions?.product_surfaces || []).includes(scope.surface)
64
- );
65
- }
66
- if (scope.issueKind) {
67
- results = results.filter(p =>
68
- (p.dimensions?.issue_kinds || []).includes(scope.issueKind)
69
- );
70
- }
71
-
72
- // Rank: strong > emerging, more specific scope first
73
- results.sort((a, b) => {
74
- const strengthOrder = { portfolio_stable: 0, strong: 1, emerging: 2 };
75
- const aStr = strengthOrder[a.pattern_strength] ?? 3;
76
- const bStr = strengthOrder[b.pattern_strength] ?? 3;
77
- if (aStr !== bStr) return aStr - bStr;
78
- return scopeSpecificity(b.transfer_scope) - scopeSpecificity(a.transfer_scope);
79
- });
80
-
81
- return results.slice(0, limit);
82
- }
83
-
84
- /**
85
- * Query accepted recommendations by scope.
86
- */
87
- export function queryRecommendations(rootDir, scope = {}) {
88
- const all = loadRecommendations(rootDir);
89
- const limit = scope.limit || 5;
90
-
91
- let results = all.filter(r => r.status === 'accepted');
92
-
93
- if (scope.surface) {
94
- results = results.filter(r =>
95
- (r.applies_to?.product_surfaces || []).includes(scope.surface)
96
- );
97
- }
98
- if (scope.executionMode) {
99
- results = results.filter(r =>
100
- !r.applies_to?.execution_modes?.length ||
101
- r.applies_to.execution_modes.includes(scope.executionMode)
102
- );
103
- }
104
-
105
- // Rank: strong confidence > emerging
106
- results.sort((a, b) => {
107
- const confOrder = { proven: 0, strong: 1, emerging: 2 };
108
- return (confOrder[a.confidence] ?? 3) - (confOrder[b.confidence] ?? 3);
109
- });
110
-
111
- return results.slice(0, limit);
112
- }
113
-
114
- /**
115
- * Query accepted doctrine by scope.
116
- */
117
- export function queryDoctrine(rootDir, scope = {}) {
118
- const all = loadDoctrines(rootDir);
119
- const limit = scope.limit || 5;
120
-
121
- let results = all.filter(d => d.status === 'accepted');
122
-
123
- if (scope.surface) {
124
- // Doctrine applies if its scope is broad enough or matches the surface
125
- // org_wide always applies; surface_archetype applies if pattern surfaces match
126
- results = results.filter(d =>
127
- d.transfer_scope === 'org_wide' ||
128
- d.transfer_scope === 'execution_mode' ||
129
- true // surface_archetype applies broadly — patterns already scoped it
130
- );
131
- }
132
-
133
- results.sort((a, b) => {
134
- const strOrder = { foundational: 0, proven: 1, emerging: 2 };
135
- return (strOrder[a.strength] ?? 3) - (strOrder[b.strength] ?? 3);
136
- });
137
-
138
- return results.slice(0, limit);
139
- }
140
-
141
- /**
142
- * Extract top failure classes from accepted findings for a scope.
143
- */
144
- export function queryFailureClasses(rootDir, scope = {}) {
145
- const findings = queryFindings(rootDir, { ...scope, limit: 50 });
146
- const counts = new Map();
147
-
148
- for (const f of findings) {
149
- const key = f.issue_kind;
150
- counts.set(key, (counts.get(key) || 0) + 1);
151
- }
152
-
153
- return [...counts.entries()]
154
- .sort((a, b) => b[1] - a[1])
155
- .slice(0, 3)
156
- .map(([issueKind, count]) => ({ issueKind, count }));
157
- }
158
-
159
- // ─── Ranking helpers ────────────────────────────────────────
160
-
161
- const SCOPE_ORDER = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
162
-
163
- function scopeSpecificity(scope) {
164
- const idx = SCOPE_ORDER.indexOf(scope);
165
- return idx >= 0 ? SCOPE_ORDER.length - idx : 0; // higher = more specific
166
- }
167
-
168
- function rankByRelevance(findings, scope) {
169
- return findings.sort((a, b) => {
170
- // Exact surface match first
171
- const aSurface = scope.surface && a.product_surface === scope.surface ? 1 : 0;
172
- const bSurface = scope.surface && b.product_surface === scope.surface ? 1 : 0;
173
- if (aSurface !== bSurface) return bSurface - aSurface;
174
-
175
- // More specific scope first
176
- const aSpec = scopeSpecificity(a.transfer_scope);
177
- const bSpec = scopeSpecificity(b.transfer_scope);
178
- if (aSpec !== bSpec) return bSpec - aSpec;
179
-
180
- return 0;
181
- });
182
- }
1
+ /**
2
+ * Query layer over accepted dogfood learning artifacts.
3
+ *
4
+ * Retrieves findings, patterns, recommendations, and doctrine
5
+ * filtered by surface, execution mode, journey stage, issue kind,
6
+ * and transfer scope. Respects ranking and caps.
7
+ */
8
+
9
+ import { loadFindings } from '../reader.js';
10
+ import { loadPatterns, loadRecommendations, loadDoctrines } from '../synthesis/write-artifacts.js';
11
+
12
+ /**
13
+ * Query accepted findings by scope.
14
+ *
15
+ * @param {string} rootDir
16
+ * @param {object} scope
17
+ * @param {string} [scope.surface]
18
+ * @param {string} [scope.executionMode]
19
+ * @param {string} [scope.journeyStage]
20
+ * @param {string} [scope.issueKind]
21
+ * @param {string} [scope.repo]
22
+ * @param {number} [scope.limit=8]
23
+ * @returns {Array}
24
+ */
25
+ export function queryFindings(rootDir, scope = {}) {
26
+ const all = loadFindings(rootDir);
27
+ const limit = scope.limit || 8;
28
+
29
+ const accepted = all.filter(f =>
30
+ f.valid &&
31
+ f.data?.status === 'accepted' &&
32
+ !f.data?.invalidation?.is_invalidated
33
+ );
34
+
35
+ let results = accepted.map(f => f.data);
36
+
37
+ if (scope.surface) results = results.filter(f => f.product_surface === scope.surface);
38
+ if (scope.executionMode) results = results.filter(f => f.execution_mode === scope.executionMode);
39
+ if (scope.journeyStage) results = results.filter(f => f.journey_stage === scope.journeyStage);
40
+ if (scope.issueKind) results = results.filter(f => f.issue_kind === scope.issueKind);
41
+ if (scope.repo) results = results.filter(f => f.repo === scope.repo);
42
+
43
+ // Rank: more specific transfer_scope first, then by surface match
44
+ results = rankByRelevance(results, scope);
45
+
46
+ return results.slice(0, limit);
47
+ }
48
+
49
+ /**
50
+ * Query accepted patterns by scope.
51
+ */
52
+ export function queryPatterns(rootDir, scope = {}) {
53
+ const all = loadPatterns(rootDir);
54
+ const limit = scope.limit || 5;
55
+
56
+ let results = all.filter(p =>
57
+ p.status === 'accepted' &&
58
+ !(p.review?.last_action === 'invalidate')
59
+ );
60
+
61
+ if (scope.surface) {
62
+ results = results.filter(p =>
63
+ (p.dimensions?.product_surfaces || []).includes(scope.surface)
64
+ );
65
+ }
66
+ if (scope.issueKind) {
67
+ results = results.filter(p =>
68
+ (p.dimensions?.issue_kinds || []).includes(scope.issueKind)
69
+ );
70
+ }
71
+
72
+ // Rank: strong > emerging, more specific scope first
73
+ results.sort((a, b) => {
74
+ const strengthOrder = { portfolio_stable: 0, strong: 1, emerging: 2 };
75
+ const aStr = strengthOrder[a.pattern_strength] ?? 3;
76
+ const bStr = strengthOrder[b.pattern_strength] ?? 3;
77
+ if (aStr !== bStr) return aStr - bStr;
78
+ return scopeSpecificity(b.transfer_scope) - scopeSpecificity(a.transfer_scope);
79
+ });
80
+
81
+ return results.slice(0, limit);
82
+ }
83
+
84
+ /**
85
+ * Query accepted recommendations by scope.
86
+ */
87
+ export function queryRecommendations(rootDir, scope = {}) {
88
+ const all = loadRecommendations(rootDir);
89
+ const limit = scope.limit || 5;
90
+
91
+ let results = all.filter(r => r.status === 'accepted');
92
+
93
+ if (scope.surface) {
94
+ results = results.filter(r =>
95
+ (r.applies_to?.product_surfaces || []).includes(scope.surface)
96
+ );
97
+ }
98
+ if (scope.executionMode) {
99
+ results = results.filter(r =>
100
+ !r.applies_to?.execution_modes?.length ||
101
+ r.applies_to.execution_modes.includes(scope.executionMode)
102
+ );
103
+ }
104
+
105
+ // Rank: strong confidence > emerging
106
+ results.sort((a, b) => {
107
+ const confOrder = { proven: 0, strong: 1, emerging: 2 };
108
+ return (confOrder[a.confidence] ?? 3) - (confOrder[b.confidence] ?? 3);
109
+ });
110
+
111
+ return results.slice(0, limit);
112
+ }
113
+
114
+ /**
115
+ * Query accepted doctrine by scope.
116
+ */
117
+ export function queryDoctrine(rootDir, scope = {}) {
118
+ const all = loadDoctrines(rootDir);
119
+ const limit = scope.limit || 5;
120
+
121
+ let results = all.filter(d => d.status === 'accepted');
122
+
123
+ if (scope.surface) {
124
+ // Doctrine applies if its scope is broad enough or matches the surface
125
+ // org_wide always applies; surface_archetype applies if pattern surfaces match
126
+ results = results.filter(d =>
127
+ d.transfer_scope === 'org_wide' ||
128
+ d.transfer_scope === 'execution_mode' ||
129
+ true // surface_archetype applies broadly — patterns already scoped it
130
+ );
131
+ }
132
+
133
+ results.sort((a, b) => {
134
+ const strOrder = { foundational: 0, proven: 1, emerging: 2 };
135
+ return (strOrder[a.strength] ?? 3) - (strOrder[b.strength] ?? 3);
136
+ });
137
+
138
+ return results.slice(0, limit);
139
+ }
140
+
141
+ /**
142
+ * Extract top failure classes from accepted findings for a scope.
143
+ */
144
+ export function queryFailureClasses(rootDir, scope = {}) {
145
+ const findings = queryFindings(rootDir, { ...scope, limit: 50 });
146
+ const counts = new Map();
147
+
148
+ for (const f of findings) {
149
+ const key = f.issue_kind;
150
+ counts.set(key, (counts.get(key) || 0) + 1);
151
+ }
152
+
153
+ return [...counts.entries()]
154
+ .sort((a, b) => b[1] - a[1])
155
+ .slice(0, 3)
156
+ .map(([issueKind, count]) => ({ issueKind, count }));
157
+ }
158
+
159
+ // ─── Ranking helpers ────────────────────────────────────────
160
+
161
+ const SCOPE_ORDER = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
162
+
163
+ function scopeSpecificity(scope) {
164
+ const idx = SCOPE_ORDER.indexOf(scope);
165
+ return idx >= 0 ? SCOPE_ORDER.length - idx : 0; // higher = more specific
166
+ }
167
+
168
+ function rankByRelevance(findings, scope) {
169
+ return findings.sort((a, b) => {
170
+ // Exact surface match first
171
+ const aSurface = scope.surface && a.product_surface === scope.surface ? 1 : 0;
172
+ const bSurface = scope.surface && b.product_surface === scope.surface ? 1 : 0;
173
+ if (aSurface !== bSurface) return bSurface - aSurface;
174
+
175
+ // More specific scope first
176
+ const aSpec = scopeSpecificity(a.transfer_scope);
177
+ const bSpec = scopeSpecificity(b.transfer_scope);
178
+ if (aSpec !== bSpec) return bSpec - aSpec;
179
+
180
+ return 0;
181
+ });
182
+ }
package/derive/dedupe.js CHANGED
@@ -1,107 +1,107 @@
1
- /**
2
- * Deduplication logic for derived candidate findings.
3
- *
4
- * Dedupe law:
5
- * - Same record re-derived → same ID → skip if identical candidate exists
6
- * - Same ID with non-candidate status → collision, not overwrite
7
- * - Multiple evidence points in one record that map to one lesson → one finding
8
- */
9
-
10
- import { computeDedupeKey } from './ids.js';
11
-
12
- /**
13
- * Deduplicate a list of raw derived candidates.
14
- * Returns unique candidates with collision info.
15
- *
16
- * @param {Array} candidates - Raw derived candidates from rules.
17
- * @returns {{ unique: Array, skipped: number }}
18
- */
19
- export function dedupeWithinBatch(candidates) {
20
- const seen = new Map();
21
- const unique = [];
22
- let skipped = 0;
23
-
24
- for (const c of candidates) {
25
- const key = computeDedupeKey({
26
- repo: c.repo,
27
- issue_kind: c.issue_kind,
28
- root_cause_kind: c.root_cause_kind,
29
- journey_stage: c.journey_stage,
30
- slug: c.finding_id
31
- });
32
-
33
- if (seen.has(key)) {
34
- skipped++;
35
- continue;
36
- }
37
- seen.set(key, true);
38
- unique.push(c);
39
- }
40
-
41
- return { unique, skipped };
42
- }
43
-
44
- /**
45
- * Check existing findings on disk for collisions.
46
- *
47
- * @param {Array} candidates - Candidate findings to check.
48
- * @param {Array<{ data: object }>} existingFindings - Already-loaded findings from disk.
49
- * @returns {{ toWrite: Array, skippedUnchanged: number, collisions: Array<{ findingId: string, existingStatus: string }> }}
50
- */
51
- export function dedupeAgainstExisting(candidates, existingFindings) {
52
- const existingById = new Map();
53
- for (const f of existingFindings) {
54
- if (f.data?.finding_id) {
55
- existingById.set(f.data.finding_id, f.data);
56
- }
57
- }
58
-
59
- const toWrite = [];
60
- let skippedUnchanged = 0;
61
- const collisions = [];
62
-
63
- for (const c of candidates) {
64
- const existing = existingById.get(c.finding_id);
65
-
66
- if (!existing) {
67
- toWrite.push(c);
68
- continue;
69
- }
70
-
71
- // Same ID exists — check status
72
- if (existing.status !== 'candidate') {
73
- // Non-candidate status: collision, don't overwrite
74
- collisions.push({
75
- findingId: c.finding_id,
76
- existingStatus: existing.status
77
- });
78
- continue;
79
- }
80
-
81
- // Same ID, still candidate — skip if unchanged
82
- if (isSameCandidate(c, existing)) {
83
- skippedUnchanged++;
84
- continue;
85
- }
86
-
87
- // Same ID, still candidate, but content changed — refresh
88
- toWrite.push(c);
89
- }
90
-
91
- return { toWrite, skippedUnchanged, collisions };
92
- }
93
-
94
- /**
95
- * Check if two findings are substantively identical.
96
- * Ignores timestamps and derivation metadata.
97
- */
98
- function isSameCandidate(a, b) {
99
- return (
100
- a.issue_kind === b.issue_kind &&
101
- a.root_cause_kind === b.root_cause_kind &&
102
- a.remediation_kind === b.remediation_kind &&
103
- a.transfer_scope === b.transfer_scope &&
104
- a.summary === b.summary &&
105
- a.title === b.title
106
- );
107
- }
1
+ /**
2
+ * Deduplication logic for derived candidate findings.
3
+ *
4
+ * Dedupe law:
5
+ * - Same record re-derived → same ID → skip if identical candidate exists
6
+ * - Same ID with non-candidate status → collision, not overwrite
7
+ * - Multiple evidence points in one record that map to one lesson → one finding
8
+ */
9
+
10
+ import { computeDedupeKey } from './ids.js';
11
+
12
+ /**
13
+ * Deduplicate a list of raw derived candidates.
14
+ * Returns unique candidates with collision info.
15
+ *
16
+ * @param {Array} candidates - Raw derived candidates from rules.
17
+ * @returns {{ unique: Array, skipped: number }}
18
+ */
19
+ export function dedupeWithinBatch(candidates) {
20
+ const seen = new Map();
21
+ const unique = [];
22
+ let skipped = 0;
23
+
24
+ for (const c of candidates) {
25
+ const key = computeDedupeKey({
26
+ repo: c.repo,
27
+ issue_kind: c.issue_kind,
28
+ root_cause_kind: c.root_cause_kind,
29
+ journey_stage: c.journey_stage,
30
+ slug: c.finding_id
31
+ });
32
+
33
+ if (seen.has(key)) {
34
+ skipped++;
35
+ continue;
36
+ }
37
+ seen.set(key, true);
38
+ unique.push(c);
39
+ }
40
+
41
+ return { unique, skipped };
42
+ }
43
+
44
+ /**
45
+ * Check existing findings on disk for collisions.
46
+ *
47
+ * @param {Array} candidates - Candidate findings to check.
48
+ * @param {Array<{ data: object }>} existingFindings - Already-loaded findings from disk.
49
+ * @returns {{ toWrite: Array, skippedUnchanged: number, collisions: Array<{ findingId: string, existingStatus: string }> }}
50
+ */
51
+ export function dedupeAgainstExisting(candidates, existingFindings) {
52
+ const existingById = new Map();
53
+ for (const f of existingFindings) {
54
+ if (f.data?.finding_id) {
55
+ existingById.set(f.data.finding_id, f.data);
56
+ }
57
+ }
58
+
59
+ const toWrite = [];
60
+ let skippedUnchanged = 0;
61
+ const collisions = [];
62
+
63
+ for (const c of candidates) {
64
+ const existing = existingById.get(c.finding_id);
65
+
66
+ if (!existing) {
67
+ toWrite.push(c);
68
+ continue;
69
+ }
70
+
71
+ // Same ID exists — check status
72
+ if (existing.status !== 'candidate') {
73
+ // Non-candidate status: collision, don't overwrite
74
+ collisions.push({
75
+ findingId: c.finding_id,
76
+ existingStatus: existing.status
77
+ });
78
+ continue;
79
+ }
80
+
81
+ // Same ID, still candidate — skip if unchanged
82
+ if (isSameCandidate(c, existing)) {
83
+ skippedUnchanged++;
84
+ continue;
85
+ }
86
+
87
+ // Same ID, still candidate, but content changed — refresh
88
+ toWrite.push(c);
89
+ }
90
+
91
+ return { toWrite, skippedUnchanged, collisions };
92
+ }
93
+
94
+ /**
95
+ * Check if two findings are substantively identical.
96
+ * Ignores timestamps and derivation metadata.
97
+ */
98
+ function isSameCandidate(a, b) {
99
+ return (
100
+ a.issue_kind === b.issue_kind &&
101
+ a.root_cause_kind === b.root_cause_kind &&
102
+ a.remediation_kind === b.remediation_kind &&
103
+ a.transfer_scope === b.transfer_scope &&
104
+ a.summary === b.summary &&
105
+ a.title === b.title
106
+ );
107
+ }