Skip to content

Commit b4832ea

Browse files
committed
feat(miner-foundation): MinerGoalSpec file discovery + example doc (#2294)
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.
1 parent a2ea220 commit b4832ea

5 files changed

Lines changed: 112 additions & 0 deletions

File tree

.gittensory-miner.yml.example

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# ============================================================================
2+
# .gittensory-miner.yml — per-repo miner configuration (EXAMPLE)
3+
# ============================================================================
4+
# A repo owner drops this file in to tell an autonomous Gittensory miner what to
5+
# look for and how to behave when targeting their repo. It is the MINER-side
6+
# analogue of `.gittensory.yml` (the review-side focus manifest).
7+
#
8+
# Every field is OPTIONAL and has a safe default (shown as "Default: X" below).
9+
# The file is parsed tolerantly: an unknown key is ignored, and a single
10+
# malformed field falls back to its default with a warning — a broken file never
11+
# hard-fails the miner, it just falls back to the defaults.
12+
#
13+
# Discovery order (first match wins):
14+
# .gittensory-miner.yml → .github/gittensory-miner.yml
15+
# → .gittensory-miner.json → .github/gittensory-miner.json
16+
#
17+
# Copy to `.gittensory-miner.yml` and edit. YAML or JSON are both accepted.
18+
19+
# Whether this repo permits autonomous miners at all. Explicit OPT-OUT: a public
20+
# repo with no file is still minable. Set false to halt all miner targeting.
21+
# Boolean. Default: true.
22+
minerEnabled: true
23+
24+
# Work areas you want a miner to focus on; a candidate touching these is preferred.
25+
# Glob list. Default: [] (no preference).
26+
wantedPaths:
27+
- "src/**"
28+
29+
# Paths off-limits to a miner; a candidate touching one should be skipped.
30+
# Glob list. Default: [] (nothing blocked).
31+
blockedPaths:
32+
- "vendor/**"
33+
- ".github/workflows/**"
34+
35+
# Issue/PR labels you prefer a miner to target; a candidate carrying one is favored.
36+
# String list. Default: [] (no preference).
37+
preferredLabels:
38+
- bug
39+
- enhancement
40+
41+
# Maximum issues a single miner may hold claimed on this repo at once, so one
42+
# miner cannot monopolize the queue. A positive integer (>= 1); a non-integer is
43+
# floored. Default: 1.
44+
maxConcurrentClaims: 1
45+
46+
# How strongly this repo encourages a miner to open discovery issues.
47+
# Values: encouraged | neutral | discouraged. Default: neutral.
48+
issueDiscoveryPolicy: neutral

packages/gittensory-engine/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,8 @@ explicit opt-out), no path/label preferences, one concurrent claim, `neutral` di
7373
`parseMinerGoalSpec(raw)` and `parseMinerGoalSpecContent(content)` are the tolerant parser pair for that file. They
7474
never throw on malformed JSON/YAML; instead they return `{ present, spec, warnings }`, where `spec` is normalized to
7575
safe defaults and `warnings` explains any dropped or invalid fields.
76+
77+
`discoverMinerGoalSpecPath(exists)` returns the first present file in the documented order (`MINER_GOAL_SPEC_FILENAMES`:
78+
`.gittensory-miner.yml``.github/gittensory-miner.yml` → the `.json` variants). It is IO-free — the caller injects
79+
the existence check — so a caller reads the returned path and feeds its content to `parseMinerGoalSpecContent`. See
80+
`.gittensory-miner.yml.example` for the documented fields.

packages/gittensory-engine/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export {
2222
DEFAULT_MINER_GOAL_SPEC,
2323
parseMinerGoalSpec,
2424
parseMinerGoalSpecContent,
25+
discoverMinerGoalSpecPath,
26+
MINER_GOAL_SPEC_FILENAMES,
2527
type MinerGoalSpec,
2628
type MinerIssueDiscoveryPolicy,
2729
type ParsedMinerGoalSpec,

packages/gittensory-engine/src/miner-goal-spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,24 @@ export function parseMinerGoalSpecContent(content: string | null | undefined): P
246246
}
247247
return parseMinerGoalSpec(parsed);
248248
}
249+
250+
/**
251+
* The documented `.gittensory-miner` file-discovery order (first match wins), mirroring how `.gittensory.yml` is
252+
* discovered: repo-root YAML, then `.github/` YAML, then the JSON variants.
253+
*/
254+
export const MINER_GOAL_SPEC_FILENAMES = [
255+
".gittensory-miner.yml",
256+
".github/gittensory-miner.yml",
257+
".gittensory-miner.json",
258+
".github/gittensory-miner.json",
259+
] as const;
260+
261+
/**
262+
* The first {@link MINER_GOAL_SPEC_FILENAMES} candidate that exists, or null. Pure: the caller injects the existence
263+
* check (e.g. `fs.existsSync`) so this module stays IO-free and unit-testable. A caller reads the returned path and
264+
* feeds its content to {@link parseMinerGoalSpecContent}.
265+
*/
266+
export function discoverMinerGoalSpecPath(exists: (path: string) => boolean): string | null {
267+
for (const name of MINER_GOAL_SPEC_FILENAMES) if (exists(name)) return name;
268+
return null;
269+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Tests for MinerGoalSpec file discovery (#2294). The tolerant parser itself is covered by
2+
// miner-goal-spec-parse.test.ts / miner-goal-spec-parser.test.ts; this covers only the discovery order. Pure —
3+
// the existence check is injected, so no filesystem is touched. Runs against compiled dist/.
4+
import { test } from "node:test";
5+
import assert from "node:assert/strict";
6+
import { discoverMinerGoalSpecPath, MINER_GOAL_SPEC_FILENAMES } from "../dist/index.js";
7+
8+
test("MINER_GOAL_SPEC_FILENAMES lists the documented discovery order", () => {
9+
assert.deepEqual([...MINER_GOAL_SPEC_FILENAMES], [
10+
".gittensory-miner.yml",
11+
".github/gittensory-miner.yml",
12+
".gittensory-miner.json",
13+
".github/gittensory-miner.json",
14+
]);
15+
});
16+
17+
test("discoverMinerGoalSpecPath: returns the first existing candidate, first match wins", () => {
18+
assert.equal(discoverMinerGoalSpecPath(() => true), ".gittensory-miner.yml");
19+
// repo-root yml missing but the .github yml present → that one is chosen
20+
assert.equal(
21+
discoverMinerGoalSpecPath((p) => p !== ".gittensory-miner.yml"),
22+
".github/gittensory-miner.yml",
23+
);
24+
// only a JSON variant present
25+
assert.equal(discoverMinerGoalSpecPath((p) => p === ".gittensory-miner.json"), ".gittensory-miner.json");
26+
});
27+
28+
test("discoverMinerGoalSpecPath: returns null when no candidate exists, and never probes unlisted paths", () => {
29+
const probed: string[] = [];
30+
const result = discoverMinerGoalSpecPath((p) => {
31+
probed.push(p);
32+
return false;
33+
});
34+
assert.equal(result, null);
35+
assert.deepEqual(probed, [...MINER_GOAL_SPEC_FILENAMES]); // exactly the listed candidates, in order
36+
});

0 commit comments

Comments
 (0)