@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
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
console.log(`Written: ${relative(ROOT,
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
console.log(`Written: ${relative(ROOT,
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
console.log(`Written: ${relative(ROOT,
|
|
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.`);
|