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
4 changes: 4 additions & 0 deletions packages/gittensory-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {

Check notice on line 30 in packages/gittensory-engine/package.json

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./miner-goal-spec-parse": {
"types": "./dist/miner-goal-spec-parse.d.ts",
"default": "./dist/miner-goal-spec-parse.js"
}
},
"files": [
Expand Down
104 changes: 104 additions & 0 deletions packages/gittensory-engine/src/miner-goal-spec-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {

Check notice on line 1 in packages/gittensory-engine/src/miner-goal-spec-parse.ts

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
DEFAULT_MINER_GOAL_SPEC,
type MinerGoalSpec,
type MinerIssueDiscoveryPolicy,
} from "./miner-goal-spec.js";

export type MinerGoalSpecParseResult = {
present: boolean;
spec: Readonly<MinerGoalSpec>;
warnings: readonly string[];
};

const MAX_LIST_ENTRIES = 200;
const MAX_STRING_LEN = 300;
const POLICIES = new Set<MinerIssueDiscoveryPolicy>(["encouraged", "neutral", "discouraged"]);

function freezeSpec(spec: MinerGoalSpec): Readonly<MinerGoalSpec> {
return Object.freeze({
...spec,
wantedPaths: Object.freeze([...spec.wantedPaths]),
blockedPaths: Object.freeze([...spec.blockedPaths]),
preferredLabels: Object.freeze([...spec.preferredLabels]),
});
}

function parseStringList(value: unknown, field: string, warnings: string[]): string[] {
if (value === undefined) return [];
if (!Array.isArray(value)) {
warnings.push(`MinerGoalSpec field "${field}" must be an array; ignoring.`);
return [];
}
const seen = new Set<string>();
const out: string[] = [];
for (const entry of value) {
if (typeof entry !== "string") {
warnings.push(`MinerGoalSpec field "${field}" entries must be strings; skipping non-string.`);
continue;
}
const trimmed = entry.trim();
if (!trimmed || trimmed.length > MAX_STRING_LEN) continue;
if (seen.has(trimmed)) continue;
seen.add(trimmed);
out.push(trimmed);
if (out.length >= MAX_LIST_ENTRIES) break;
}
return out;
}

function parseBoolean(value: unknown, field: string, fallback: boolean, warnings: string[]): boolean {
if (value === undefined) return fallback;
if (typeof value === "boolean") return value;
warnings.push(`MinerGoalSpec field "${field}" must be a boolean; using default.`);
return fallback;
}

function parseClaims(value: unknown, warnings: string[]): number {
if (value === undefined) return DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims;
if (typeof value !== "number" || !Number.isFinite(value)) {
warnings.push(`MinerGoalSpec field "maxConcurrentClaims" must be a number; using default.`);
return DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims;
}
const floored = Math.floor(value);
if (floored < 1) {
warnings.push(`MinerGoalSpec field "maxConcurrentClaims" must be >= 1; using 1.`);
return 1;
}
return floored;
}

function parsePolicy(value: unknown, warnings: string[]): MinerIssueDiscoveryPolicy {
if (value === undefined) return DEFAULT_MINER_GOAL_SPEC.issueDiscoveryPolicy;
if (typeof value !== "string") {
warnings.push(`MinerGoalSpec field "issueDiscoveryPolicy" must be a string; using neutral.`);
return "neutral";
}
const normalized = value.trim().toLowerCase();
if (POLICIES.has(normalized as MinerIssueDiscoveryPolicy)) return normalized as MinerIssueDiscoveryPolicy;
warnings.push(
`MinerGoalSpec field "issueDiscoveryPolicy" must be encouraged, neutral, or discouraged; using neutral.`,
);
return "neutral";
}

/** Parse raw JSON/YAML-decoded config into a deep-frozen {@link MinerGoalSpec}. Pure — no IO. */
export function parseMinerGoalSpec(raw: unknown): MinerGoalSpecParseResult {
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
return { present: false, spec: DEFAULT_MINER_GOAL_SPEC, warnings: [] };
}

const record = raw as Record<string, unknown>;
const warnings: string[] = [];
const present = Object.keys(record).length > 0;

const spec = freezeSpec({
minerEnabled: parseBoolean(record.minerEnabled, "minerEnabled", DEFAULT_MINER_GOAL_SPEC.minerEnabled, warnings),
wantedPaths: parseStringList(record.wantedPaths, "wantedPaths", warnings),
blockedPaths: parseStringList(record.blockedPaths, "blockedPaths", warnings),
preferredLabels: parseStringList(record.preferredLabels, "preferredLabels", warnings),
maxConcurrentClaims: parseClaims(record.maxConcurrentClaims, warnings),
issueDiscoveryPolicy: parsePolicy(record.issueDiscoveryPolicy, warnings),
});

return { present, spec, warnings: Object.freeze(warnings) };
}
63 changes: 63 additions & 0 deletions packages/gittensory-engine/test/miner-goal-spec-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { test } from "node:test";

