The four share files (index.ts, module.json, guard.test.ts), so they land as one commit. GIT1: seat.ts serves pull, open-pulls, tags, trees, file, contains, statuses, status, comment, view, note, merge-pull as tools on the seat, each answering the contract's shape and an error of kind refused, not-found or failed; a comment, view or note whose act id is held is answered held (note matched by id). merge-pull merges the head the caller names (its from-G judgements are GIT6, GIT7, GIT9). GIT2: seatevents.ts says pull-updated, pull-merged, pull-closed, forge-pull, forge-status, forge-delete and repo-writable on mesh.seat.git.event.<event>; the module's emits drop them. GIT3: merge.ts, check.ts, the acting tools, the owed watch, the gate's source read and check-gates-merge are gone. GIT10: catalog.ts follows the seat mesh-catalog (revision, catalog-revision); no write is granted before the first view. Builds: design 47 G §310, §311, §312, §313, §314, §163, §317, §278, §321, §196 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
85 lines
4.7 KiB
TypeScript
85 lines
4.7 KiB
TypeScript
// What the forge's holder does for a delivery (novox/hq ADR 0239): the commit's note under
|
|
// refs/notes/mesh-plan and the delivery's view on its pull request, asked through the seat git's verbs `note` and
|
|
// `view` (seat.ts). The forge is this module's; no delivery part writes to it itself.
|
|
//
|
|
// **The note is written in the forge's own repository, as the forge's own user.** The forge's API reads a
|
|
// note and writes none, and a push needs a clone and a credential nobody else should hold; the forge's
|
|
// container holds the bare repository and git. So the line is appended there, by `git notes append`, as the
|
|
// account the forge runs as — the mesh's own forge, never a person's or an agent's hand. Appending is
|
|
// idempotent here: a line the note already has is not added again, so asking twice adds nothing.
|
|
//
|
|
// Pure functions and an injectable runner, so this is tested without a forge.
|
|
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
/** How a command is run: docker on the forge's machine, or a test's. */
|
|
export type Runner = (file: string, args: string[]) => Promise<{ stdout: string; code: number }>;
|
|
|
|
const execFileP = promisify(execFile);
|
|
|
|
export const run: Runner = async (file, args) => {
|
|
try {
|
|
const { stdout } = await execFileP(file, args, { maxBuffer: 4 << 20 });
|
|
return { stdout, code: 0 };
|
|
} catch (err) {
|
|
const e = err as { code?: number; stdout?: string };
|
|
return { stdout: e.stdout ?? "", code: typeof e.code === "number" ? e.code : 1 };
|
|
}
|
|
};
|
|
|
|
const name = /^[A-Za-z0-9_.-]+$/;
|
|
const sha = /^[0-9a-fA-F]{7,64}$/;
|
|
const ref = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
|
/** A note line as it may be written: one line, bounded, nothing that is not text. */
|
|
export function noteLine(line: string): string {
|
|
const one = String(line ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
if (!one) throw new Error("an empty line is no note");
|
|
return one.length > 2000 ? one.slice(0, 1999) + "…" : one;
|
|
}
|
|
|
|
/** Where the forge keeps a repository, inside its container. */
|
|
export function gitDir(owner: string, repo: string): string {
|
|
if (!name.test(owner) || !name.test(repo)) throw new Error(`${owner}/${repo} is not a repository's name`);
|
|
return `/data/git/repositories/${owner.toLowerCase()}/${repo.toLowerCase()}.git`;
|
|
}
|
|
|
|
/** The commands the note is read and appended with, in the forge's container, as its user. */
|
|
export function noteArgs(container: string, owner: string, repo: string, commit: string, notesRef: string, line?: string): string[] {
|
|
if (!name.test(container)) throw new Error(`${container} is not a container's name`);
|
|
if (!sha.test(commit)) throw new Error(`${commit} is not a commit`);
|
|
if (!ref.test(notesRef)) throw new Error(`${notesRef} is not a notes ref`);
|
|
const base = ["exec", "-u", "git", container, "git", "--git-dir", gitDir(owner, repo),
|
|
"-c", "user.name=mesh", "-c", "user.email=mesh@mesh.invalid", "notes", `--ref=${notesRef}`];
|
|
return line === undefined ? [...base, "show", commit] : [...base, "append", "-m", noteLine(line), commit];
|
|
}
|
|
|
|
/** Append a line to a commit's note, unless the note already holds it. Answers whether it was added. */
|
|
export async function appendNote(runner: Runner, container: string, owner: string, repo: string, commit: string,
|
|
notesRef: string, line: string, heldId?: string): Promise<{ added: boolean; lines: number }> {
|
|
const wanted = noteLine(line);
|
|
const shown = await runner("docker", noteArgs(container, owner, repo, commit, notesRef));
|
|
// No note yet is git's exit 1 with nothing on stdout; anything else unreadable is said.
|
|
const lines = shown.code === 0 ? shown.stdout.split("\n").filter((l) => l.trim() !== "") : [];
|
|
// A line that carries the act's id is the act already done, whatever else it says.
|
|
if (lines.includes(wanted) || (heldId && lines.some((l) => l.includes(`[${heldId}]`)))) return { added: false, lines: lines.length };
|
|
const appended = await runner("docker", noteArgs(container, owner, repo, commit, notesRef, wanted));
|
|
if (appended.code !== 0) throw new Error(`the note on ${commit.slice(0, 8)} could not be appended (git exited ${appended.code})`);
|
|
return { added: true, lines: lines.length + 1 };
|
|
}
|
|
|
|
/** The marker that makes one comment of a pull request the delivery's view. */
|
|
export const VIEW_MARKER = "<!-- mesh-delivery:view -->";
|
|
|
|
/** The view's body, marked: mesh-delivery writes it, this keeps exactly one of them per pull request. */
|
|
export function viewBody(body: string): string {
|
|
const b = String(body ?? "");
|
|
return b.includes(VIEW_MARKER) ? b : `${VIEW_MARKER}\n${b}`;
|
|
}
|
|
|
|
/** Which comment is the view: the first that carries the marker, or none yet. */
|
|
export function viewComment<T extends { id: number; body: string }>(comments: T[]): T | undefined {
|
|
return comments.find((c) => c.body.includes(VIEW_MARKER));
|
|
}
|
|
|