@dogfood-lab/findings 1.2.1
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/LICENSE +21 -0
- package/README.md +96 -0
- package/advise/advice-bundle.js +155 -0
- package/advise/index.js +5 -0
- package/advise/query.js +182 -0
- package/cli.js +936 -0
- package/derive/dedupe.js +107 -0
- package/derive/derive-findings.js +187 -0
- package/derive/ids.js +48 -0
- package/derive/index.js +9 -0
- package/derive/load-records.js +153 -0
- package/derive/rules.js +415 -0
- package/derive/write-findings.js +63 -0
- package/index.js +11 -0
- package/lib/atomic-write.js +47 -0
- package/lib/file-lock.js +359 -0
- package/lib/rename-with-retry.js +43 -0
- package/package.json +70 -0
- package/reader.js +156 -0
- package/review/event-log.js +177 -0
- package/review/index.js +6 -0
- package/review/review-engine.js +288 -0
- package/review/transitions.js +79 -0
- package/synthesis/doctrine-derivation.js +128 -0
- package/synthesis/index.js +8 -0
- package/synthesis/pattern-derivation.js +184 -0
- package/synthesis/recommendation-derivation.js +156 -0
- package/synthesis/validate-artifacts.js +46 -0
- package/synthesis/write-artifacts.js +75 -0
- package/validate.js +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mcp-tool-shop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/dogfood-lab/testing-os">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/dogfood-lab/testing-os/main/assets/logo.png" alt="testing-os" width="280">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
# @dogfood-lab/findings
|
|
8
|
+
|
|
9
|
+
> Finding contract spine for testing-os. Validates, reads, lists, and queries evidence-bound findings — the fourth contract alongside record, scenario, and policy.
|
|
10
|
+
|
|
11
|
+
Part of the [`testing-os`](https://github.com/dogfood-lab/testing-os) monorepo — the operating system for testing in the AI era.
|
|
12
|
+
|
|
13
|
+
Findings are the evidence-bound observations produced by dogfood runs. Each finding is anchored to a specific source line, carries severity (`CRITICAL` / `HIGH` / `MEDIUM` / `LOW`) and status (`open` / `fixed` / `regressed` / etc.) fields, and feeds the four-stage intelligence pipeline: **derive → review → synthesize → advise**.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @dogfood-lab/findings
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CLI
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @dogfood-lab/findings --help
|
|
25
|
+
|
|
26
|
+
# Derive findings from records
|
|
27
|
+
npx @dogfood-lab/findings derive --records-dir records/ --out findings/
|
|
28
|
+
|
|
29
|
+
# Review pipeline (apply state transitions, build event log)
|
|
30
|
+
npx @dogfood-lab/findings review --findings-dir findings/
|
|
31
|
+
|
|
32
|
+
# Synthesize patterns + recommendations + doctrine
|
|
33
|
+
npx @dogfood-lab/findings synthesize --findings-dir findings/
|
|
34
|
+
|
|
35
|
+
# Query / advise downstream consumers
|
|
36
|
+
npx @dogfood-lab/findings advise --topic <topic>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Programmatic surface
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { listFindings, readFinding } from '@dogfood-lab/findings/reader.js';
|
|
43
|
+
import { validateFinding } from '@dogfood-lab/findings/validate.js';
|
|
44
|
+
|
|
45
|
+
const findings = listFindings({ dir: './findings' });
|
|
46
|
+
const first = readFinding(findings[0].path);
|
|
47
|
+
|
|
48
|
+
const result = validateFinding(first);
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
console.error(result.errors);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Pipeline stages
|
|
55
|
+
|
|
56
|
+
| Stage | Module | Output |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| Derive | `derive/derive-findings.js` | New finding files under `findings/`; deduplication via `derive/dedupe.js` |
|
|
59
|
+
| Review | `review/review-engine.js` | Status transitions + `event-log.jsonl` audit trail |
|
|
60
|
+
| Synthesize | `synthesis/pattern-derivation.js`, `synthesis/recommendation-derivation.js`, `synthesis/doctrine-derivation.js` | Pattern, recommendation, doctrine artifacts |
|
|
61
|
+
| Advise | `advise/advice-bundle.js`, `advise/query.js` | Advisory bundles for downstream consumers (e.g., `@dogfood-lab/dogfood-swarm`) |
|
|
62
|
+
|
|
63
|
+
## Finding shape
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
finding_id: F-XXXXXX-XXX
|
|
67
|
+
severity: HIGH
|
|
68
|
+
status: open
|
|
69
|
+
title: <short imperative-mood phrase>
|
|
70
|
+
evidence:
|
|
71
|
+
source_pin: { file: <path>, line: <n>, snippet: <verbatim> }
|
|
72
|
+
test_pin: { file: <path>, line: <n>, name: <test-name> }
|
|
73
|
+
remediation:
|
|
74
|
+
approach: <strategy>
|
|
75
|
+
rationale: <why>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Full shape: `@dogfood-lab/schemas/json/dogfood-finding.schema.json`.
|
|
79
|
+
|
|
80
|
+
## Atomic I/O (shared discipline)
|
|
81
|
+
|
|
82
|
+
Internal helpers under `lib/`:
|
|
83
|
+
|
|
84
|
+
- `atomic-write.js` — two-phase commit for finding-file writes (`writeFileSync` to shadow → `renameSync` to canonical)
|
|
85
|
+
- `file-lock.js` — cross-process advisory lock via `linkSync` CAS, Windows-compatible
|
|
86
|
+
- `rename-with-retry.js` — bounded retry on EPERM/EBUSY (Windows AV scanner handle-release window)
|
|
87
|
+
|
|
88
|
+
These helpers are intentionally shared cross-package discipline. They're exported via the `./lib/*` subpath so `@dogfood-lab/dogfood-swarm` and `@dogfood-lab/ingest` can reuse the same atomic-write semantics. The CLAUDE.md in the repo root documents the cycle this creates and why it's accepted.
|
|
89
|
+
|
|
90
|
+
## Docs
|
|
91
|
+
|
|
92
|
+
📖 Full handbook: **<https://dogfood-lab.github.io/testing-os/handbook/>**
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT © 2026 mcp-tool-shop
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advice bundle synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Produces structured guidance bundles for future projects
|
|
5
|
+
* based on surface, execution mode, and archetype.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
queryFindings,
|
|
10
|
+
queryPatterns,
|
|
11
|
+
queryRecommendations,
|
|
12
|
+
queryDoctrine,
|
|
13
|
+
queryFailureClasses
|
|
14
|
+
} from './query.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate an advice bundle for a given scope.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} rootDir - dogfood-labs repo root
|
|
20
|
+
* @param {object} scope
|
|
21
|
+
* @param {string} [scope.surface] - product surface
|
|
22
|
+
* @param {string} [scope.executionMode] - bot/human/mixed
|
|
23
|
+
* @param {string} [scope.repo] - specific repo
|
|
24
|
+
* @param {string} [scope.journeyStage] - journey stage filter
|
|
25
|
+
* @param {string} [scope.issueKind] - issue kind filter
|
|
26
|
+
* @returns {object} Structured advice bundle
|
|
27
|
+
*/
|
|
28
|
+
export function generateAdviceBundle(rootDir, scope = {}) {
|
|
29
|
+
const recommendations = queryRecommendations(rootDir, scope);
|
|
30
|
+
const doctrine = queryDoctrine(rootDir, scope);
|
|
31
|
+
const patterns = queryPatterns(rootDir, scope);
|
|
32
|
+
const failureClasses = queryFailureClasses(rootDir, scope);
|
|
33
|
+
const findings = queryFindings(rootDir, { ...scope, limit: 8 });
|
|
34
|
+
|
|
35
|
+
// Categorize recommendations by kind
|
|
36
|
+
const starterChecks = recommendations.filter(r =>
|
|
37
|
+
r.recommendation_kind === 'starter_check' || r.recommendation_kind === 'starter_scenario'
|
|
38
|
+
);
|
|
39
|
+
const evidenceExpectations = recommendations.filter(r =>
|
|
40
|
+
r.recommendation_kind === 'evidence_expectation' || r.recommendation_kind === 'policy_seed'
|
|
41
|
+
);
|
|
42
|
+
const verificationRules = recommendations.filter(r =>
|
|
43
|
+
r.recommendation_kind === 'verification_rule' || r.recommendation_kind === 'review_prompt'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
query: {
|
|
48
|
+
product_surface: scope.surface || null,
|
|
49
|
+
execution_mode: scope.executionMode || null,
|
|
50
|
+
repo: scope.repo || null
|
|
51
|
+
},
|
|
52
|
+
advice: {
|
|
53
|
+
starter_checks: starterChecks.map(r => ({
|
|
54
|
+
id: r.recommendation_id,
|
|
55
|
+
title: r.title,
|
|
56
|
+
action: r.action,
|
|
57
|
+
confidence: r.confidence
|
|
58
|
+
})),
|
|
59
|
+
evidence_expectations: evidenceExpectations.map(r => ({
|
|
60
|
+
id: r.recommendation_id,
|
|
61
|
+
title: r.title,
|
|
62
|
+
action: r.action,
|
|
63
|
+
confidence: r.confidence
|
|
64
|
+
})),
|
|
65
|
+
verification_rules: verificationRules.map(r => ({
|
|
66
|
+
id: r.recommendation_id,
|
|
67
|
+
title: r.title,
|
|
68
|
+
action: r.action,
|
|
69
|
+
confidence: r.confidence
|
|
70
|
+
})),
|
|
71
|
+
likely_failure_classes: failureClasses,
|
|
72
|
+
relevant_doctrine: doctrine.map(d => ({
|
|
73
|
+
id: d.doctrine_id,
|
|
74
|
+
statement: d.statement,
|
|
75
|
+
strength: d.strength,
|
|
76
|
+
kind: d.doctrine_kind
|
|
77
|
+
}))
|
|
78
|
+
},
|
|
79
|
+
support: {
|
|
80
|
+
pattern_ids: patterns.map(p => p.pattern_id),
|
|
81
|
+
finding_ids: findings.map(f => f.finding_id),
|
|
82
|
+
pattern_count: patterns.length,
|
|
83
|
+
finding_count: findings.length,
|
|
84
|
+
recommendation_count: recommendations.length,
|
|
85
|
+
doctrine_count: doctrine.length
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate a sync-friendly export of all accepted artifacts.
|
|
92
|
+
* For repo-knowledge consumption.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} rootDir
|
|
95
|
+
* @returns {object} Export bundle with all accepted artifacts and lineage
|
|
96
|
+
*/
|
|
97
|
+
export function generateSyncExport(rootDir) {
|
|
98
|
+
const findings = queryFindings(rootDir, { limit: 500 });
|
|
99
|
+
const patterns = queryPatterns(rootDir, { limit: 100 });
|
|
100
|
+
const recommendations = queryRecommendations(rootDir, { limit: 100 });
|
|
101
|
+
const doctrine = queryDoctrine(rootDir, { limit: 100 });
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
exported_at: new Date().toISOString(),
|
|
105
|
+
source: 'dogfood-labs',
|
|
106
|
+
counts: {
|
|
107
|
+
findings: findings.length,
|
|
108
|
+
patterns: patterns.length,
|
|
109
|
+
recommendations: recommendations.length,
|
|
110
|
+
doctrine: doctrine.length
|
|
111
|
+
},
|
|
112
|
+
findings: findings.map(f => ({
|
|
113
|
+
finding_id: f.finding_id,
|
|
114
|
+
title: f.title,
|
|
115
|
+
repo: f.repo,
|
|
116
|
+
product_surface: f.product_surface,
|
|
117
|
+
issue_kind: f.issue_kind,
|
|
118
|
+
root_cause_kind: f.root_cause_kind,
|
|
119
|
+
remediation_kind: f.remediation_kind,
|
|
120
|
+
transfer_scope: f.transfer_scope,
|
|
121
|
+
summary: f.summary,
|
|
122
|
+
doctrine_statement: f.doctrine_statement
|
|
123
|
+
})),
|
|
124
|
+
patterns: patterns.map(p => ({
|
|
125
|
+
pattern_id: p.pattern_id,
|
|
126
|
+
title: p.title,
|
|
127
|
+
pattern_kind: p.pattern_kind,
|
|
128
|
+
pattern_strength: p.pattern_strength,
|
|
129
|
+
transfer_scope: p.transfer_scope,
|
|
130
|
+
summary: p.summary,
|
|
131
|
+
source_finding_ids: p.source_finding_ids,
|
|
132
|
+
dimensions: p.dimensions,
|
|
133
|
+
support: p.support
|
|
134
|
+
})),
|
|
135
|
+
recommendations: recommendations.map(r => ({
|
|
136
|
+
recommendation_id: r.recommendation_id,
|
|
137
|
+
title: r.title,
|
|
138
|
+
recommendation_kind: r.recommendation_kind,
|
|
139
|
+
confidence: r.confidence,
|
|
140
|
+
applies_to: r.applies_to,
|
|
141
|
+
action: r.action,
|
|
142
|
+
based_on_pattern_ids: r.based_on_pattern_ids
|
|
143
|
+
})),
|
|
144
|
+
doctrine: doctrine.map(d => ({
|
|
145
|
+
doctrine_id: d.doctrine_id,
|
|
146
|
+
title: d.title,
|
|
147
|
+
doctrine_kind: d.doctrine_kind,
|
|
148
|
+
strength: d.strength,
|
|
149
|
+
statement: d.statement,
|
|
150
|
+
rationale: d.rationale,
|
|
151
|
+
transfer_scope: d.transfer_scope,
|
|
152
|
+
based_on_pattern_ids: d.based_on_pattern_ids
|
|
153
|
+
}))
|
|
154
|
+
};
|
|
155
|
+
}
|
package/advise/index.js
ADDED
package/advise/query.js
ADDED
|
@@ -0,0 +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
|
+
}
|