@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/review/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review system exports.
|
|
3
|
-
*/
|
|
4
|
-
export { isLawfulTransition, validateTransition, ACTION_TARGET_STATUS, REASON_REQUIRED } from './transitions.js';
|
|
5
|
-
export { createEvent, appendEvent, getEventsForFinding, getAllEvents, getLogPath, generateEventId } from './event-log.js';
|
|
6
|
-
export { performAction, performMerge, getReviewQueue } from './review-engine.js';
|
|
1
|
+
/**
|
|
2
|
+
* Review system exports.
|
|
3
|
+
*/
|
|
4
|
+
export { isLawfulTransition, validateTransition, ACTION_TARGET_STATUS, REASON_REQUIRED } from './transitions.js';
|
|
5
|
+
export { createEvent, appendEvent, getEventsForFinding, getAllEvents, getLogPath, generateEventId } from './event-log.js';
|
|
6
|
+
export { performAction, performMerge, getReviewQueue } from './review-engine.js';
|
package/review/review-engine.js
CHANGED
|
@@ -101,13 +101,34 @@ export function performAction(rootDir, params) {
|
|
|
101
101
|
|
|
102
102
|
// Apply review metadata
|
|
103
103
|
const now = new Date().toISOString();
|
|
104
|
+
|
|
105
|
+
// H4 / F-stage-a-h4 — auto-populate review.reject_reason whenever the
|
|
106
|
+
// target status is 'rejected'. Pre-amend, the engine only set the field
|
|
107
|
+
// for `action === 'reject'` and silently produced rejected findings on
|
|
108
|
+
// merge / supersede without a structured reason. The H4 schema if/then
|
|
109
|
+
// would then reject those findings at the contract boundary — but the
|
|
110
|
+
// engine was producing them. The fix:
|
|
111
|
+
//
|
|
112
|
+
// - Operator-supplied `params.rejectReason` always wins (override).
|
|
113
|
+
// - `merge` and `supersede` auto-default to `'merged_into_canonical'`
|
|
114
|
+
// since lineage already carries `superseded_by` / `merged_from`.
|
|
115
|
+
// - For `action === 'reject'`, the engine does NOT invent a default —
|
|
116
|
+
// defaulting would erase operator intent. The schema enforces that
|
|
117
|
+
// the field is set; an operator who forgets sees a validation error
|
|
118
|
+
// downstream and learns to pass one. (This is the intentional
|
|
119
|
+
// test-and-tell path in h4-engine-auto-reject-reason.test.js.)
|
|
120
|
+
let effectiveRejectReason = params.rejectReason;
|
|
121
|
+
if (!effectiveRejectReason && toStatus === 'rejected' && (action === 'merge' || action === 'supersede')) {
|
|
122
|
+
effectiveRejectReason = 'merged_into_canonical';
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
finding.review = {
|
|
105
126
|
reviewed_by: actor,
|
|
106
127
|
reviewed_at: now,
|
|
107
128
|
last_action: action,
|
|
108
129
|
...(params.reason ? { decision_reason: params.reason } : {}),
|
|
109
130
|
...(params.notes ? { review_notes: params.notes } : {}),
|
|
110
|
-
...(
|
|
131
|
+
...(effectiveRejectReason ? { reject_reason: effectiveRejectReason } : {})
|
|
111
132
|
};
|
|
112
133
|
|
|
113
134
|
// Update timestamps
|
package/review/transitions.js
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Status transition law for findings.
|
|
3
|
-
*
|
|
4
|
-
* Lawful transitions:
|
|
5
|
-
* candidate -> reviewed, accepted, rejected
|
|
6
|
-
* reviewed -> accepted, rejected
|
|
7
|
-
* accepted -> reviewed (via reopen/invalidate only)
|
|
8
|
-
* accepted -> rejected (via invalidation/reversal only)
|
|
9
|
-
* rejected -> reviewed (via reopen only)
|
|
10
|
-
*
|
|
11
|
-
* Forbidden:
|
|
12
|
-
* rejected -> candidate (no rewinding to machine output)
|
|
13
|
-
* any status -> candidate (except initial creation)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const TRANSITIONS = {
|
|
17
|
-
candidate: new Set(['reviewed', 'accepted', 'rejected']),
|
|
18
|
-
reviewed: new Set(['accepted', 'rejected']),
|
|
19
|
-
accepted: new Set(['reviewed', 'rejected']),
|
|
20
|
-
rejected: new Set(['reviewed'])
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check if a status transition is lawful.
|
|
25
|
-
* @param {string} from - Current status.
|
|
26
|
-
* @param {string} to - Desired status.
|
|
27
|
-
* @returns {boolean}
|
|
28
|
-
*/
|
|
29
|
-
export function isLawfulTransition(from, to) {
|
|
30
|
-
const allowed = TRANSITIONS[from];
|
|
31
|
-
if (!allowed) return false;
|
|
32
|
-
return allowed.has(to);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate a transition and return error if invalid.
|
|
37
|
-
* @param {string} from
|
|
38
|
-
* @param {string} to
|
|
39
|
-
* @returns {{ valid: boolean, error?: string }}
|
|
40
|
-
*/
|
|
41
|
-
export function validateTransition(from, to) {
|
|
42
|
-
if (!TRANSITIONS[from]) {
|
|
43
|
-
return { valid: false, error: `Unknown status: "${from}"` };
|
|
44
|
-
}
|
|
45
|
-
if (!isLawfulTransition(from, to)) {
|
|
46
|
-
const allowed = [...TRANSITIONS[from]].join(', ');
|
|
47
|
-
return { valid: false, error: `Cannot transition from "${from}" to "${to}". Allowed: ${allowed}` };
|
|
48
|
-
}
|
|
49
|
-
return { valid: true };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Map review actions to their target statuses.
|
|
54
|
-
*/
|
|
55
|
-
export const ACTION_TARGET_STATUS = {
|
|
56
|
-
review: 'reviewed',
|
|
57
|
-
accept: 'accepted',
|
|
58
|
-
reject: 'rejected',
|
|
59
|
-
edit: null, // edit preserves current status
|
|
60
|
-
merge: 'rejected', // merged sources become rejected (merged_into_canonical)
|
|
61
|
-
reopen: 'reviewed',
|
|
62
|
-
invalidate: 'reviewed', // invalidated accepted → back to reviewed with invalidation metadata
|
|
63
|
-
supersede: 'rejected' // superseded finding becomes rejected
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Actions that require a reason.
|
|
68
|
-
*/
|
|
69
|
-
export const REASON_REQUIRED = new Set(['reject', 'invalidate', 'merge', 'supersede']);
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Actions that require from_status to be accepted.
|
|
73
|
-
*/
|
|
74
|
-
export const REQUIRES_ACCEPTED = new Set(['invalidate']);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Actions that require from_status to be accepted or rejected.
|
|
78
|
-
*/
|
|
79
|
-
export const REQUIRES_CLOSED = new Set(['reopen']);
|
|
1
|
+
/**
|
|
2
|
+
* Status transition law for findings.
|
|
3
|
+
*
|
|
4
|
+
* Lawful transitions:
|
|
5
|
+
* candidate -> reviewed, accepted, rejected
|
|
6
|
+
* reviewed -> accepted, rejected
|
|
7
|
+
* accepted -> reviewed (via reopen/invalidate only)
|
|
8
|
+
* accepted -> rejected (via invalidation/reversal only)
|
|
9
|
+
* rejected -> reviewed (via reopen only)
|
|
10
|
+
*
|
|
11
|
+
* Forbidden:
|
|
12
|
+
* rejected -> candidate (no rewinding to machine output)
|
|
13
|
+
* any status -> candidate (except initial creation)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const TRANSITIONS = {
|
|
17
|
+
candidate: new Set(['reviewed', 'accepted', 'rejected']),
|
|
18
|
+
reviewed: new Set(['accepted', 'rejected']),
|
|
19
|
+
accepted: new Set(['reviewed', 'rejected']),
|
|
20
|
+
rejected: new Set(['reviewed'])
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a status transition is lawful.
|
|
25
|
+
* @param {string} from - Current status.
|
|
26
|
+
* @param {string} to - Desired status.
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function isLawfulTransition(from, to) {
|
|
30
|
+
const allowed = TRANSITIONS[from];
|
|
31
|
+
if (!allowed) return false;
|
|
32
|
+
return allowed.has(to);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate a transition and return error if invalid.
|
|
37
|
+
* @param {string} from
|
|
38
|
+
* @param {string} to
|
|
39
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
40
|
+
*/
|
|
41
|
+
export function validateTransition(from, to) {
|
|
42
|
+
if (!TRANSITIONS[from]) {
|
|
43
|
+
return { valid: false, error: `Unknown status: "${from}"` };
|
|
44
|
+
}
|
|
45
|
+
if (!isLawfulTransition(from, to)) {
|
|
46
|
+
const allowed = [...TRANSITIONS[from]].join(', ');
|
|
47
|
+
return { valid: false, error: `Cannot transition from "${from}" to "${to}". Allowed: ${allowed}` };
|
|
48
|
+
}
|
|
49
|
+
return { valid: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Map review actions to their target statuses.
|
|
54
|
+
*/
|
|
55
|
+
export const ACTION_TARGET_STATUS = {
|
|
56
|
+
review: 'reviewed',
|
|
57
|
+
accept: 'accepted',
|
|
58
|
+
reject: 'rejected',
|
|
59
|
+
edit: null, // edit preserves current status
|
|
60
|
+
merge: 'rejected', // merged sources become rejected (merged_into_canonical)
|
|
61
|
+
reopen: 'reviewed',
|
|
62
|
+
invalidate: 'reviewed', // invalidated accepted → back to reviewed with invalidation metadata
|
|
63
|
+
supersede: 'rejected' // superseded finding becomes rejected
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Actions that require a reason.
|
|
68
|
+
*/
|
|
69
|
+
export const REASON_REQUIRED = new Set(['reject', 'invalidate', 'merge', 'supersede']);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Actions that require from_status to be accepted.
|
|
73
|
+
*/
|
|
74
|
+
export const REQUIRES_ACCEPTED = new Set(['invalidate']);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Actions that require from_status to be accepted or rejected.
|
|
78
|
+
*/
|
|
79
|
+
export const REQUIRES_CLOSED = new Set(['reopen']);
|
|
@@ -1,128 +1,137 @@
|
|
|
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
|
-
*
|
|
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
|
-
|
|
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, loadAcceptedPatternsWithSkips } from './recommendation-derivation.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive doctrine from strong accepted patterns.
|
|
18
|
+
*
|
|
19
|
+
* D2B-001 — return shape carries a `skipped: [{path, error}]` field so the
|
|
20
|
+
* silent-loader signal propagates through the derive API. A torn pattern
|
|
21
|
+
* that WOULD have been considered for doctrine synthesis no longer
|
|
22
|
+
* disappears with no signal. Legacy callers reading `doctrines` / `stats`
|
|
23
|
+
* are unaffected.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} rootDir - dogfood-labs repo root
|
|
26
|
+
* @returns {{ doctrines: Array, skipped: Array<{path: string, error: string}>, stats: { patternsConsidered: number, doctrinesEmitted: number, belowThreshold: number, patternsSkipped: number } }}
|
|
27
|
+
*/
|
|
28
|
+
export function deriveDoctrine(rootDir) {
|
|
29
|
+
const { entries, skipped } = loadAcceptedPatternsWithSkips(rootDir);
|
|
30
|
+
const patterns = entries.map(e => e.data);
|
|
31
|
+
const strong = patterns.filter(p =>
|
|
32
|
+
p.pattern_strength === 'strong' || p.pattern_strength === 'portfolio_stable'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const doctrines = [];
|
|
36
|
+
let belowThreshold = 0;
|
|
37
|
+
|
|
38
|
+
// Group strong patterns by shared doctrine theme
|
|
39
|
+
const themes = groupByDoctrineTheme(strong);
|
|
40
|
+
|
|
41
|
+
for (const [theme, themePatterns] of themes) {
|
|
42
|
+
// org_wide doctrine requires 2+ patterns
|
|
43
|
+
const maxScope = widestScope(themePatterns);
|
|
44
|
+
if (maxScope === 'org_wide' && themePatterns.length < 2) {
|
|
45
|
+
belowThreshold++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
doctrines.push(buildDoctrineCandidate(theme, themePatterns));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
doctrines,
|
|
54
|
+
skipped, // D2B-001: structured skip list for operator legibility
|
|
55
|
+
stats: {
|
|
56
|
+
patternsConsidered: patterns.length,
|
|
57
|
+
doctrinesEmitted: doctrines.length,
|
|
58
|
+
belowThreshold,
|
|
59
|
+
patternsSkipped: skipped.length
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Group patterns by doctrine theme (shared root cause family).
|
|
66
|
+
*/
|
|
67
|
+
function groupByDoctrineTheme(patterns) {
|
|
68
|
+
const themes = new Map();
|
|
69
|
+
for (const p of patterns) {
|
|
70
|
+
const rootCauses = p.dimensions?.root_cause_kinds || [];
|
|
71
|
+
const theme = rootCauses[0] || 'general';
|
|
72
|
+
if (!themes.has(theme)) themes.set(theme, []);
|
|
73
|
+
themes.get(theme).push(p);
|
|
74
|
+
}
|
|
75
|
+
return themes;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function widestScope(patterns) {
|
|
79
|
+
const order = ['repo_local', 'surface_local', 'surface_archetype', 'execution_mode', 'org_wide'];
|
|
80
|
+
let widest = 0;
|
|
81
|
+
for (const p of patterns) {
|
|
82
|
+
const idx = order.indexOf(p.transfer_scope);
|
|
83
|
+
if (idx > widest) widest = idx;
|
|
84
|
+
}
|
|
85
|
+
return order[widest];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildDoctrineCandidate(theme, patterns) {
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
const issueKinds = [...new Set(patterns.flatMap(p => p.dimensions?.issue_kinds || []))];
|
|
91
|
+
const surfaces = [...new Set(patterns.flatMap(p => p.dimensions?.product_surfaces || []))];
|
|
92
|
+
const scope = widestScope(patterns);
|
|
93
|
+
|
|
94
|
+
const kind = classifyDoctrineKind(issueKinds, theme);
|
|
95
|
+
const slug = `${theme}-${kind}`.replace(/_/g, '-');
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
schema_version: '1.0.0',
|
|
99
|
+
doctrine_id: `ddoc-${slug}`,
|
|
100
|
+
title: buildDoctrineTitle(theme, issueKinds, surfaces),
|
|
101
|
+
status: 'candidate',
|
|
102
|
+
doctrine_kind: kind,
|
|
103
|
+
statement: buildDoctrineStatement(theme, issueKinds, surfaces),
|
|
104
|
+
rationale: buildDoctrineRationale(patterns, theme),
|
|
105
|
+
based_on_pattern_ids: patterns.map(p => p.pattern_id),
|
|
106
|
+
transfer_scope: scope === 'repo_local' || scope === 'surface_local' ? 'surface_archetype' : scope,
|
|
107
|
+
strength: patterns.length >= 3 ? 'foundational' : 'proven',
|
|
108
|
+
created_at: now,
|
|
109
|
+
updated_at: now
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function classifyDoctrineKind(issueKinds, theme) {
|
|
114
|
+
if (/evidence/.test(theme) || issueKinds.some(k => /evidence/.test(k))) return 'evidence_law';
|
|
115
|
+
if (/surface|interface/.test(theme)) return 'surface_law';
|
|
116
|
+
if (/policy|calibration/.test(theme)) return 'calibration_law';
|
|
117
|
+
if (/verification|provenance/.test(theme)) return 'verification_law';
|
|
118
|
+
return 'rollout_law';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildDoctrineTitle(theme, issueKinds, surfaces) {
|
|
122
|
+
const label = theme.replace(/_/g, ' ');
|
|
123
|
+
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all surfaces';
|
|
124
|
+
return `${label}: verified rule for ${surfaceStr}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildDoctrineStatement(theme, issueKinds, surfaces) {
|
|
128
|
+
const issueLabel = issueKinds.map(k => k.replace(/_/g, ' ')).join(' and ');
|
|
129
|
+
const surfaceStr = surfaces.length ? surfaces.join(', ') : 'all product surfaces';
|
|
130
|
+
return `Verify ${issueLabel} truth before authoring rollout assumptions for ${surfaceStr}. This is a proven recurring failure class — do not skip this step.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildDoctrineRationale(patterns, theme) {
|
|
134
|
+
const findingCount = patterns.reduce((sum, p) => sum + (p.support?.finding_count || 0), 0);
|
|
135
|
+
const repoCount = patterns.reduce((sum, p) => sum + (p.support?.repo_count || 0), 0);
|
|
136
|
+
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.`;
|
|
137
|
+
}
|
package/synthesis/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
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 {
|
|
1
|
+
/**
|
|
2
|
+
* Synthesis layer exports.
|
|
3
|
+
*/
|
|
4
|
+
export { derivePatterns } from './pattern-derivation.js';
|
|
5
|
+
export { deriveRecommendations, loadAcceptedPatterns, loadAcceptedPatternsWithSkips } from './recommendation-derivation.js';
|
|
6
|
+
export { deriveDoctrine } from './doctrine-derivation.js';
|
|
7
|
+
export { validatePattern, validateRecommendation, validateDoctrine } from './validate-artifacts.js';
|
|
8
|
+
export {
|
|
9
|
+
writePattern, writeRecommendation, writeDoctrine,
|
|
10
|
+
writePatterns, writeRecommendations, writeDoctrines,
|
|
11
|
+
loadPatterns, loadRecommendations, loadDoctrines
|
|
12
|
+
} from './write-artifacts.js';
|