qa-boot 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/cli/commands/generate.js +7 -0
  4. package/dist/cli/commands/init.js +24 -0
  5. package/dist/cli/commands/scan.js +30 -0
  6. package/dist/cli/commands/tell.js +27 -0
  7. package/dist/cli/index.js +67 -0
  8. package/dist/cli/version.js +5 -0
  9. package/dist/config/config.js +34 -0
  10. package/dist/core/fact-store.js +50 -0
  11. package/dist/core/fact.js +30 -0
  12. package/dist/core/fs-utils.js +39 -0
  13. package/dist/core/scan-context.js +13 -0
  14. package/dist/core/slug.js +8 -0
  15. package/dist/domains/agent-facts.js +31 -0
  16. package/dist/domains/build-facts.js +31 -0
  17. package/dist/domains/ci-facts.js +43 -0
  18. package/dist/domains/docs-facts.js +28 -0
  19. package/dist/domains/repo-facts.js +52 -0
  20. package/dist/domains/repo-quality-facts.js +34 -0
  21. package/dist/domains/test-facts.js +65 -0
  22. package/dist/generate/claude-qa.js +20 -0
  23. package/dist/generate/domain-summaries.js +20 -0
  24. package/dist/generate/generate.js +68 -0
  25. package/dist/generate/known-unknown.js +33 -0
  26. package/dist/generate/skills.js +83 -0
  27. package/dist/generate/special-renderers.js +99 -0
  28. package/dist/maturity/rubric.js +114 -0
  29. package/dist/pipeline/run-scan.js +53 -0
  30. package/dist/providers/evidence-provider.js +1 -0
  31. package/dist/providers/qaradar-contract.js +1 -0
  32. package/dist/providers/qaradar-parse.js +51 -0
  33. package/dist/providers/qaradar-provider.js +30 -0
  34. package/dist/scanners/agent-config-scanner.js +23 -0
  35. package/dist/scanners/build-scanner.js +27 -0
  36. package/dist/scanners/ci-scanner.js +51 -0
  37. package/dist/scanners/docs-scanner.js +24 -0
  38. package/dist/scanners/raw-evidence.js +1 -0
  39. package/dist/scanners/repo-scanner.js +41 -0
  40. package/dist/scanners/test-scanner.js +85 -0
  41. package/dist/tell/interview-log.js +22 -0
  42. package/dist/tell/tell.js +77 -0
  43. package/dist/unknowns/unknowns.js +101 -0
  44. package/package.json +44 -0
