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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Murat Kus
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,104 @@
1
+ # QA Boot
2
+
3
+ Bootstrap QA context for AI coding agents.
4
+
5
+ QA Boot scans a repository or multi-repo workspace, collects deterministic testing/build/CI evidence, captures unknowns, and generates Claude Code-compatible QA context files.
6
+
7
+ It helps QA engineers and quality leads onboard into messy projects without letting AI agents invent missing process knowledge.
8
+
9
+ QA Boot is not a test generator or QA automation framework. It is the context and safety layer before AI-assisted QA work.
10
+
11
+ ## Core idea
12
+
13
+ Most AI QA tooling focuses on generating, executing, or reviewing tests.
14
+
15
+ QA Boot focuses on the missing layer before that:
16
+
17
+ - What does this project know?
18
+ - What is unknown?
19
+ - Which facts are observed, inferred, or human-provided?
20
+ - Which quality risks are technical versus business-critical?
21
+ - What is Claude allowed to assume?
22
+ - What should Claude ask before acting?
23
+
24
+ ## V1 product boundary
25
+
26
+ V1 is a CLI-first file generator.
27
+
28
+ It should:
29
+
30
+ - scan repos deterministically,
31
+ - consume evidence providers like QA Radar,
32
+ - generate `qa-context/`,
33
+ - generate Claude Code project skills,
34
+ - generate unknowns and maturity summaries,
35
+ - avoid external writes by default.
36
+
37
+ V1 should not:
38
+
39
+ - auto-comment on PRs,
40
+ - auto-create Jira tickets,
41
+ - update TestRail,
42
+ - trigger CI,
43
+ - approve releases,
44
+ - access secrets,
45
+ - act as a full autonomous QA agent.
46
+
47
+ ## Install & usage
48
+
49
+ Requires Node 20+. Run qa-boot directly in any repo — no install step needed:
50
+
51
+ ```bash
52
+ npx qa-boot init --project-name my-service
53
+ npx qa-boot scan # auto-runs generate
54
+ ```
55
+
56
+ `scan` detects languages/tests/CI/docs/build/agent-config, writes
57
+ `qa-context/facts.json`, and (unless `--no-generate`) renders the `qa-context/*.md`
58
+ summaries, `unknowns.md`, `maturity.md`, `CLAUDE.qa.md`, and the `.claude/skills`.
59
+
60
+ Record human-told QA knowledge (answers to open unknowns, or free-form facts):
61
+
62
+ ```bash
63
+ npx qa-boot tell <unknown-id> "the answer" --by alice
64
+ npx qa-boot tell "we release every Tuesday" --domain release --by alice
65
+ ```
66
+
67
+ Prefer a permanent command? `npm install -g qa-boot` gives you `qa-boot` on your
68
+ PATH; `npm install -D qa-boot` pins a version per-project.
69
+
70
+ QA Radar is consumed live when installed:
71
+
72
+ ```bash
73
+ npx qa-boot scan --with-qaradar # spawns `qaradar analyze --json-output`
74
+ npx qa-boot scan --skip-qaradar # built-in scanners only
75
+ ```
76
+
77
+ When `qaradar` is absent the scan still succeeds and records the gap as an
78
+ unknown (`repo-risk.md` is only written when QA Radar ran).
79
+
80
+ ### Developing qa-boot itself
81
+
82
+ ```bash
83
+ git clone https://github.com/MuratKus/qa-boot.git
84
+ cd qa-boot && npm install
85
+ npm test # vitest
86
+ npm run dev -- scan # run the CLI from source via tsx
87
+ ```
88
+
89
+ Design and plan: [`docs/superpowers/specs/2026-06-02-qa-boot-v0-design.md`](./docs/superpowers/specs/2026-06-02-qa-boot-v0-design.md)
90
+ and [`docs/superpowers/plans/2026-06-02-qa-boot-v0.md`](./docs/superpowers/plans/2026-06-02-qa-boot-v0.md).
91
+
92
+ ## Start here
93
+
94
+ Read [`SPEC_INDEX.md`](./SPEC_INDEX.md) first.
95
+
96
+ For implementation planning, start with:
97
+
98
+ - [`docs/00-overview.md`](./docs/00-overview.md)
99
+ - [`docs/02-v1-scope.md`](./docs/02-v1-scope.md)
100
+ - [`docs/03-architecture.md`](./docs/03-architecture.md)
101
+
102
+ ## Tagline
103
+
104
+ > Make Claude more grounded before making it more autonomous.
@@ -0,0 +1,7 @@
1
+ import { loadConfig } from "../../config/config.js";
2
+ import { runGenerate } from "../../generate/generate.js";
3
+ export async function cmdGenerate(repoPath, opts) {
4
+ const config = loadConfig(repoPath);
5
+ const written = runGenerate({ repoPath, config, claude: opts.claude });
6
+ console.log(`Generated ${written.length} files.`);
7
+ }
@@ -0,0 +1,24 @@
1
+ import { writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { defaultConfig } from "../../config/config.js";
4
+ const README = `# QA Context
5
+
6
+ This directory is generated and owned by \`qa-boot\`. Do not hand-edit these files —
7
+ they are regenerated from \`facts.json\` on every \`qa-boot generate\`.
8
+
9
+ - \`facts.json\` — the source of truth (written by \`qa-boot scan\`).
10
+ - \`unknowns.md\` — what QA Boot could not determine, with questions to ask a human.
11
+ - other \`*.md\` — per-domain summaries.
12
+ `;
13
+ export async function cmdInit(repoPath, opts) {
14
+ const configPath = join(repoPath, "qa-boot.config.json");
15
+ if (existsSync(configPath) && !opts.force) {
16
+ throw new Error("qa-boot.config.json already exists. Pass --force to overwrite.");
17
+ }
18
+ const config = defaultConfig(opts.projectName);
19
+ config.claude.enabled = opts.claude;
20
+ config.evidence_providers.qaradar.enabled = opts.qaradar;
21
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
22
+ mkdirSync(join(repoPath, "qa-context"), { recursive: true });
23
+ writeFileSync(join(repoPath, "qa-context", "README.md"), README, "utf8");
24
+ }
@@ -0,0 +1,30 @@
1
+ import { join } from "node:path";
2
+ import { loadConfig } from "../../config/config.js";
3
+ import { buildScanContext } from "../../core/scan-context.js";
4
+ import { FactStore } from "../../core/fact-store.js";
5
+ import { todayISO } from "../../core/fact.js";
6
+ import { runScanDetailed } from "../../pipeline/run-scan.js";
7
+ import { cmdGenerate } from "./generate.js";
8
+ export async function cmdScan(repoPath, opts) {
9
+ const config = loadConfig(repoPath);
10
+ if (opts.skipQaradar)
11
+ config.evidence_providers.qaradar.enabled = "false";
12
+ if (opts.withQaradar)
13
+ config.evidence_providers.qaradar.enabled = "true";
14
+ const ctx = buildScanContext(repoPath, config);
15
+ const today = todayISO();
16
+ const factsPath = join(repoPath, "qa-context", "facts.json");
17
+ const store = FactStore.load(factsPath);
18
+ store.markTimeStaleness(today);
19
+ const priorTold = store.all().filter((f) => f.provenance === "told" && !f.stale);
20
+ const { facts, qaradarRan } = await runScanDetailed(ctx, today, (m) => console.log(m), priorTold);
21
+ if (qaradarRan)
22
+ console.log("QA Radar: included its analysis.");
23
+ store.upsert(facts);
24
+ store.markTimeStaleness(today);
25
+ store.save(factsPath);
26
+ console.log(`Wrote ${store.all().length} facts to qa-context/facts.json`);
27
+ if (opts.generate) {
28
+ await cmdGenerate(repoPath, { claude: opts.claude });
29
+ }
30
+ }
@@ -0,0 +1,27 @@
1
+ import { join } from "node:path";
2
+ import { FactStore } from "../../core/fact-store.js";
3
+ import { todayISO } from "../../core/fact.js";
4
+ import { applyTell } from "../../tell/tell.js";
5
+ import { appendInterviewLog } from "../../tell/interview-log.js";
6
+ export async function cmdTell(repoPath, unknownId, statement, opts) {
7
+ const factsPath = join(repoPath, "qa-context", "facts.json");
8
+ const store = FactStore.load(factsPath);
9
+ const today = todayISO();
10
+ store.markTimeStaleness(today);
11
+ const result = applyTell(store, { unknownId, domain: opts.domain, statement, by: opts.by ?? "", source: opts.source, idSlug: opts.id }, today);
12
+ store.save(factsPath);
13
+ appendInterviewLog(repoPath, {
14
+ date: today,
15
+ by: result.fact.told_by ?? "",
16
+ unknownId: result.fact.answers_unknown,
17
+ question: result.question,
18
+ domain: result.fact.domain,
19
+ statement: result.fact.statement,
20
+ factId: result.fact.id,
21
+ scope: result.fact.scope ?? "repo",
22
+ });
23
+ console.log(`Recorded told fact ${result.fact.id} (scope: ${result.fact.scope ?? "repo"}).`);
24
+ if (result.fact.answers_unknown)
25
+ console.log(`Resolved unknown ${result.fact.answers_unknown}.`);
26
+ console.log(`${result.remainingUnknowns} unknowns remain open. Run \`qa-boot scan\` to refresh context files and maturity.`);
27
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { cmdInit } from "./commands/init.js";
4
+ import { cmdScan } from "./commands/scan.js";
5
+ import { cmdGenerate } from "./commands/generate.js";
6
+ import { cmdTell } from "./commands/tell.js";
7
+ import { cliVersion } from "./version.js";
8
+ import { basename } from "node:path";
9
+ const program = new Command();
10
+ program.name("qa-boot").description("Bootstrap QA context for AI coding agents.").version(cliVersion());
11
+ program
12
+ .command("init")
13
+ .description("Initialize qa-boot config in the current repo")
14
+ .option("--project-name <name>", "project name")
15
+ .option("--no-claude", "disable Claude output")
16
+ .option("--qaradar <mode>", "auto|true|false", "auto")
17
+ .option("--force", "overwrite existing config")
18
+ .action(async (o) => {
19
+ await cmdInit(process.cwd(), {
20
+ projectName: o.projectName ?? basename(process.cwd()),
21
+ claude: o.claude,
22
+ qaradar: o.qaradar,
23
+ force: o.force,
24
+ });
25
+ });
26
+ program
27
+ .command("scan")
28
+ .description("Scan the repo, write facts.json, and generate context")
29
+ .option("--no-generate", "write facts.json only")
30
+ .option("--no-claude", "skip Claude output during generate")
31
+ .option("--with-qaradar", "force-enable QA Radar")
32
+ .option("--skip-qaradar", "disable QA Radar")
33
+ .action(async (o) => {
34
+ await cmdScan(process.cwd(), {
35
+ generate: o.generate,
36
+ claude: o.claude,
37
+ withQaradar: Boolean(o.withQaradar),
38
+ skipQaradar: Boolean(o.skipQaradar),
39
+ });
40
+ });
41
+ program
42
+ .command("generate")
43
+ .description("Generate context files from qa-context/facts.json")
44
+ .option("--no-claude", "skip Claude output")
45
+ .action(async (o) => {
46
+ await cmdGenerate(process.cwd(), { claude: o.claude });
47
+ });
48
+ program
49
+ .command("tell <idOrStatement> [statement]")
50
+ .description("Record human-told QA knowledge (answer an unknown by id, or add --domain knowledge)")
51
+ .option("--domain <domain>", "domain for free-form knowledge")
52
+ .option("--by <who>", "who provided this knowledge (required)")
53
+ .option("--source <url>", "optional source link")
54
+ .option("--id <slug>", "explicit id slug for free-form mode")
55
+ .action(async (idOrStatement, statement, o) => {
56
+ const isAnswer = statement !== undefined;
57
+ await cmdTell(process.cwd(), isAnswer ? idOrStatement : undefined, isAnswer ? statement : idOrStatement, {
58
+ domain: o.domain,
59
+ by: o.by,
60
+ source: o.source,
61
+ id: o.id,
62
+ });
63
+ });
64
+ program.parseAsync().catch((err) => {
65
+ console.error(String(err instanceof Error ? err.message : err));
66
+ process.exitCode = 1;
67
+ });
@@ -0,0 +1,5 @@
1
+ import { createRequire } from "node:module";
2
+ export function cliVersion() {
3
+ const pkg = createRequire(import.meta.url)("../../package.json");
4
+ return pkg.version;
5
+ }
@@ -0,0 +1,34 @@
1
+ import { readJson } from "../core/fs-utils.js";
2
+ export function defaultConfig(projectName) {
3
+ return {
4
+ project_name: projectName,
5
+ mode: "single-repo",
6
+ claude: { enabled: true, generate_skills: true },
7
+ evidence_providers: { qaradar: { enabled: "auto", days: 90, top: 20 } },
8
+ refresh: { default_days: 30 },
9
+ };
10
+ }
11
+ export function normalizeConfig(partial) {
12
+ const base = defaultConfig(partial.project_name ?? "project");
13
+ return {
14
+ project_name: partial.project_name ?? base.project_name,
15
+ mode: "single-repo",
16
+ claude: { ...base.claude, ...partial.claude },
17
+ evidence_providers: {
18
+ qaradar: { ...base.evidence_providers.qaradar, ...partial.evidence_providers?.qaradar },
19
+ },
20
+ refresh: { ...base.refresh, ...partial.refresh },
21
+ };
22
+ }
23
+ export function loadConfig(repoPath) {
24
+ const raw = readJson(repoPath, "qa-boot.config.json");
25
+ return normalizeConfig(raw ?? { project_name: "project" });
26
+ }
27
+ export function qaradarEnabled(c) {
28
+ const v = c.evidence_providers.qaradar.enabled;
29
+ if (v === true)
30
+ return "true";
31
+ if (v === false)
32
+ return "false";
33
+ return v;
34
+ }
@@ -0,0 +1,50 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { todayISO } from "./fact.js";
4
+ function daysBetween(a, b) {
5
+ const ms = Date.parse(b) - Date.parse(a);
6
+ return Math.floor(ms / 86_400_000);
7
+ }
8
+ export class FactStore {
9
+ facts = new Map();
10
+ constructor(initial = []) {
11
+ for (const f of initial)
12
+ this.facts.set(f.id, f);
13
+ }
14
+ static load(path) {
15
+ if (!existsSync(path))
16
+ return new FactStore([]);
17
+ const raw = JSON.parse(readFileSync(path, "utf8"));
18
+ return new FactStore(raw.facts ?? []);
19
+ }
20
+ save(path) {
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ const out = { facts: this.all() };
23
+ writeFileSync(path, JSON.stringify(out, null, 2) + "\n", "utf8");
24
+ }
25
+ upsert(incoming) {
26
+ for (const f of incoming) {
27
+ const next = { ...f };
28
+ delete next.stale;
29
+ delete next.stale_reason;
30
+ this.facts.set(f.id, next);
31
+ }
32
+ }
33
+ markTimeStaleness(today = todayISO()) {
34
+ for (const f of this.facts.values()) {
35
+ if (daysBetween(f.last_verified, today) > f.expires_after_days) {
36
+ f.stale = true;
37
+ f.stale_reason = "expired";
38
+ }
39
+ }
40
+ }
41
+ remove(id) {
42
+ return this.facts.delete(id);
43
+ }
44
+ byId(id) {
45
+ return this.facts.get(id);
46
+ }
47
+ all() {
48
+ return [...this.facts.values()].sort((a, b) => a.id.localeCompare(b.id));
49
+ }
50
+ }
@@ -0,0 +1,30 @@
1
+ export function todayISO(d = new Date()) {
2
+ return d.toISOString().slice(0, 10);
3
+ }
4
+ export function makeFact(input, today = todayISO()) {
5
+ const f = {
6
+ id: input.id,
7
+ domain: input.domain,
8
+ statement: input.statement,
9
+ value: input.value ?? null,
10
+ provenance: input.provenance,
11
+ confidence: input.confidence,
12
+ evidence_provider: input.evidence_provider,
13
+ evidence_command: input.evidence_command ?? "qa-boot scan",
14
+ evidence: input.evidence ?? [],
15
+ limitations: input.limitations ?? [],
16
+ risk_if_wrong: input.risk_if_wrong ?? "",
17
+ needs_human_confirmation: input.needs_human_confirmation ?? false,
18
+ last_verified: today,
19
+ expires_after_days: input.expires_after_days ?? 30,
20
+ };
21
+ if (input.told_by !== undefined)
22
+ f.told_by = input.told_by;
23
+ if (input.scope !== undefined)
24
+ f.scope = input.scope;
25
+ if (input.source !== undefined)
26
+ f.source = input.source;
27
+ if (input.answers_unknown !== undefined)
28
+ f.answers_unknown = input.answers_unknown;
29
+ return f;
30
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import fg from "fast-glob";
4
+ import { minimatch } from "minimatch";
5
+ const IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
6
+ export function listFiles(root) {
7
+ return fg.sync("**/*", {
8
+ cwd: root,
9
+ dot: true,
10
+ ignore: IGNORE,
11
+ onlyFiles: false,
12
+ markDirectories: true,
13
+ });
14
+ }
15
+ export function exists(files, pattern) {
16
+ return files.some((f) => minimatch(f, pattern, { dot: true }));
17
+ }
18
+ export function matches(files, pattern) {
19
+ return files.filter((f) => minimatch(f, pattern, { dot: true }));
20
+ }
21
+ export function readText(root, rel) {
22
+ try {
23
+ return readFileSync(join(root, rel), "utf8");
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function readJson(root, rel) {
30
+ const text = readText(root, rel);
31
+ if (text === null)
32
+ return null;
33
+ try {
34
+ return JSON.parse(text);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
@@ -0,0 +1,13 @@
1
+ import { listFiles, exists, matches, readText, readJson } from "./fs-utils.js";
2
+ export function buildScanContext(repoPath, config) {
3
+ const files = listFiles(repoPath);
4
+ return {
5
+ repoPath,
6
+ config,
7
+ files,
8
+ has: (p) => exists(files, p),
9
+ match: (p) => matches(files, p),
10
+ read: (rel) => readText(repoPath, rel),
11
+ readJson: (rel) => readJson(repoPath, rel),
12
+ };
13
+ }
@@ -0,0 +1,8 @@
1
+ export function slug(name, maxLen = 60) {
2
+ return name
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, "-")
5
+ .replace(/^-+|-+$/g, "")
6
+ .slice(0, maxLen)
7
+ .replace(/-+$/g, "");
8
+ }
@@ -0,0 +1,31 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ import { slug } from "../core/slug.js";
3
+ const PROVIDER = "agent-config-scanner";
4
+ export function agentFacts(ev, today) {
5
+ const facts = [];
6
+ for (const e of ev.filter((x) => x.kind === "agent-config")) {
7
+ const name = String(e.detail?.name);
8
+ facts.push(makeFact({
9
+ id: `agent_permissions.config.${slug(name)}`,
10
+ domain: "agent_permissions",
11
+ statement: `Agent config ${name} is present.`,
12
+ provenance: "observed",
13
+ confidence: 0.8,
14
+ evidence_provider: PROVIDER,
15
+ evidence: e.path ? [e.path] : [],
16
+ limitations: ["Presence does not define what the agent is permitted to do."],
17
+ }, today));
18
+ }
19
+ if (ev.some((e) => e.kind === "agent-skills")) {
20
+ facts.push(makeFact({
21
+ id: "agent_permissions.skills",
22
+ domain: "agent_permissions",
23
+ statement: "Existing Claude skills were detected.",
24
+ provenance: "observed",
25
+ confidence: 0.8,
26
+ evidence_provider: PROVIDER,
27
+ evidence: [".claude/skills"],
28
+ }, today));
29
+ }
30
+ return facts;
31
+ }
@@ -0,0 +1,31 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "build-scanner";
3
+ export function buildFacts(ev, today) {
4
+ const facts = [];
5
+ for (const e of ev.filter((x) => x.kind === "build-command")) {
6
+ const name = String(e.detail?.name);
7
+ facts.push(makeFact({
8
+ id: `build.command.${name}`,
9
+ domain: "build",
10
+ statement: `A "${name}" command is defined.`,
11
+ provenance: "observed",
12
+ confidence: 0.8,
13
+ evidence_provider: PROVIDER,
14
+ evidence: e.path ? [e.path] : [],
15
+ value: { command: String(e.detail?.cmd) },
16
+ }, today));
17
+ }
18
+ for (const e of ev.filter((x) => x.kind === "build-file")) {
19
+ const name = String(e.detail?.name);
20
+ facts.push(makeFact({
21
+ id: `build.tool.${name}`,
22
+ domain: "build",
23
+ statement: `Build tooling detected: ${name}.`,
24
+ provenance: "observed",
25
+ confidence: 0.8,
26
+ evidence_provider: PROVIDER,
27
+ evidence: e.path ? [e.path] : [],
28
+ }, today));
29
+ }
30
+ return facts;
31
+ }
@@ -0,0 +1,43 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "ci-scanner";
3
+ export function ciFacts(ev, today) {
4
+ const facts = [];
5
+ for (const e of ev.filter((x) => x.kind === "ci-system")) {
6
+ const name = String(e.detail?.name);
7
+ facts.push(makeFact({
8
+ id: `ci.system.${name}`,
9
+ domain: "ci",
10
+ statement: `CI system ${name} detected.`,
11
+ provenance: "observed",
12
+ confidence: 0.8,
13
+ evidence_provider: PROVIDER,
14
+ evidence: e.path ? [e.path] : [],
15
+ }, today));
16
+ }
17
+ const jobs = ev.filter((x) => x.kind === "ci-job");
18
+ const hasKind = (k) => jobs.some((j) => j.detail?.kind === k);
19
+ if (hasKind("test")) {
20
+ facts.push(makeFact({
21
+ id: "ci.runs-tests",
22
+ domain: "ci",
23
+ statement: "CI appears to run tests.",
24
+ provenance: "inferred",
25
+ confidence: 0.5,
26
+ evidence_provider: PROVIDER,
27
+ evidence: jobs.filter((j) => j.detail?.kind === "test").map((j) => j.path).filter(Boolean),
28
+ limitations: ["Job name suggests tests; not confirmed to block merges."],
29
+ }, today));
30
+ }
31
+ if (hasKind("deploy")) {
32
+ facts.push(makeFact({
33
+ id: "ci.has-deploy-job",
34
+ domain: "release",
35
+ statement: "A deploy/release job is present in CI.",
36
+ provenance: "inferred",
37
+ confidence: 0.5,
38
+ evidence_provider: PROVIDER,
39
+ evidence: jobs.filter((j) => j.detail?.kind === "deploy").map((j) => j.path).filter(Boolean),
40
+ }, today));
41
+ }
42
+ return facts;
43
+ }
@@ -0,0 +1,28 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "docs-scanner";
3
+ const LABEL = {
4
+ readme: "README documentation",
5
+ contributing: "Contributing guide",
6
+ "docs-dir": "A docs/ directory",
7
+ codeowners: "CODEOWNERS file",
8
+ "pr-template": "Pull request template",
9
+ "issue-template": "Issue template",
10
+ changelog: "Changelog",
11
+ adr: "Architecture decision records",
12
+ };
13
+ export function docsFacts(ev, today) {
14
+ return ev
15
+ .filter((e) => e.kind === "doc")
16
+ .map((e) => {
17
+ const kind = String(e.detail?.kind);
18
+ return makeFact({
19
+ id: `knowledge_sources.${kind}`,
20
+ domain: "knowledge_sources",
21
+ statement: `${LABEL[kind] ?? kind} is present.`,
22
+ provenance: "observed",
23
+ confidence: 0.8,
24
+ evidence_provider: PROVIDER,
25
+ evidence: e.path ? [e.path] : [],
26
+ }, today);
27
+ });
28
+ }
@@ -0,0 +1,52 @@
1
+ import { makeFact } from "../core/fact.js";
2
+ const PROVIDER = "repo-scanner";
3
+ export function repoFacts(ev, today) {
4
+ const facts = [];
5
+ for (const e of ev.filter((x) => x.kind === "language")) {
6
+ const name = String(e.detail?.name);
7
+ facts.push(makeFact({
8
+ id: `repo.language.${name}`,
9
+ domain: "repo",
10
+ statement: `Repository uses ${name}.`,
11
+ provenance: "observed",
12
+ confidence: 0.8,
13
+ evidence_provider: PROVIDER,
14
+ evidence: e.path ? [e.path] : [],
15
+ }, today));
16
+ }
17
+ for (const e of ev.filter((x) => x.kind === "package-manager")) {
18
+ const name = String(e.detail?.name);
19
+ facts.push(makeFact({
20
+ id: `repo.package-manager.${name}`,
21
+ domain: "repo",
22
+ statement: `Package manager ${name} detected.`,
23
+ provenance: "observed",
24
+ confidence: 0.8,
25
+ evidence_provider: PROVIDER,
26
+ evidence: e.path ? [e.path] : [],
27
+ }, today));
28
+ }
29
+ if (ev.some((e) => e.kind === "readme")) {
30
+ facts.push(makeFact({
31
+ id: "repo.readme",
32
+ domain: "repo",
33
+ statement: "A README is present.",
34
+ provenance: "observed",
35
+ confidence: 0.8,
36
+ evidence_provider: PROVIDER,
37
+ evidence: ev.filter((e) => e.kind === "readme").map((e) => e.path).filter(Boolean),
38
+ }, today));
39
+ }
40
+ if (ev.some((e) => e.kind === "monorepo")) {
41
+ facts.push(makeFact({
42
+ id: "repo.monorepo",
43
+ domain: "repo",
44
+ statement: "Monorepo workspace markers detected.",
45
+ provenance: "inferred",
46
+ confidence: 0.5,
47
+ evidence_provider: PROVIDER,
48
+ evidence: ev.filter((e) => e.kind === "monorepo").map((e) => e.path).filter(Boolean),
49
+ }, today));
50
+ }
51
+ return facts;
52
+ }