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
6 changes: 6 additions & 0 deletions packages/gittensory-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 5 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
66 changes: 66 additions & 0 deletions packages/gittensory-engine/src/miner-goal-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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: readonly string[];
/**
* Paths off-limits to a miner. A candidate touching one of these should be skipped. Glob list.
* Default: [] (nothing blocked).
*/
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: 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 a non-integer toward zero
* (`Math.floor`) and reject any value below 1. 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.
*
* 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: Readonly<MinerGoalSpec> = Object.freeze({
minerEnabled: true,
wantedPaths: Object.freeze([]),
blockedPaths: Object.freeze([]),
preferredLabels: Object.freeze([]),
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
56 changes: 56 additions & 0 deletions packages/gittensory-engine/test/miner-goal-spec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 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",
"issueDiscoveryPolicy",
"maxConcurrentClaims",
"minerEnabled",
"preferredLabels",
"wantedPaths",
]);
});

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");
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. 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:`);
}
});