@@ -0,0 +1,34 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ export function repoQualityFacts(results, today) {
3
+ const rq = results.find((r) => r.domain === "repo_quality");
4
+ if (!rq)
5
+ return [];
6
+ const facts = [
7
+ makeFact({
8
+ id: "repo_quality.high-churn-untested",
9
+ domain: "repo_quality",
10
+ statement: rq.statement,
11
+ provenance: "observed",
12
+ confidence: rq.confidence,
13
+ evidence_provider: rq.provider,
14
+ evidence_command: "qaradar analyze . --json-output",
15
+ evidence: rq.evidence,
16
+ value: rq.value,
17
+ limitations: rq.limitations ?? [],
18
+ risk_if_wrong: "The agent may prioritize technically risky files that are not the most business-critical.",
19
+ }, today),
20
+ makeFact({
21
+ id: "business_priority.vs-repo-risk",
22
+ domain: "business_priority",
23
+ statement: "Repo risk is known technically, but business criticality of those files is unknown.",
24
+ provenance: "unknown",
25
+ confidence: 0,
26
+ evidence_provider: "qaradar",
27
+ evidence: [],
28
+ value: { question: "Which of the technically risky files are business-critical or customer-facing?" },
29
+ risk_if_wrong: "The agent may treat technical risk as business priority.",
30
+ needs_human_confirmation: true,
31
+ }, today),
32
+ ];
33
+ return facts;
34
+ }
@@ -0,0 +1,65 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "test-scanner";
3
+ export function testFacts(ev, today) {
4
+ const facts = [];
5
+ for (const e of ev.filter((x) => x.kind === "test-framework")) {
6
+ const name = String(e.detail?.name);
7
+ const observed = e.detail?.source === "config";
8
+ facts.push(makeFact({
9
+ id: `test.framework.${name}`,
10
+ domain: "test",
11
+ statement: `Test framework ${name} detected.`,
12
+ provenance: observed ? "observed" : "inferred",
13
+ confidence: observed ? 0.8 : 0.5,
14
+ evidence_provider: PROVIDER,
15
+ evidence: e.path ? [e.path] : [],
16
+ limitations: observed ? [] : ["Detected via dependency only; not confirmed running."],
17
+ }, today));
18
+ }
19
+ const dirs = ev.filter((x) => x.kind === "test-dir");
20
+ if (dirs.length) {
21
+ facts.push(makeFact({
22
+ id: "test.directory",
23
+ domain: "test",
24
+ statement: "Test directories are present.",
25
+ provenance: "observed",
26
+ confidence: 0.8,
27
+ evidence_provider: PROVIDER,
28
+ evidence: dirs.map((d) => d.path).filter(Boolean),
29
+ }, today));
30
+ }
31
+ const cov = ev.filter((x) => x.kind === "coverage-tool");
32
+ if (cov.length) {
33
+ facts.push(makeFact({
34
+ id: "test.coverage-tool",
35
+ domain: "test",
36
+ statement: "A coverage tool/artifact was detected.",
37
+ provenance: "observed",
38
+ confidence: 0.8,
39
+ evidence_provider: PROVIDER,
40
+ evidence: cov.map((c) => String(c.detail?.name)),
41
+ limitations: ["Coverage freshness is unknown."],
42
+ }, today));
43
+ }
44
+ return facts;
45
+ }
46
+ export function qaradarTestFacts(results, today) {
47
+ const t = results.find((r) => r.domain === "test");
48
+ if (!t)
49
+ return [];
50
+ const v = t.value;
51
+ return [
52
+ makeFact({
53
+ id: "test.coverage-shape",
54
+ domain: "test",
55
+ statement: `QA Radar: ${v.files_with_tests}/${v.source_files} source files have mapped tests (ratio ${v.test_to_source_ratio}).`,
56
+ provenance: "observed",
57
+ confidence: t.confidence,
58
+ evidence_provider: t.provider,
59
+ evidence_command: "qaradar analyze . --json-output",
60
+ evidence: t.evidence,
61
+ value: t.value,
62
+ limitations: t.limitations ?? [],
63
+ }, today),
64
+ ];
65
+ }
@@ -0,0 +1,20 @@
1
+ export function renderClaudeQa(opts) {
2
+ const lines = [
3
+ "# QA Context Instructions",
4
+ "",
5
+ "Before answering QA/testing/release questions, check the files in `qa-context/`.",
6
+ "",
7
+ "Important:",
8
+ "- Do not assume missing build, release, environment, or ownership knowledge.",
9
+ "- If a required fact is marked unknown in `qa-context/unknowns.md`, say so and ask the human question listed there.",
10
+ "- Do not treat technical repo risk as business priority unless confirmed in `qa-context/product-risk-map.md`.",
11
+ "- Do not suggest external writes (PR comments, tickets, CI triggers) unless explicitly approved by the user.",
12
+ "- Facts marked _told_ were stated by a human (see `qa-context/qa-interview-log.md`). Prefer them over inferred facts; if marked stale, re-confirm via the question in `unknowns.md` instead of assuming.",
13
+ ];
14
+ if (opts.hasRepoRisk) {
15
+ lines.push("- Use `qa-context/repo-risk.md` for technical repo risk when asked what to test first.");
16
+ }
17
+ lines.push("", "Getting started (new QA or automation engineers):", "- If the user asks how or where to start with QA in this repo, walk them through `qa-context/` (start with `maturity.md`, then `unknowns.md`, then `test-stack.md`) before giving generic advice.", "- If `qa-context/unknowns.md` has open questions, offer the `qa-onboard` skill to capture answers. Only do this when the user asks how to get started or an unknown blocks the current task. Do not start onboarding proactively in every session.");
18
+ lines.push("", "These files are generated by `qa-boot` from `qa-context/facts.json`. Do not hand-edit them.", "");
19
+ return lines.join("\n");
20
+ }
@@ -0,0 +1,20 @@
1
+ import { renderKnownUnknown } from "./known-unknown.js";
2
+ const SPECS = [
3
+ { file: "qa-context/test-stack.md", title: "Test Stack", purpose: "What testing tooling this repo uses.", domains: ["test", "test_trust"] },
4
+ { file: "qa-context/build-and-run.md", title: "Build and Run", purpose: "How to build and run this project.", domains: ["build"] },
5
+ { file: "qa-context/ci-and-release.md", title: "CI and Release", purpose: "Continuous integration and release signals.", domains: ["ci", "release"] },
6
+ { file: "qa-context/environments.md", title: "Environments", purpose: "Test environments and their stability.", domains: ["environment"] },
7
+ { file: "qa-context/test-data.md", title: "Test Data", purpose: "How test data is created and reset.", domains: ["test_data"] },
8
+ { file: "qa-context/knowledge-sources.md", title: "Knowledge Sources", purpose: "Where project knowledge lives.", domains: ["knowledge_sources"] },
9
+ { file: "qa-context/product-risk-map.md", title: "Product Risk Map", purpose: "Business priority of repo areas (usually unknown in V0).", domains: ["business_priority"] },
10
+ { file: "qa-context/ownership.md", title: "Ownership", purpose: "Who owns and approves quality-relevant changes.", domains: ["ownership"] },
11
+ { file: "qa-context/agent-permissions.md", title: "Agent Permissions", purpose: "What an AI agent is allowed to do in this repo.", domains: ["agent_permissions"] },
12
+ ];
13
+ export function renderDomainSummaries(facts) {
14
+ const out = {};
15
+ for (const spec of SPECS) {
16
+ const subset = facts.filter((f) => spec.domains.includes(f.domain));
17
+ out[spec.file] = renderKnownUnknown(spec.title, spec.purpose, subset);
18
+ }
19
+ return out;
20
+ }
@@ -0,0 +1,68 @@
1
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { renderDomainSummaries } from "./domain-summaries.js";
4
+ import { renderUnknowns, renderRepoRisk, renderMaturity, renderQualityRisks, renderRefreshPolicy } from "./special-renderers.js";
5
+ import { renderClaudeQa } from "./claude-qa.js";
6
+ import { renderSkills } from "./skills.js";
7
+ function writeFile(repoPath, rel, content) {
8
+ const abs = join(repoPath, rel);
9
+ mkdirSync(dirname(abs), { recursive: true });
10
+ writeFileSync(abs, content, "utf8");
11
+ }
12
+ export function loadFacts(repoPath) {
13
+ const path = join(repoPath, "qa-context", "facts.json");
14
+ if (!existsSync(path)) {
15
+ throw new Error("qa-context/facts.json not found. Run `qa-boot scan` first.");
16
+ }
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(readFileSync(path, "utf8"));
20
+ }
21
+ catch {
22
+ throw new Error("qa-context/facts.json is malformed. Re-run `qa-boot scan` to regenerate it.");
23
+ }
24
+ return parsed.facts ?? [];
25
+ }
26
+ export function runGenerate(opts) {
27
+ const facts = loadFacts(opts.repoPath);
28
+ const written = [];
29
+ const emit = (rel, content) => {
30
+ writeFile(opts.repoPath, rel, content);
31
+ written.push(rel);
32
+ };
33
+ for (const [rel, content] of Object.entries(renderDomainSummaries(facts)))
34
+ emit(rel, content);
35
+ emit("qa-context/unknowns.md", renderUnknowns(facts));
36
+ emit("qa-context/maturity.md", renderMaturity(facts));
37
+ emit("qa-context/quality-risks.md", renderQualityRisks(facts));
38
+ emit("qa-context/refresh-policy.md", renderRefreshPolicy(opts.config));
39
+ const repoRisk = renderRepoRisk(facts);
40
+ const hasRepoRisk = repoRisk !== null;
41
+ if (repoRisk)
42
+ emit("qa-context/repo-risk.md", repoRisk);
43
+ if (opts.claude && opts.config.claude.enabled) {
44
+ emit("CLAUDE.qa.md", renderClaudeQa({ hasRepoRisk }));
45
+ ensureClaudeMdImport(opts.repoPath, written);
46
+ if (opts.config.claude.generate_skills) {
47
+ for (const [rel, content] of Object.entries(renderSkills({ hasRepoRisk })))
48
+ emit(rel, content);
49
+ }
50
+ }
51
+ return written;
52
+ }
53
+ const CLAUDE_QA_IMPORT = "@CLAUDE.qa.md";
54
+ /** Claude Code only auto-loads CLAUDE.md (and its @imports), so the generated
55
+ * CLAUDE.qa.md must be referenced from there to enter agent context. */
56
+ function ensureClaudeMdImport(repoPath, written) {
57
+ const path = join(repoPath, "CLAUDE.md");
58
+ if (!existsSync(path)) {
59
+ writeFileSync(path, `# Project instructions\n\n${CLAUDE_QA_IMPORT}\n`, "utf8");
60
+ written.push("CLAUDE.md");
61
+ return;
62
+ }
63
+ const current = readFileSync(path, "utf8");
64
+ if (!current.includes(CLAUDE_QA_IMPORT)) {
65
+ writeFileSync(path, current.trimEnd() + `\n\n${CLAUDE_QA_IMPORT}\n`, "utf8");
66
+ written.push("CLAUDE.md");
67
+ }
68
+ }
@@ -0,0 +1,33 @@
1
+ export function renderKnownUnknown(title, purpose, facts) {
2
+ const known = facts.filter((f) => f.provenance !== "unknown");
3
+ const unknown = facts.filter((f) => f.provenance === "unknown");
4
+ const lines = [`# ${title}`, "", purpose, ""];
5
+ if (known.length === 0 && unknown.length > 0) {
6
+ lines.push("## Unknown", "");
7
+ for (const f of unknown)
8
+ lines.push(...unknownLines(f));
9
+ return lines.join("\n").trimEnd() + "\n";
10
+ }
11
+ if (known.length) {
12
+ lines.push("## Known", "");
13
+ for (const f of known) {
14
+ const ev = f.evidence.length ? ` (evidence: ${f.evidence.map((e) => `\`${e}\``).join(", ")})` : "";
15
+ const stale = f.stale ? " _[stale]_" : "";
16
+ lines.push(`- ${f.statement} — _${f.provenance}, confidence ${f.confidence}_${ev}${stale}`);
17
+ }
18
+ lines.push("");
19
+ }
20
+ if (unknown.length) {
21
+ lines.push("## Unknown", "");
22
+ for (const f of unknown)
23
+ lines.push(...unknownLines(f));
24
+ }
25
+ return lines.join("\n").trimEnd() + "\n";
26
+ }
27
+ function unknownLines(f) {
28
+ const q = f.value?.question;
29
+ const out = [`- ${f.statement}`];
30
+ if (q)
31
+ out.push(` - **Ask a human:** ${q}`);
32
+ return out;
33
+ }
@@ -0,0 +1,83 @@
1
+ function skillFile(def) {
2
+ return `---\nname: ${def.name}\ndescription: ${def.description}\n---\n\n${def.body}\n`;
3
+ }
4
+ export function renderSkills(opts) {
5
+ const qaRiskBody = opts.hasRepoRisk
6
+ ? [
7
+ "Answer repo-risk questions using deterministic evidence from QA Radar.",
8
+ "",
9
+ "Rules:",
10
+ "- Use `qa-context/repo-risk.md`.",
11
+ "- Do not recalculate churn or coverage manually unless data is missing.",
12
+ "- Do not confuse technical risk with business priority.",
13
+ "- Mention limitations explicitly.",
14
+ ].join("\n")
15
+ : [
16
+ "Answer repo-risk questions from deterministic evidence.",
17
+ "",
18
+ "QA Radar data is **not available** in this repo's context (`repo-risk.md` was not generated).",
19
+ "Say so plainly and suggest running `qa-boot scan --with-qaradar`. Do not invent risk rankings.",
20
+ ].join("\n");
21
+ const defs = [
22
+ {
23
+ name: "qa-context",
24
+ description: "Load and summarize the current QA context for this repo.",
25
+ body: [
26
+ "Load and summarize the QA context QA Boot generated.",
27
+ "",
28
+ "Rules:",
29
+ "- Read `qa-context/`.",
30
+ "- Separate observed, inferred, and unknown facts.",
31
+ "- Mention any facts marked stale.",
32
+ ].join("\n"),
33
+ },
34
+ {
35
+ name: "qa-unknowns",
36
+ description: "Prevent guessing — surface the exact question to ask a human.",
37
+ body: [
38
+ "Prevent the agent from guessing about QA/build/release/ownership.",
39
+ "",
40
+ "Rules:",
41
+ "- Check `qa-context/unknowns.md`.",
42
+ "- If required knowledge is unknown, say so clearly.",
43
+ "- Surface the exact 'Ask a human' question rather than inventing an answer.",
44
+ ].join("\n"),
45
+ },
46
+ {
47
+ name: "qa-risk",
48
+ description: "Answer 'what should I test first' from deterministic repo risk.",
49
+ body: qaRiskBody,
50
+ },
51
+ {
52
+ name: "qa-release-readiness",
53
+ description: "Draft release-readiness guidance from known CI/release facts.",
54
+ body: [
55
+ "Draft release-readiness guidance based on known facts.",
56
+ "",
57
+ "Rules:",
58
+ "- Use known CI/test/release facts from `qa-context/ci-and-release.md`.",
59
+ "- Mention unknown release blockers from `qa-context/unknowns.md`.",
60
+ "- Do not approve a release. Do not invent release gates.",
61
+ ].join("\n"),
62
+ },
63
+ {
64
+ name: "qa-onboard",
65
+ description: "Interview a human to capture QA knowledge as told facts via qa-boot tell.",
66
+ body: [
67
+ "Interview the human to convert QA unknowns into recorded told facts.",
68
+ "",
69
+ "Phases:",
70
+ "1. Precondition: if `qa-context/` or `qa-context/unknowns.md` is missing, say so and suggest `qa-boot scan`. Do not improvise context.",
71
+ "2. Gather: if you lack baseline understanding of this repo, build it first — README, docs, repo structure, `qa-context/*.md`, `repo-risk.md` if present. Do not interview from ignorance.",
72
+ "3. Play back: summarize your understanding to the human and let them correct it. Corrections are capturable answers.",
73
+ "4. Interview: ask the open unknowns from `qa-context/unknowns.md` one question at a time, anchored in specifics you gathered. Then invite domain knowledge (environments, ownership, release, test data, business priority).",
74
+ '5. Capture: after each human answer run `qa-boot tell <unknown-id> "<answer>" --by <name>` (or `qa-boot tell "<statement>" --domain <domain> --by <name>` for knowledge with no matching unknown). Record only what the human said or explicitly confirmed — never your own inference.',
75
+ "6. Wrap: summarize what was captured, list remaining unknowns, and suggest `qa-boot scan` to refresh the generated context files.",
76
+ ].join("\n"),
77
+ },
78
+ ];
79
+ const out = {};
80
+ for (const def of defs)
81
+ out[`.claude/skills/${def.name}/SKILL.md`] = skillFile(def);
82
+ return out;
83
+ }
@@ -0,0 +1,99 @@
1
+ export function renderUnknowns(facts) {
2
+ const unknowns = facts.filter((f) => f.provenance === "unknown");
3
+ const lines = ["# Unknowns", "", "Knowledge QA Boot could not determine. Ask a human before assuming.", ""];
4
+ const staleAnswers = new Map(facts
5
+ .filter((f) => f.provenance === "told" && f.stale && f.answers_unknown)
6
+ .map((f) => [f.answers_unknown, f]));
7
+ const byDomain = new Map();
8
+ for (const f of unknowns) {
9
+ const list = byDomain.get(f.domain) ?? [];
10
+ list.push(f);
11
+ byDomain.set(f.domain, list);
12
+ }
13
+ for (const domain of [...byDomain.keys()].sort()) {
14
+ lines.push(`## ${domain}`, "");
15
+ for (const f of byDomain.get(domain)) {
16
+ lines.push(`- ${f.statement}`);
17
+ const q = f.value?.question;
18
+ if (q)
19
+ lines.push(` - Ask a human: ${q}`);
20
+ const prev = staleAnswers.get(f.id);
21
+ if (prev) {
22
+ lines.push(` - Previously answered ${prev.last_verified} by ${prev.told_by ?? "unknown"}: "${prev.statement}" — please re-confirm or update via \`qa-boot tell\`.`);
23
+ }
24
+ }
25
+ lines.push("");
26
+ }
27
+ return lines.join("\n").trimEnd() + "\n";
28
+ }
29
+ export function renderRepoRisk(facts) {
30
+ const rq = facts.find((f) => f.id === "repo_quality.high-churn-untested");
31
+ if (!rq)
32
+ return null;
33
+ const v = rq.value;
34
+ const lines = [
35
+ "# Repository Risk Summary",
36
+ "",
37
+ "Generated from deterministic repo analysis (QA Radar).",
38
+ "",
39
+ `Critical: ${v.critical_count} · High: ${v.high_count}`,
40
+ "",
41
+ "## Highest-risk areas",
42
+ "",
43
+ ];
44
+ v.top_risky.forEach((m, i) => {
45
+ lines.push(`${i + 1}. \`${m.path}\` (${m.risk})`);
46
+ for (const r of m.reasons)
47
+ lines.push(` - ${r}`);
48
+ });
49
+ lines.push("", "## Important limitation", "", "This is a technical risk view. It does not know business priority, customer impact, ownership, or release criticality unless captured elsewhere in QA context.");
50
+ return lines.join("\n").trimEnd() + "\n";
51
+ }
52
+ const TITLE = {
53
+ discoverability: "Discoverability",
54
+ test_signal: "Test Signal",
55
+ trust: "Trust",
56
+ release_readiness: "Release Readiness",
57
+ quality_ownership: "Quality Ownership",
58
+ agent_readiness: "Agent Readiness",
59
+ };
60
+ export function renderMaturity(facts) {
61
+ const dims = facts.filter((f) => f.domain === "maturity");
62
+ const lines = ["# Maturity", "", "Deterministic rubric scores (0–5).", ""];
63
+ for (const f of dims) {
64
+ const v = f.value;
65
+ const key = f.id.split(".")[1];
66
+ lines.push(`## ${TITLE[key] ?? key}: ${v.score}/5`, "", v.explanation, "");
67
+ if (v.evidence.length)
68
+ lines.push("Evidence:", ...v.evidence.map((e) => `- \`${e}\``), "");
69
+ if (v.unknowns.length)
70
+ lines.push("Unknowns:", ...v.unknowns.map((u) => `- ${u}`), "");
71
+ lines.push(`Next step:`, v.next_step, "");
72
+ }
73
+ return lines.join("\n").trimEnd() + "\n";
74
+ }
75
+ export function renderQualityRisks(facts) {
76
+ const lines = ["# Quality Risks", "", "Synthesis of technical repo risk and explicit unknowns.", ""];
77
+ const rq = facts.find((f) => f.id === "repo_quality.high-churn-untested");
78
+ if (rq)
79
+ lines.push("- Technical repo risk is available — see `repo-risk.md`.");
80
+ else
81
+ lines.push("- No QA Radar data; technical repo risk is unknown.");
82
+ for (const f of facts.filter((x) => x.domain === "repo_quality" && x.provenance === "told")) {
83
+ lines.push(`- ${f.statement} — _told by ${f.told_by ?? "unknown"}_`);
84
+ }
85
+ const unknownCount = facts.filter((f) => f.provenance === "unknown").length;
86
+ lines.push(`- ${unknownCount} important unknowns recorded — see \`unknowns.md\`.`, "");
87
+ return lines.join("\n").trimEnd() + "\n";
88
+ }
89
+ export function renderRefreshPolicy(config) {
90
+ return [
91
+ "# Refresh Policy",
92
+ "",
93
+ `Facts expire after ${config.refresh.default_days} days and are then marked stale.`,
94
+ "",
95
+ "Re-run `qa-boot scan` to refresh facts. (V0 marks time-expired facts stale; the",
96
+ "refresh diff and drop-on-absence rules arrive in V1.)",
97
+ "",
98
+ ].join("\n");
99
+ }
@@ -0,0 +1,114 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "maturity-rubric";
3
+ export function scoreMaturity(facts, today) {
4
+ const ids = facts.map((f) => f.id);
5
+ const toldAnswers = new Set(facts
6
+ .filter((f) => f.provenance === "told" && f.answers_unknown && !f.stale)
7
+ .map((f) => f.answers_unknown));
8
+ const boost = (score, unknownId) => toldAnswers.has(unknownId) ? Math.min(5, score + 2) : score;
9
+ const dims = [];
10
+ // Discoverability
11
+ {
12
+ const signals = [
13
+ ids.includes("repo.readme"),
14
+ ids.some((i) => i.startsWith("build.command.") || i.startsWith("build.tool.")),
15
+ ids.some((i) => i.startsWith("ci.system.")),
16
+ ids.some((i) => i.startsWith("knowledge_sources.docs-dir") || i.startsWith("knowledge_sources.contributing")),
17
+ ids.some((i) => i.startsWith("agent_permissions.config.")),
18
+ ];
19
+ const n = signals.filter(Boolean).length;
20
+ const score = signals[0] && signals[1] && signals[2] ? (n >= 5 ? 5 : 3) : signals[0] ? 1 : 0;
21
+ dims.push({
22
+ id: "maturity.discoverability",
23
+ score,
24
+ explanation: "Based on README, build files, CI config, docs, and agent config presence.",
25
+ evidence: ids.filter((i) => i === "repo.readme" || i.startsWith("build.command.") || i.startsWith("build.tool.") || i.startsWith("ci.system.")),
26
+ unknowns: n >= 5 ? [] : ["Onboarding docs and/or agent config may be missing."],
27
+ next_step: "Ensure a README, build instructions, and CI are discoverable.",
28
+ });
29
+ }
30
+ // Test signal
31
+ {
32
+ const shape = facts.find((f) => f.id === "test.coverage-shape")?.value;
33
+ const hasTests = ids.some((i) => i.startsWith("test.framework.")) ||
34
+ ids.includes("test.directory") ||
35
+ (shape?.files_with_tests ?? 0) > 0;
36
+ const runsTests = ids.includes("ci.runs-tests");
37
+ const coverage = ids.includes("test.coverage-tool") || shape?.coverage_status === "ok";
38
+ const score = !hasTests ? 0 : coverage && runsTests ? 5 : runsTests ? 3 : 2;
39
+ dims.push({
40
+ id: "maturity.test_signal",
41
+ score,
42
+ explanation: "Based on test presence, CI running tests, and coverage artifacts.",
43
+ evidence: ids.filter((i) => i.startsWith("test.")),
44
+ unknowns: coverage ? [] : ["Coverage freshness is unknown."],
45
+ next_step: "Confirm tests run in CI and whether coverage is current.",
46
+ });
47
+ }
48
+ // Trust (capped at 2 in V0 — freshness is told knowledge)
49
+ {
50
+ // test.coverage-shape is presence, not freshness — excluded from trust by design
51
+ const coverage = ids.includes("test.coverage-tool");
52
+ const score = boost(coverage ? 2 : 0, "test_trust.confidence");
53
+ dims.push({
54
+ id: "maturity.trust",
55
+ score,
56
+ explanation: "Trust depends on coverage freshness and flake handling, which are unknown in V0.",
57
+ evidence: coverage ? ["test.coverage-tool"] : [],
58
+ unknowns: ["Coverage freshness unknown.", "Flake handling unknown.", ...(toldAnswers.has("test_trust.confidence") ? [] : ["Team trust in tests unknown."])],
59
+ next_step: "Capture whether the team trusts the suite and whether coverage is fresh.",
60
+ });
61
+ }
62
+ // Release readiness
63
+ {
64
+ const deploy = ids.includes("ci.has-deploy-job");
65
+ const releaseDocs = ids.some((i) => i.startsWith("knowledge_sources.changelog"));
66
+ const score = boost(deploy && releaseDocs ? 3 : deploy ? 1 : 0, "release.blocking-gates");
67
+ dims.push({
68
+ id: "maturity.release_readiness",
69
+ score,
70
+ explanation: "Based on deploy/release jobs and release docs. Blocking gates are unknown in V0.",
71
+ evidence: ids.filter((i) => i === "ci.has-deploy-job" || i.startsWith("knowledge_sources.changelog")),
72
+ unknowns: ["Release-blocking rules unknown."],
73
+ next_step: "Document what blocks a release and who approves it.",
74
+ });
75
+ }
76
+ // Quality ownership
77
+ {
78
+ const codeowners = ids.includes("knowledge_sources.codeowners");
79
+ const template = ids.includes("knowledge_sources.pr-template") || ids.includes("knowledge_sources.issue-template");
80
+ const score = boost(codeowners && template ? 3 : codeowners || template ? 2 : 0, "ownership.approvers");
81
+ dims.push({
82
+ id: "maturity.quality_ownership",
83
+ score,
84
+ explanation: "Based on CODEOWNERS and PR/issue templates. Explicit ownership facts are V1.5+.",
85
+ evidence: ids.filter((i) => i.startsWith("knowledge_sources.codeowners") || i.includes("template")),
86
+ unknowns: codeowners ? [] : ["Code ownership is unknown."],
87
+ next_step: "Add CODEOWNERS and confirm approvers for risky areas.",
88
+ });
89
+ }
90
+ // Agent readiness
91
+ {
92
+ const config = ids.some((i) => i.startsWith("agent_permissions.config."));
93
+ const skills = ids.includes("agent_permissions.skills");
94
+ const score = boost(config && skills ? 3 : config ? 2 : 0, "agent_permissions.boundaries");
95
+ dims.push({
96
+ id: "maturity.agent_readiness",
97
+ score,
98
+ explanation: "Based on agent config and existing skills. Permission boundaries are told knowledge (V1.5+).",
99
+ evidence: ids.filter((i) => i.startsWith("agent_permissions.")),
100
+ unknowns: ["Agent permission boundaries unknown."],
101
+ next_step: "Define what an AI agent may do in this repo.",
102
+ });
103
+ }
104
+ return dims.map((d) => makeFact({
105
+ id: d.id,
106
+ domain: "maturity",
107
+ statement: `${d.id.split(".")[1]}: ${d.score}/5`,
108
+ provenance: "inferred",
109
+ confidence: 0.5,
110
+ evidence_provider: PROVIDER,
111
+ evidence: d.evidence,
112
+ value: { score: d.score, max: 5, explanation: d.explanation, evidence: d.evidence, unknowns: d.unknowns, next_step: d.next_step },
113
+ }, today));
114
+ }
@@ -0,0 +1,53 @@
1
+ import { scanRepo } from "../scanners/repo-scanner.js";
2
+ import { scanTests } from "../scanners/test-scanner.js";
3
+ import { scanCi } from "../scanners/ci-scanner.js";
4
+ import { scanDocs } from "../scanners/docs-scanner.js";
5
+ import { scanBuild } from "../scanners/build-scanner.js";
6
+ import { scanAgentConfig } from "../scanners/agent-config-scanner.js";
7
+ import { repoFacts } from "../domains/repo-facts.js";
8
+ import { testFacts, qaradarTestFacts } from "../domains/test-facts.js";
9
+ import { ciFacts } from "../domains/ci-facts.js";
10
+ import { docsFacts } from "../domains/docs-facts.js";
11
+ import { buildFacts } from "../domains/build-facts.js";
12
+ import { agentFacts } from "../domains/agent-facts.js";
13
+ import { repoQualityFacts } from "../domains/repo-quality-facts.js";
14
+ import { deriveUnknownFacts } from "../unknowns/unknowns.js";
15
+ import { scoreMaturity } from "../maturity/rubric.js";
16
+ import { qaradarProvider } from "../providers/qaradar-provider.js";
17
+ export async function runScan(ctx, today, log = () => { }, priorFacts = []) {
18
+ const result = await runScanDetailed(ctx, today, log, priorFacts);
19
+ return result.facts;
20
+ }
21
+ export async function runScanDetailed(ctx, today, log = () => { }, priorFacts = []) {
22
+ const deterministic = [
23
+ ...repoFacts(scanRepo(ctx), today),
24
+ ...testFacts(scanTests(ctx), today),
25
+ ...ciFacts(scanCi(ctx), today),
26
+ ...docsFacts(scanDocs(ctx), today),
27
+ ...buildFacts(scanBuild(ctx), today),
28
+ ...agentFacts(scanAgentConfig(ctx), today),
29
+ ];
30
+ let qaradarRan = false;
31
+ let qaFacts = [];
32
+ try {
33
+ if (await qaradarProvider.isAvailable(ctx)) {
34
+ const results = await qaradarProvider.collect(ctx);
35
+ qaFacts = [...repoQualityFacts(results, today), ...qaradarTestFacts(results, today)];
36
+ qaradarRan = results.length > 0;
37
+ if (results.length === 0) {
38
+ log("QA Radar ran but found no usable data (e.g. no commits or no risk signals). Continuing with built-in scanners.");
39
+ }
40
+ }
41
+ else {
42
+ log("QA Radar not available. Continuing with built-in scanners.");
43
+ }
44
+ }
45
+ catch {
46
+ log("QA Radar run failed. Continuing with built-in scanners.");
47
+ }
48
+ const base = [...deterministic, ...qaFacts, ...priorFacts];
49
+ const unknowns = deriveUnknownFacts(base, today);
50
+ const withUnknowns = [...base, ...unknowns];
51
+ const maturity = scoreMaturity(withUnknowns, today);
52
+ return { facts: [...withUnknowns, ...maturity], qaradarRan };
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ export function parseQaradar(report) {
2
+ const s = report.summary;
3
+ const results = [];
4
+ if (report.risky_modules.length || report.untested_files.length) {
5
+ const topRisky = report.risky_modules
6
+ .slice()
7
+ .sort((a, b) => b.risk_score - a.risk_score)
8
+ .slice(0, 5)
9
+ .map((m) => ({ path: m.path, risk: m.risk_level, score: m.risk_score, reasons: m.reasons }));
10
+ results.push({
11
+ provider: "qaradar",
12
+ domain: "repo_quality",
13
+ statement: "QA Radar flagged high-risk and/or untested files.",
14
+ value: {
15
+ critical_count: s.critical_risk_count,
16
+ high_count: s.high_risk_count,
17
+ files_without_tests: s.files_without_tests,
18
+ coverage_status: s.coverage_status,
19
+ top_risky: topRisky,
20
+ untested_files: report.untested_files.slice(0, 20),
21
+ },
22
+ evidence: ["git history", "test-to-source mapping", report.summary.coverage_status === "ok" ? "coverage report" : "no coverage report"],
23
+ confidence: 0.82,
24
+ limitations: [
25
+ "Business criticality is unknown.",
26
+ "Risk is based on repository signals, not production impact.",
27
+ ],
28
+ needsHumanConfirmation: false,
29
+ });
30
+ }
31
+ if (s.source_files > 0) {
32
+ results.push({
33
+ provider: "qaradar",
34
+ domain: "test",
35
+ statement: "QA Radar mapped tests to source files.",
36
+ value: {
37
+ test_to_source_ratio: s.test_to_source_ratio,
38
+ files_with_tests: s.files_with_tests,
39
+ files_without_tests: s.files_without_tests,
40
+ source_files: s.source_files,
41
+ test_files: s.test_files,
42
+ coverage_status: s.coverage_status,
43
+ },
44
+ evidence: ["qaradar test-to-source mapping"],
45
+ confidence: 0.8,
46
+ limitations: ["Mapping is name/convention based, not execution-verified."],
47
+ needsHumanConfirmation: false,
48
+ });
49
+ }
50
+ return results;
51
+ }