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
2 changes: 2 additions & 0 deletions packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ no enforcement wiring yet. (#2328)

## Install

See [`docs/miner-goal-spec.md`](docs/miner-goal-spec.md) for the `.gittensory-miner.yml` field reference and [`.gittensory-miner.yml.example`](../../.gittensory-miner.yml.example) at the repo root.

From a local checkout:

```sh
Expand Down
51 changes: 51 additions & 0 deletions packages/gittensory-miner/docs/miner-goal-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# MinerGoalSpec (`.gittensory-miner.yml`)

Per-repo configuration telling an autonomous Gittensory miner what to look for and how to behave when targeting a repo. Parsed by `@jsonbored/gittensory-engine` (`parseMinerGoalSpec` / `parseMinerGoalSpecContent`); this document is the field reference. Machine-readable shape: [`../schema/miner-goal-spec.schema.json`](../schema/miner-goal-spec.schema.json). Copy [`.gittensory-miner.yml.example`](../../../.gittensory-miner.yml.example) to `.gittensory-miner.yml` and edit.

Discovery order (first match wins):

- `.gittensory-miner.yml`
- `.github/gittensory-miner.yml`
- `.gittensory-miner.json`
- `.github/gittensory-miner.json`

Every field is optional. Unknown keys are ignored; a malformed field falls back to its documented default with a warning — a broken file never hard-fails the miner.

## Relationship to `.gittensory.yml`

| File | Actor | Purpose |
|------|-------|---------|
| `.gittensory.yml` | Review stack | How a maintainer's repo **reviews** incoming PRs (focus manifest, gate, scoring knobs). |
| `.gittensory-miner.yml` | Miner runtime | How a miner **searches for and prioritizes** work in a target repo. |

They are read by different components and do not conflict. A miner should still treat a target repo's public `.gittensory.yml` `wantedPaths` / `blockedPaths` as a hard floor when both files exist.

## Fields

### `minerEnabled` (boolean, default: `true`)

Explicit opt-out: a public repo with no file remains minable. Set `false` to halt all miner targeting.

### `wantedPaths` (string list, default: `[]`)

Work areas the maintainer wants a miner to focus on. Glob list. Empty means no preference.

### `blockedPaths` (string list, default: `[]`)

Paths off-limits to a miner; candidates touching one should be skipped. Glob list. Mirrors `.gittensory.yml` `blockedPaths` semantics.

### `preferredLabels` (string list, default: `[]`)

Issue labels a miner should favor. Empty means no preference.

### `blockedLabels` (string list, default: `[]`)

Issue labels a miner must skip.

### `maxConcurrentClaims` (integer `>= 1`, default: `1`)

Maximum issues one miner may hold claimed on this repo at once.

### `issueDiscoveryPolicy` (`encouraged` | `neutral` | `discouraged`, default: `neutral`)

How strongly this repo encourages a miner to open discovery issues.
51 changes: 51 additions & 0 deletions packages/gittensory-miner/schema/miner-goal-spec.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://jsonbored.dev/schemas/gittensory-miner-goal-spec.json",
"title": "MinerGoalSpec",
"description": "Per-repo miner configuration parsed from `.gittensory-miner.yml` (see packages/gittensory-miner/docs/miner-goal-spec.md).",
"type": "object",
"additionalProperties": true,
"properties": {
"minerEnabled": {
"type": "boolean",
"default": true,
"description": "Whether autonomous miners may target this repo. Default: true."
},
"wantedPaths": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"default": [],
"description": "Work areas a miner should prefer. Glob list. Default: []."
},
"blockedPaths": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"default": [],
"description": "Paths a miner must skip. Glob list. Default: []."
},
"preferredLabels": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"default": [],
"description": "Issue labels a miner should favor. Default: []."
},
"blockedLabels": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"default": [],
"description": "Issue labels a miner must skip. Default: []."
},
"maxConcurrentClaims": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "Maximum active claims one miner may hold on this repo. Default: 1."
},
"issueDiscoveryPolicy": {
"type": "string",
"enum": ["encouraged", "neutral", "discouraged"],
"default": "neutral",
"description": "How strongly opening discovery issues is encouraged. Default: neutral."
}
}
}
59 changes: 59 additions & 0 deletions test/unit/miner-goal-spec-doc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

import { parseMinerGoalSpecContent } from "../../packages/gittensory-engine/src/miner-goal-spec";

const repoRoot = process.cwd();
const schemaPath = join(repoRoot, "packages/gittensory-miner/schema/miner-goal-spec.schema.json");
const docPath = join(repoRoot, "packages/gittensory-miner/docs/miner-goal-spec.md");
const examplePath = join(repoRoot, ".gittensory-miner.yml.example");

const SPEC_FIELDS = [
"minerEnabled",
"wantedPaths",
"blockedPaths",
"preferredLabels",
"blockedLabels",
"maxConcurrentClaims",
"issueDiscoveryPolicy",
] as const;

describe("miner goal spec docs (#2300)", () => {
it("documents every MinerGoalSpec field and the relationship to .gittensory.yml", () => {
const doc = readFileSync(docPath, "utf8");
expect(doc).toContain("Relationship to `.gittensory.yml`");
expect(doc).toContain("wantedPaths");
expect(doc).toContain("blockedPaths");
for (const field of SPEC_FIELDS) {
expect(doc).toContain(field);
}
});

it("ships a JSON Schema draft 2020-12 with the MinerGoalSpec properties", () => {
const schema = JSON.parse(readFileSync(schemaPath, "utf8")) as {
$schema: string;
properties: Record<string, unknown>;
};
expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
for (const field of SPEC_FIELDS) {
expect(schema.properties).toHaveProperty(field);
}
});

it("parses the root example file through the engine parser", () => {
const example = readFileSync(examplePath, "utf8");
const parsed = parseMinerGoalSpecContent(example);
expect(parsed.present).toBe(true);
expect(parsed.spec).toMatchObject({
minerEnabled: true,
wantedPaths: ["src/**"],
blockedPaths: ["vendor/**", ".github/workflows/**"],
preferredLabels: ["bug", "enhancement"],
blockedLabels: ["wontfix", "duplicate"],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
expect(parsed.warnings).toEqual([]);
});
});
Loading