diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index 8e38bfba8d..b8b5ab8a39 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -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 diff --git a/packages/gittensory-miner/docs/miner-goal-spec.md b/packages/gittensory-miner/docs/miner-goal-spec.md new file mode 100644 index 0000000000..777721d721 --- /dev/null +++ b/packages/gittensory-miner/docs/miner-goal-spec.md @@ -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. diff --git a/packages/gittensory-miner/schema/miner-goal-spec.schema.json b/packages/gittensory-miner/schema/miner-goal-spec.schema.json new file mode 100644 index 0000000000..14340a3e27 --- /dev/null +++ b/packages/gittensory-miner/schema/miner-goal-spec.schema.json @@ -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." + } + } +} diff --git a/test/unit/miner-goal-spec-doc.test.ts b/test/unit/miner-goal-spec-doc.test.ts new file mode 100644 index 0000000000..a151eefe6b --- /dev/null +++ b/test/unit/miner-goal-spec-doc.test.ts @@ -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; + }; + 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([]); + }); +});