From b4832ea1ea3bfb0497f4b0a894fac797bb23dd5c Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:09:01 -0500 Subject: [PATCH 1/2] feat(miner-foundation): MinerGoalSpec file discovery + example doc (#2294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tolerant parser (parseMinerGoalSpec / parseMinerGoalSpecContent) landed via #2652/#2658; this adds the remaining #2294 pieces that were still missing on main: - discoverMinerGoalSpecPath(exists) + MINER_GOAL_SPEC_FILENAMES in miner-goal-spec.ts: the documented discovery order (.gittensory-miner.yml → .github/… → .json variants, first match wins). Pure — the existence check is injected, so it stays IO-free; a caller reads the returned path and feeds it to parseMinerGoalSpecContent. Exported from the barrel. - .gittensory-miner.yml.example at the repo root, matching .gittensory.yml.example's header + per-field "Default: X" style. Tests: discovery order, first-match-wins, null when absent, and that only the listed candidates are probed. Engine package: 37/37 pass. --- .gittensory-miner.yml.example | 48 +++++++++++++++++++ packages/gittensory-engine/README.md | 5 ++ packages/gittensory-engine/src/index.ts | 2 + .../gittensory-engine/src/miner-goal-spec.ts | 21 ++++++++ .../test/miner-goal-spec-discovery.test.ts | 36 ++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 .gittensory-miner.yml.example create mode 100644 packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts diff --git a/.gittensory-miner.yml.example b/.gittensory-miner.yml.example new file mode 100644 index 0000000000..7e1a5368e3 --- /dev/null +++ b/.gittensory-miner.yml.example @@ -0,0 +1,48 @@ +# ============================================================================ +# .gittensory-miner.yml — per-repo miner configuration (EXAMPLE) +# ============================================================================ +# A repo owner drops this file in to tell an autonomous Gittensory miner what to +# look for and how to behave when targeting their repo. It is the MINER-side +# analogue of `.gittensory.yml` (the review-side focus manifest). +# +# Every field is OPTIONAL and has a safe default (shown as "Default: X" below). +# The file is parsed tolerantly: an unknown key is ignored, and a single +# malformed field falls back to its default with a warning — a broken file never +# hard-fails the miner, it just falls back to the defaults. +# +# Discovery order (first match wins): +# .gittensory-miner.yml → .github/gittensory-miner.yml +# → .gittensory-miner.json → .github/gittensory-miner.json +# +# Copy to `.gittensory-miner.yml` and edit. YAML or JSON are both accepted. + +# Whether this repo permits autonomous miners at all. Explicit OPT-OUT: a public +# repo with no file is still minable. Set false to halt all miner targeting. +# Boolean. Default: true. +minerEnabled: true + +# Work areas you want a miner to focus on; a candidate touching these is preferred. +# Glob list. Default: [] (no preference). +wantedPaths: + - "src/**" + +# Paths off-limits to a miner; a candidate touching one should be skipped. +# Glob list. Default: [] (nothing blocked). +blockedPaths: + - "vendor/**" + - ".github/workflows/**" + +# Issue/PR labels you prefer a miner to target; a candidate carrying one is favored. +# String list. Default: [] (no preference). +preferredLabels: + - bug + - enhancement + +# Maximum issues a single miner may hold claimed on this repo at once, so one +# miner cannot monopolize the queue. A positive integer (>= 1); a non-integer is +# floored. Default: 1. +maxConcurrentClaims: 1 + +# How strongly this repo encourages a miner to open discovery issues. +# Values: encouraged | neutral | discouraged. Default: neutral. +issueDiscoveryPolicy: neutral diff --git a/packages/gittensory-engine/README.md b/packages/gittensory-engine/README.md index 3d4ae77e2c..fe29f83787 100644 --- a/packages/gittensory-engine/README.md +++ b/packages/gittensory-engine/README.md @@ -73,3 +73,8 @@ explicit opt-out), no path/label preferences, one concurrent claim, `neutral` di `parseMinerGoalSpec(raw)` and `parseMinerGoalSpecContent(content)` are the tolerant parser pair for that file. They never throw on malformed JSON/YAML; instead they return `{ present, spec, warnings }`, where `spec` is normalized to safe defaults and `warnings` explains any dropped or invalid fields. + +`discoverMinerGoalSpecPath(exists)` returns the first present file in the documented order (`MINER_GOAL_SPEC_FILENAMES`: +`.gittensory-miner.yml` → `.github/gittensory-miner.yml` → the `.json` variants). It is IO-free — the caller injects +the existence check — so a caller reads the returned path and feeds its content to `parseMinerGoalSpecContent`. See +`.gittensory-miner.yml.example` for the documented fields. diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index bfd54a096f..500be3e59d 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -22,6 +22,8 @@ export { DEFAULT_MINER_GOAL_SPEC, parseMinerGoalSpec, parseMinerGoalSpecContent, + discoverMinerGoalSpecPath, + MINER_GOAL_SPEC_FILENAMES, type MinerGoalSpec, type MinerIssueDiscoveryPolicy, type ParsedMinerGoalSpec, diff --git a/packages/gittensory-engine/src/miner-goal-spec.ts b/packages/gittensory-engine/src/miner-goal-spec.ts index 9bc433bd3c..757414b86b 100644 --- a/packages/gittensory-engine/src/miner-goal-spec.ts +++ b/packages/gittensory-engine/src/miner-goal-spec.ts @@ -246,3 +246,24 @@ export function parseMinerGoalSpecContent(content: string | null | undefined): P } return parseMinerGoalSpec(parsed); } + +/** + * The documented `.gittensory-miner` file-discovery order (first match wins), mirroring how `.gittensory.yml` is + * discovered: repo-root YAML, then `.github/` YAML, then the JSON variants. + */ +export const MINER_GOAL_SPEC_FILENAMES = [ + ".gittensory-miner.yml", + ".github/gittensory-miner.yml", + ".gittensory-miner.json", + ".github/gittensory-miner.json", +] as const; + +/** + * The first {@link MINER_GOAL_SPEC_FILENAMES} candidate that exists, or null. Pure: the caller injects the existence + * check (e.g. `fs.existsSync`) so this module stays IO-free and unit-testable. A caller reads the returned path and + * feeds its content to {@link parseMinerGoalSpecContent}. + */ +export function discoverMinerGoalSpecPath(exists: (path: string) => boolean): string | null { + for (const name of MINER_GOAL_SPEC_FILENAMES) if (exists(name)) return name; + return null; +} diff --git a/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts b/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts new file mode 100644 index 0000000000..93da55424c --- /dev/null +++ b/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts @@ -0,0 +1,36 @@ +// Tests for MinerGoalSpec file discovery (#2294). The tolerant parser itself is covered by +// miner-goal-spec-parse.test.ts / miner-goal-spec-parser.test.ts; this covers only the discovery order. Pure — +// the existence check is injected, so no filesystem is touched. Runs against compiled dist/. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { discoverMinerGoalSpecPath, MINER_GOAL_SPEC_FILENAMES } from "../dist/index.js"; + +test("MINER_GOAL_SPEC_FILENAMES lists the documented discovery order", () => { + assert.deepEqual([...MINER_GOAL_SPEC_FILENAMES], [ + ".gittensory-miner.yml", + ".github/gittensory-miner.yml", + ".gittensory-miner.json", + ".github/gittensory-miner.json", + ]); +}); + +test("discoverMinerGoalSpecPath: returns the first existing candidate, first match wins", () => { + assert.equal(discoverMinerGoalSpecPath(() => true), ".gittensory-miner.yml"); + // repo-root yml missing but the .github yml present → that one is chosen + assert.equal( + discoverMinerGoalSpecPath((p) => p !== ".gittensory-miner.yml"), + ".github/gittensory-miner.yml", + ); + // only a JSON variant present + assert.equal(discoverMinerGoalSpecPath((p) => p === ".gittensory-miner.json"), ".gittensory-miner.json"); +}); + +test("discoverMinerGoalSpecPath: returns null when no candidate exists, and never probes unlisted paths", () => { + const probed: string[] = []; + const result = discoverMinerGoalSpecPath((p) => { + probed.push(p); + return false; + }); + assert.equal(result, null); + assert.deepEqual(probed, [...MINER_GOAL_SPEC_FILENAMES]); // exactly the listed candidates, in order +}); From 41a65dd03ac218089d33bf0c7aa8cebee98521d3 Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:18:55 -0500 Subject: [PATCH 2/2] refactor(miner-foundation): assert discovery short-circuit + ASCII-only example (#2294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a probe-recording test that discoverMinerGoalSpecPath stops after the first match (.github yml), asserting the later .json variants are never probed — so a regression that keeps probing would be caught. - Replace the Unicode arrows/em-dashes in .gittensory-miner.yml.example with ASCII (-> and -) for copy/paste-friendly, diff-friendly config docs. Engine package: 38/38 pass. --- .gittensory-miner.yml.example | 8 ++++---- .../test/miner-goal-spec-discovery.test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.gittensory-miner.yml.example b/.gittensory-miner.yml.example index 7e1a5368e3..4f7452f19f 100644 --- a/.gittensory-miner.yml.example +++ b/.gittensory-miner.yml.example @@ -1,5 +1,5 @@ # ============================================================================ -# .gittensory-miner.yml — per-repo miner configuration (EXAMPLE) +# .gittensory-miner.yml - per-repo miner configuration (EXAMPLE) # ============================================================================ # A repo owner drops this file in to tell an autonomous Gittensory miner what to # look for and how to behave when targeting their repo. It is the MINER-side @@ -7,12 +7,12 @@ # # Every field is OPTIONAL and has a safe default (shown as "Default: X" below). # The file is parsed tolerantly: an unknown key is ignored, and a single -# malformed field falls back to its default with a warning — a broken file never +# malformed field falls back to its default with a warning - a broken file never # hard-fails the miner, it just falls back to the defaults. # # Discovery order (first match wins): -# .gittensory-miner.yml → .github/gittensory-miner.yml -# → .gittensory-miner.json → .github/gittensory-miner.json +# .gittensory-miner.yml -> .github/gittensory-miner.yml +# -> .gittensory-miner.json -> .github/gittensory-miner.json # # Copy to `.gittensory-miner.yml` and edit. YAML or JSON are both accepted. diff --git a/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts b/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts index 93da55424c..5890c7d8f4 100644 --- a/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts +++ b/packages/gittensory-engine/test/miner-goal-spec-discovery.test.ts @@ -25,6 +25,17 @@ test("discoverMinerGoalSpecPath: returns the first existing candidate, first mat assert.equal(discoverMinerGoalSpecPath((p) => p === ".gittensory-miner.json"), ".gittensory-miner.json"); }); +test("discoverMinerGoalSpecPath: short-circuits — stops probing once a candidate matches", () => { + const probed: string[] = []; + const result = discoverMinerGoalSpecPath((p) => { + probed.push(p); + return p === ".github/gittensory-miner.yml"; // the 2nd candidate matches + }); + assert.equal(result, ".github/gittensory-miner.yml"); + // only the first two candidates are probed; the later .json variants are never reached + assert.deepEqual(probed, [".gittensory-miner.yml", ".github/gittensory-miner.yml"]); +}); + test("discoverMinerGoalSpecPath: returns null when no candidate exists, and never probes unlisted paths", () => { const probed: string[] = []; const result = discoverMinerGoalSpecPath((p) => {