Check notice on line 1 in packages/gittensory-engine/test/miner-goal-spec-parse.test.ts

View check run for this annotation

Loopover ORB / Gittensory Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
import assert from "node:assert/strict";

import { DEFAULT_MINER_GOAL_SPEC } from "../dist/miner-goal-spec.js";
import { parseMinerGoalSpec } from "../dist/miner-goal-spec-parse.js";

test("parseMinerGoalSpec returns defaults for missing or non-object input", () => {
for (const raw of [undefined, null, "nope", 42, []]) {
const result = parseMinerGoalSpec(raw);
assert.equal(result.present, false);
assert.equal(result.spec, DEFAULT_MINER_GOAL_SPEC);
assert.deepEqual(result.warnings, []);
}
});

test("parseMinerGoalSpec coerces a full valid object and marks present", () => {
const result = parseMinerGoalSpec({
minerEnabled: false,
wantedPaths: [" src/** ", "src/**"],
blockedPaths: ["dist/**"],
preferredLabels: [" bug ", "feature"],
maxConcurrentClaims: 3,
issueDiscoveryPolicy: "ENCOURAGED",
});

assert.equal(result.present, true);
assert.deepEqual(result.spec, {
minerEnabled: false,
wantedPaths: ["src/**"],
blockedPaths: ["dist/**"],
preferredLabels: ["bug", "feature"],
maxConcurrentClaims: 3,
issueDiscoveryPolicy: "encouraged",
});
assert.deepEqual(result.warnings, []);
assert.ok(Object.isFrozen(result.spec));
assert.ok(Object.isFrozen(result.spec.wantedPaths));
});

test("parseMinerGoalSpec floors claims and rejects values below 1", () => {
const floored = parseMinerGoalSpec({ maxConcurrentClaims: 2.9 });
assert.equal(floored.spec.maxConcurrentClaims, 2);

const zero = parseMinerGoalSpec({ maxConcurrentClaims: 0 });
assert.equal(zero.spec.maxConcurrentClaims, 1);
assert.match(zero.warnings.join(" "), /maxConcurrentClaims/);
});

test("parseMinerGoalSpec warns and falls back on malformed fields", () => {
const result = parseMinerGoalSpec({
minerEnabled: "yes",
wantedPaths: "src/**",
maxConcurrentClaims: "two",
issueDiscoveryPolicy: "aggressive",
});

assert.equal(result.present, true);
assert.equal(result.spec.minerEnabled, DEFAULT_MINER_GOAL_SPEC.minerEnabled);
assert.deepEqual(result.spec.wantedPaths, []);
assert.equal(result.spec.maxConcurrentClaims, DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims);
assert.equal(result.spec.issueDiscoveryPolicy, "neutral");
assert.ok(result.warnings.length >= 4);
});