Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .gittensory-miner.yml.example
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions packages/gittensory-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export {
DEFAULT_MINER_GOAL_SPEC,
parseMinerGoalSpec,
parseMinerGoalSpecContent,
discoverMinerGoalSpecPath,
MINER_GOAL_SPEC_FILENAMES,
type MinerGoalSpec,
type MinerIssueDiscoveryPolicy,
type ParsedMinerGoalSpec,
Expand Down
21 changes: 21 additions & 0 deletions packages/gittensory-engine/src/miner-goal-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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: 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) => {
probed.push(p);
return false;
});
assert.equal(result, null);
assert.deepEqual(probed, [...MINER_GOAL_SPEC_FILENAMES]); // exactly the listed candidates, in order
});