@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/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/cli.js CHANGED
@@ -51,6 +51,9 @@ import {
51
51
  writePattern,
52
52
  writeRecommendation,
53
53
  writeDoctrine,
54
+ writePatterns,
55
+ writeRecommendations,
56
+ writeDoctrines,
54
57
  loadPatterns,
55
58
  loadRecommendations,
56
59
  loadDoctrines
@@ -429,7 +432,10 @@ Filters (for list):
429
432
  if (errors.length > 0) {
430
433
  console.error(`Errors: ${errors.length}`);
431
434
  for (const e of errors) {
432
- console.error(` ${e.findingId}: ${e.error}`);
435
+ // L2-004 (Wave A2 amend2): surface the structured `.code` so
436
+ // operators can grep for FINDING_ID_COLLISION etc., matching
437
+ // the sibling artifact CLIs (cli.js:676/756/832).
438
+ console.error(` ${e.findingId || '<unknown>'}: ${e.code || 'WRITE_ERROR'} ${e.error}`);
433
439
  }
434
440
  process.exit(1);
435
441
  }
@@ -663,9 +669,16 @@ Filters (for list):
663
669
  }
664
670
 
665
671
  if (write && patterns.length > 0) {
666
- for (const p of patterns) {
667
- const path = writePattern(ROOT, p);
668
- console.log(`Written: ${relative(ROOT, path)}`);
672
+ const { written, errors } = writePatterns(ROOT, patterns);
673
+ for (const p of written) {
674
+ console.log(`Written: ${relative(ROOT, p)}`);
675
+ }
676
+ if (errors.length > 0) {
677
+ console.error(`Errors: ${errors.length}`);
678
+ for (const e of errors) {
679
+ console.error(` ${e.patternId || '<unknown>'}: ${e.code || 'WRITE_ERROR'} ${e.error}`);
680
+ }
681
+ process.exit(1);
669
682
  }
670
683
  } else if (!write && patterns.length > 0) {
671
684
  console.log(`(dry-run) ${patterns.length} pattern(s) would be written. Use --write to materialize.`);
@@ -714,7 +727,17 @@ Filters (for list):
714
727
  const sub = positional[0];
715
728
  if (sub === 'derive') {
716
729
  const write = flags.write;
717
- const { recommendations, stats } = deriveRecommendations(ROOT);
730
+ const { recommendations, skipped, stats } = deriveRecommendations(ROOT);
731
+
732
+ // D2B-001 — surface structured skip signal to operators. The derive
733
+ // engine still emits clean recommendations from the patterns it could
734
+ // read; the skipped list documents the partial-completion honestly.
735
+ if (skipped && skipped.length > 0) {
736
+ console.error(`${skipped.length} pattern(s) skipped (torn/unreadable):`);
737
+ for (const s of skipped) {
738
+ console.error(` ${relative(ROOT, s.path)} — ${s.error}`);
739
+ }
740
+ }
718
741
 
719
742
  console.log(`Patterns considered: ${stats.patternsConsidered}`);
720
743
  console.log(`Recommendations emitted: ${stats.recommendationsEmitted}\n`);
@@ -726,9 +749,16 @@ Filters (for list):
726
749
  }
727
750
 
728
751
  if (write && recommendations.length > 0) {
729
- for (const r of recommendations) {
730
- const path = writeRecommendation(ROOT, r);
731
- console.log(`Written: ${relative(ROOT, path)}`);
752
+ const { written, errors } = writeRecommendations(ROOT, recommendations);
753
+ for (const p of written) {
754
+ console.log(`Written: ${relative(ROOT, p)}`);
755
+ }
756
+ if (errors.length > 0) {
757
+ console.error(`Errors: ${errors.length}`);
758
+ for (const e of errors) {
759
+ console.error(` ${e.recommendationId || '<unknown>'}: ${e.code || 'WRITE_ERROR'} ${e.error}`);
760
+ }
761
+ process.exit(1);
732
762
  }
733
763
  } else if (!write && recommendations.length > 0) {
734
764
  console.log(`(dry-run) ${recommendations.length} recommendation(s) would be written. Use --write to materialize.`);
@@ -774,7 +804,15 @@ Filters (for list):
774
804
  const sub = positional[0];
775
805
  if (sub === 'derive') {
776
806
  const write = flags.write;
777
- const { doctrines, stats } = deriveDoctrine(ROOT);
807
+ const { doctrines, skipped, stats } = deriveDoctrine(ROOT);
808
+
809
+ // D2B-001 — surface structured skip signal to operators.
810
+ if (skipped && skipped.length > 0) {
811
+ console.error(`${skipped.length} pattern(s) skipped (torn/unreadable):`);
812
+ for (const s of skipped) {
813
+ console.error(` ${relative(ROOT, s.path)} — ${s.error}`);
814
+ }
815
+ }
778
816
 
779
817
  console.log(`Patterns considered: ${stats.patternsConsidered}`);
780
818
  console.log(`Doctrines emitted: ${stats.doctrinesEmitted}`);
@@ -787,9 +825,16 @@ Filters (for list):
787
825
  }
788
826
 
789
827
  if (write && doctrines.length > 0) {
790
- for (const d of doctrines) {
791
- const path = writeDoctrine(ROOT, d);
792
- console.log(`Written: ${relative(ROOT, path)}`);
828
+ const { written, errors } = writeDoctrines(ROOT, doctrines);
829
+ for (const p of written) {
830
+ console.log(`Written: ${relative(ROOT, p)}`);
831
+ }
832
+ if (errors.length > 0) {
833
+ console.error(`Errors: ${errors.length}`);
834
+ for (const e of errors) {
835
+ console.error(` ${e.doctrineId || '<unknown>'}: ${e.code || 'WRITE_ERROR'} ${e.error}`);
836
+ }
837
+ process.exit(1);
793
838
  }
794
839
  } else if (!write && doctrines.length > 0) {
795
840
  console.log(`(dry-run) ${doctrines.length} doctrine(s) would be written. Use --write to materialize.`);