@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 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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Advise layer exports.
3
+ */
4
+ export { queryFindings, queryPatterns, queryRecommendations, queryDoctrine, queryFailureClasses } from './query.js';
5
+ export { generateAdviceBundle, generateSyncExport } from './advice-bundle.js';
@@ -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
+ }