Files
mesh-catalog/modules/gitea/registrations.ts
T
jschoubben bc2e5574ef GIT7, GIT8: the forge module keeps the runner's failed required steps and refuses a head by them; git_failed_steps and git_failed_steps_drop
merge-pull refuses a head at whose fingerprint a run failed a required step (policy-failed, naming run, module, step), from the
module's own state failed-steps kept from step-ended, with the seen keys of every step end and the deletion once no watched
branch's head nor open pull request's head names a commit. The two tools read it with its fill and drop a key after the hand act.

Builds: design 47 G §39, §83, §84, §85, §135, §475, §487, §548

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:01:27 +01:00

63 lines
3.4 KiB
TypeScript

// **The first registrations switch the merge on** (novox/hq design 47 §95, §353): until the catalog half has said its first
// `registration-changed`, the seat git's `merge-pull` refuses every merge naming the missing registrations, because a merge
// made with no Approve could reach the old assigner, which writes wants from the trunk with no pipeline. The module learns it
// two ways, both through the seat `mesh-catalog`: its verb `registrations` asked at the start (a restart after the first
// registrations were written), and its event `registration-changed` (the first being written, or heard again from the
// seat's kept stream). Neither is the holder's state or a module named.
import {
seat as CATALOG, verbs as CATALOG_VERBS, events as CATALOG_EVENTS,
type RegistrationChangedEvent, type RegistrationsAnswer, type RegistrationsInput,
} from "@novox/mesh-delivery-contracts-mesh-catalog";
/** The seat verb asked, as the manifest's `invokes` names it. */
export const REGISTRATIONS_VERB = `seat:${CATALOG}.${CATALOG_VERBS.registrations}`;
/** The seat event heard, as the manifest's `consumes` names it. */
export const REGISTRATION_CHANGED_EVENT = `${CATALOG}.${CATALOG_EVENTS.registrationChanged}`;
export class Registrations {
private first = false;
private byRepository = new Map<string, Set<string>>();
constructor(private readonly ask: (verb: string, input: RegistrationsInput) => Promise<unknown>,
private readonly log: (line: string) => void = () => {}) {}
/** Whether the first registrations are written: the one fact the merge switches on. */
get written(): boolean {
return this.first;
}
/** The watched branches the registrations name, by their origin repository (`<owner>/<name>`) as of the last read. */
get watched(): ReadonlyMap<string, ReadonlySet<string>> {
return this.byRepository;
}
/** One `registration-changed`: the first registration written, however many were before this module started. */
hear(ev: RegistrationChangedEvent | undefined): void {
if (!ev || typeof ev.module !== "string" || ev.module === "") {
throw new Error(`not a ${REGISTRATION_CHANGED_EVENT} event: ${JSON.stringify(ev ?? null).slice(0, 200)}`);
}
if (!this.first) this.log(`the first registration is written (${ev.module}); merge-pull merges from here`);
this.first = true;
}
/** The start's first view, asked again after each `registration-changed`: any registration held is the first written, and its
* watched branches are read. A failed ask is said and left to the event. */
async start(): Promise<void> {
try {
let a: unknown = await this.ask(REGISTRATIONS_VERB, {});
if (typeof a === "string") a = JSON.parse(a);
const r = (a as RegistrationsAnswer | undefined)?.registrations;
if (!Array.isArray(r)) throw new Error(`${REGISTRATIONS_VERB} answered no registrations: ${JSON.stringify(a ?? null).slice(0, 200)}`);
const by = new Map<string, Set<string>>();
for (const x of r) for (const b of x.branches ?? []) (by.get(x.origin) ?? by.set(x.origin, new Set()).get(x.origin)!).add(b);
this.byRepository = by;
if (r.length > 0) {
this.first = true;
this.log(`${r.length} registrations are held; merge-pull merges`);
}
} catch (err) {
this.log(`the registrations could not be read: ${err instanceof Error ? err.message : String(err)}; merge-pull waits for registration-changed`);
}
}
}