From 5755836ed75f70184574e74603a33b7fe1bb2914 Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Thu, 2 Jul 2026 07:31:16 -0500 Subject: [PATCH 1/4] feat(miner-foundation): MinerGoalSpec type definitions (#2293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the type surface for a repo's .gittensory-miner.yml — the miner-side analogue of the .gittensory.yml focus manifest — in the shared gittensory-engine package. Types only; the parser is a separate follow-up. - src/miner-goal-spec.ts: `MinerGoalSpec` (minerEnabled, wantedPaths, blockedPaths, preferredLabels, maxConcurrentClaims, issueDiscoveryPolicy) with a JSDoc "Default: X" on every field, plus `DEFAULT_MINER_GOAL_SPEC` (safe defaults: minable by explicit opt-out, no path/label preference, 1 claim, neutral discovery) and the `MinerIssueDiscoveryPolicy` union. Field names/semantics overlapping the review side are carried over verbatim from .gittensory.yml. - Export all three from the package barrel. Tests: assert the default's runtime values + exact field surface, a compile-time check that DEFAULT_MINER_GOAL_SPEC satisfies MinerGoalSpec, and a lint-style test that every field documents a "Default:" in its JSDoc. 18/18 pass. --- packages/gittensory-engine/README.md | 6 ++ packages/gittensory-engine/src/index.ts | 5 ++ .../gittensory-engine/src/miner-goal-spec.ts | 62 +++++++++++++++++++ .../test/miner-goal-spec.test.ts | 44 +++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 packages/gittensory-engine/src/miner-goal-spec.ts create mode 100644 packages/gittensory-engine/test/miner-goal-spec.test.ts diff --git a/packages/gittensory-engine/README.md b/packages/gittensory-engine/README.md index 380ba0f111..43582fc2a2 100644 --- a/packages/gittensory-engine/README.md +++ b/packages/gittensory-engine/README.md @@ -63,3 +63,9 @@ input order. `scanAiPolicyText` and `resolveAiPolicyVerdict` provide the deterministic policy gate used by miner discovery. They only deny on small, explicit AI-contribution ban phrases in `AI-USAGE.md` or `CONTRIBUTING.md`; ambiguous, missing, or empty policy text stays allowed so discovery does not invent a ban. + +## MinerGoalSpec + +`MinerGoalSpec` is the type surface for a repo's `.gittensory-miner.yml` (miner-side analogue of `.gittensory.yml`). +`DEFAULT_MINER_GOAL_SPEC` is the safe default a repo with no file behaves as — minable (`minerEnabled: true`, an +explicit opt-out), no path/label preferences, one concurrent claim, `neutral` discovery. Parsing is a separate module. diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 031283c159..eb9cdaba6e 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -17,3 +17,8 @@ export { type AiPolicySource, type AiPolicyVerdict, } from "./ai-policy-map.js"; +export { + DEFAULT_MINER_GOAL_SPEC, + type MinerGoalSpec, + type MinerIssueDiscoveryPolicy, +} from "./miner-goal-spec.js"; diff --git a/packages/gittensory-engine/src/miner-goal-spec.ts b/packages/gittensory-engine/src/miner-goal-spec.ts new file mode 100644 index 0000000000..17f2f97988 --- /dev/null +++ b/packages/gittensory-engine/src/miner-goal-spec.ts @@ -0,0 +1,62 @@ +// MinerGoalSpec (#2293). The type surface for `.gittensory-miner.yml` — the per-repo config a maintainer/repo-owner +// drops in to tell an autonomous miner what to look for and how to behave when targeting their repo. This is the +// MINER-side analogue of the review-side `.gittensory.yml` focus manifest (see `src/signals/focus-manifest.ts`'s +// `FocusManifest`): a small typed config object paired with an explicit safe-defaults constant. +// +// This module is TYPES ONLY — no parsing, no IO. The parser (validation + safe-default coercion of raw YAML) is a +// separate follow-up issue; keeping the shape small here is deliberate, because it is easy to add a field later and +// painful to remove one contributors already rely on. Field names/semantics that overlap the review side are +// carried over verbatim from `.gittensory.yml` so the two manifests stay obviously paired. + +/** How strongly opening discovery issues is encouraged for this repo. Mirrors the review-side policy vocabulary. */ +export type MinerIssueDiscoveryPolicy = "encouraged" | "neutral" | "discouraged"; + +/** Per-repo miner configuration parsed from `.gittensory-miner.yml`. See {@link DEFAULT_MINER_GOAL_SPEC}. */ +export type MinerGoalSpec = { + /** + * Whether this repo permits autonomous miners at all. Explicit OPT-OUT, not opt-in: a public repo with no + * `.gittensory-miner.yml` is still minable, mirroring `.gittensory.yml`'s "safe by default" stance. Set `false` + * to halt all miner targeting of this repo. Default: true. + */ + minerEnabled: boolean; + /** + * Work areas the maintainer wants a miner to focus on; a candidate touching these is preferred. Glob list. + * Default: [] (no preference). + */ + wantedPaths: string[]; + /** + * Paths off-limits to a miner. A candidate touching one of these should be skipped. Glob list. + * Default: [] (nothing blocked). + */ + blockedPaths: string[]; + /** + * Issue/PR labels the maintainer prefers a miner to target; a candidate carrying one is favored. String list. + * Default: [] (no preference). + */ + preferredLabels: string[]; + /** + * Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize + * a repo's queue. Default: 1. + */ + maxConcurrentClaims: number; + /** + * How strongly this repo encourages a miner to open discovery issues. Values: encouraged | neutral | discouraged. + * Default: neutral. + */ + issueDiscoveryPolicy: MinerIssueDiscoveryPolicy; +}; + +/** + * The safe defaults applied when a field is absent from `.gittensory-miner.yml` (or the file itself is missing). + * Every value here matches the "Default: X" documented on its field above. Analogous to the defaults constant that + * accompanies `FocusManifest` in `src/signals/focus-manifest.ts` — a repo with no file behaves as if it declared + * this: minable, with no path/label preferences, one concurrent claim, and neutral discovery. + */ +export const DEFAULT_MINER_GOAL_SPEC: MinerGoalSpec = { + minerEnabled: true, + wantedPaths: [], + blockedPaths: [], + preferredLabels: [], + maxConcurrentClaims: 1, + issueDiscoveryPolicy: "neutral", +}; diff --git a/packages/gittensory-engine/test/miner-goal-spec.test.ts b/packages/gittensory-engine/test/miner-goal-spec.test.ts new file mode 100644 index 0000000000..aa25babf3f --- /dev/null +++ b/packages/gittensory-engine/test/miner-goal-spec.test.ts @@ -0,0 +1,44 @@ +// Tests for the MinerGoalSpec type contract (#2293). Types-only module, so these assert (a) the safe-defaults +// constant's runtime values, (b) that it satisfies the MinerGoalSpec type, and (c) a lightweight "every field is +// documented with a Default:" lint over the source — not parser behavior (that lands in a separate issue). +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { DEFAULT_MINER_GOAL_SPEC, type MinerGoalSpec } from "../dist/index.js"; + +// Compile-time contract: the exported default must satisfy MinerGoalSpec (fails `tsc` if the shape drifts). +const _contract: MinerGoalSpec = DEFAULT_MINER_GOAL_SPEC; +void _contract; + +test("DEFAULT_MINER_GOAL_SPEC carries the documented safe defaults", () => { + assert.deepEqual(DEFAULT_MINER_GOAL_SPEC, { + minerEnabled: true, // opt-out, not opt-in: a repo with no file is still minable + wantedPaths: [], + blockedPaths: [], + preferredLabels: [], + maxConcurrentClaims: 1, + issueDiscoveryPolicy: "neutral", + }); +}); + +test("DEFAULT_MINER_GOAL_SPEC exposes exactly the specified field surface", () => { + assert.deepEqual(Object.keys(DEFAULT_MINER_GOAL_SPEC).sort(), [ + "blockedPaths", + "issueDiscoveryPolicy", + "maxConcurrentClaims", + "minerEnabled", + "preferredLabels", + "wantedPaths", + ]); +}); + +test("every MinerGoalSpec field is documented with a JSDoc 'Default:' in the source", () => { + const source = readFileSync(new URL("../src/miner-goal-spec.ts", import.meta.url), "utf8"); + for (const field of Object.keys(DEFAULT_MINER_GOAL_SPEC)) { + // Grab the JSDoc block immediately preceding the field declaration inside the type. + const doc = source.match(new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)\\s*\\n\\s*${field}:`)); + const jsdoc = doc?.[1]; + assert.ok(jsdoc, `field '${field}' should have a JSDoc block`); + assert.match(jsdoc, /Default:/, `field '${field}' JSDoc should state its Default:`); + } +}); From a097840c7d216ef9294b7f5afbb02284ae7547b7 Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Thu, 2 Jul 2026 07:44:03 -0500 Subject: [PATCH 2/4] refactor(miner-foundation): freeze the default spec + document the claims range (#2293) - Make the array fields `readonly string[]` and deep-freeze DEFAULT_MINER_GOAL_SPEC (object + arrays) so the shared singleton can't be mutated; a test asserts the freeze. Callers clone before layering repo overrides. - Document maxConcurrentClaims as a positive integer (>= 1) with the parser expected to floor/reject below-1 values. npm test: 19/19 pass. --- .../gittensory-engine/src/miner-goal-spec.ts | 22 +++++++++++-------- .../test/miner-goal-spec.test.ts | 7 ++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/gittensory-engine/src/miner-goal-spec.ts b/packages/gittensory-engine/src/miner-goal-spec.ts index 17f2f97988..5bc596fa68 100644 --- a/packages/gittensory-engine/src/miner-goal-spec.ts +++ b/packages/gittensory-engine/src/miner-goal-spec.ts @@ -23,20 +23,21 @@ export type MinerGoalSpec = { * Work areas the maintainer wants a miner to focus on; a candidate touching these is preferred. Glob list. * Default: [] (no preference). */ - wantedPaths: string[]; + wantedPaths: readonly string[]; /** * Paths off-limits to a miner. A candidate touching one of these should be skipped. Glob list. * Default: [] (nothing blocked). */ - blockedPaths: string[]; + blockedPaths: readonly string[]; /** * Issue/PR labels the maintainer prefers a miner to target; a candidate carrying one is favored. String list. * Default: [] (no preference). */ - preferredLabels: string[]; + preferredLabels: readonly string[]; /** * Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize - * a repo's queue. Default: 1. + * a repo's queue. A positive integer (`>= 1`); the parser is expected to floor/round and reject values below 1. + * Default: 1. */ maxConcurrentClaims: number; /** @@ -51,12 +52,15 @@ export type MinerGoalSpec = { * Every value here matches the "Default: X" documented on its field above. Analogous to the defaults constant that * accompanies `FocusManifest` in `src/signals/focus-manifest.ts` — a repo with no file behaves as if it declared * this: minable, with no path/label preferences, one concurrent claim, and neutral discovery. + * + * Deep-frozen: this is a shared singleton, so runtime code can read it freely but must not mutate it — clone before + * layering repo-specific overrides on top. */ -export const DEFAULT_MINER_GOAL_SPEC: MinerGoalSpec = { +export const DEFAULT_MINER_GOAL_SPEC: MinerGoalSpec = Object.freeze({ minerEnabled: true, - wantedPaths: [], - blockedPaths: [], - preferredLabels: [], + wantedPaths: Object.freeze([]), + blockedPaths: Object.freeze([]), + preferredLabels: Object.freeze([]), maxConcurrentClaims: 1, issueDiscoveryPolicy: "neutral", -}; +}); diff --git a/packages/gittensory-engine/test/miner-goal-spec.test.ts b/packages/gittensory-engine/test/miner-goal-spec.test.ts index aa25babf3f..af99981d96 100644 --- a/packages/gittensory-engine/test/miner-goal-spec.test.ts +++ b/packages/gittensory-engine/test/miner-goal-spec.test.ts @@ -21,6 +21,13 @@ test("DEFAULT_MINER_GOAL_SPEC carries the documented safe defaults", () => { }); }); +test("DEFAULT_MINER_GOAL_SPEC is deep-frozen so the shared singleton can't be mutated", () => { + assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC)); + assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.wantedPaths)); + assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.blockedPaths)); + assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.preferredLabels)); +}); + test("DEFAULT_MINER_GOAL_SPEC exposes exactly the specified field surface", () => { assert.deepEqual(Object.keys(DEFAULT_MINER_GOAL_SPEC).sort(), [ "blockedPaths", From d76e44e6ab476ac5bd8c583eb2a9a6b201c18812 Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Thu, 2 Jul 2026 07:49:48 -0500 Subject: [PATCH 3/4] docs(miner-foundation): pin one integer-coercion rule + confirm test path (#2293) - maxConcurrentClaims contract now names a single coercion rule (floor a non-integer via Math.floor, reject below 1) instead of the ambiguous "floor/round", since the comment is the parser contract. - Comment the field-documentation test to confirm dist-test/ is a sibling of src/ so ../src/ resolves independent of emit details (readFileSync fails loud otherwise). npm test: 19/19 pass. --- packages/gittensory-engine/src/miner-goal-spec.ts | 4 ++-- packages/gittensory-engine/test/miner-goal-spec.test.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/gittensory-engine/src/miner-goal-spec.ts b/packages/gittensory-engine/src/miner-goal-spec.ts index 5bc596fa68..a8ba324c39 100644 --- a/packages/gittensory-engine/src/miner-goal-spec.ts +++ b/packages/gittensory-engine/src/miner-goal-spec.ts @@ -36,8 +36,8 @@ export type MinerGoalSpec = { preferredLabels: readonly string[]; /** * Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize - * a repo's queue. A positive integer (`>= 1`); the parser is expected to floor/round and reject values below 1. - * Default: 1. + * a repo's queue. A positive integer (`>= 1`); the parser is expected to floor a non-integer toward zero + * (`Math.floor`) and reject any value below 1. Default: 1. */ maxConcurrentClaims: number; /** diff --git a/packages/gittensory-engine/test/miner-goal-spec.test.ts b/packages/gittensory-engine/test/miner-goal-spec.test.ts index af99981d96..6c57ae5257 100644 --- a/packages/gittensory-engine/test/miner-goal-spec.test.ts +++ b/packages/gittensory-engine/test/miner-goal-spec.test.ts @@ -40,6 +40,9 @@ test("DEFAULT_MINER_GOAL_SPEC exposes exactly the specified field surface", () = }); test("every MinerGoalSpec field is documented with a JSDoc 'Default:' in the source", () => { + // The suite compiles to dist-test/ (tsconfig.test.json outDir), a sibling of src/, so ../src/ from this file's + // runtime location resolves to the TypeScript source. readFileSync throws loudly if that layout ever changes, so + // this test can't silently pass on a bad path. const source = readFileSync(new URL("../src/miner-goal-spec.ts", import.meta.url), "utf8"); for (const field of Object.keys(DEFAULT_MINER_GOAL_SPEC)) { // Grab the JSDoc block immediately preceding the field declaration inside the type. From ab71d36cf715cfbeea9fbf7cfa356c54707a3591 Mon Sep 17 00:00:00 2001 From: Nick M <274344962+nickmopen@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:03:04 -0500 Subject: [PATCH 4/4] refactor(miner-foundation): type DEFAULT_MINER_GOAL_SPEC as Readonly (#2293) The default was runtime-frozen but typed mutable, so DEFAULT.maxConcurrentClaims = 2 compiled and only failed at runtime. Type it Readonly so the compile-time API matches the frozen runtime contract. Also escape field names before interpolating them into the source-lint RegExp, so that helper stays safe if reused on a less controlled field surface. npm test: 19/19 pass. --- packages/gittensory-engine/src/miner-goal-spec.ts | 2 +- packages/gittensory-engine/test/miner-goal-spec.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/gittensory-engine/src/miner-goal-spec.ts b/packages/gittensory-engine/src/miner-goal-spec.ts index a8ba324c39..973bdae566 100644 --- a/packages/gittensory-engine/src/miner-goal-spec.ts +++ b/packages/gittensory-engine/src/miner-goal-spec.ts @@ -56,7 +56,7 @@ export type MinerGoalSpec = { * Deep-frozen: this is a shared singleton, so runtime code can read it freely but must not mutate it — clone before * layering repo-specific overrides on top. */ -export const DEFAULT_MINER_GOAL_SPEC: MinerGoalSpec = Object.freeze({ +export const DEFAULT_MINER_GOAL_SPEC: Readonly = Object.freeze({ minerEnabled: true, wantedPaths: Object.freeze([]), blockedPaths: Object.freeze([]), diff --git a/packages/gittensory-engine/test/miner-goal-spec.test.ts b/packages/gittensory-engine/test/miner-goal-spec.test.ts index 6c57ae5257..329b4a933f 100644 --- a/packages/gittensory-engine/test/miner-goal-spec.test.ts +++ b/packages/gittensory-engine/test/miner-goal-spec.test.ts @@ -44,9 +44,11 @@ test("every MinerGoalSpec field is documented with a JSDoc 'Default:' in the sou // runtime location resolves to the TypeScript source. readFileSync throws loudly if that layout ever changes, so // this test can't silently pass on a bad path. const source = readFileSync(new URL("../src/miner-goal-spec.ts", import.meta.url), "utf8"); + const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); for (const field of Object.keys(DEFAULT_MINER_GOAL_SPEC)) { - // Grab the JSDoc block immediately preceding the field declaration inside the type. - const doc = source.match(new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)\\s*\\n\\s*${field}:`)); + // Grab the JSDoc block immediately preceding the field declaration inside the type. Field names are escaped + // before interpolation so the pattern stays safe if it is ever reused for a less controlled field surface. + const doc = source.match(new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)\\s*\\n\\s*${escapeRe(field)}:`)); const jsdoc = doc?.[1]; assert.ok(jsdoc, `field '${field}' should have a JSDoc block`); assert.match(jsdoc, /Default:/, `field '${field}' JSDoc should state its Default:`);