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
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/lib/command-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Generated by scripts/gen-command-reference.mjs. Do not edit manually.
// Regenerate via `npm run command-reference`.
export const PUBLIC_COMMAND_LIST =
"@gittensory help\n@gittensory ask\n@gittensory preflight\n@gittensory blockers\n@gittensory duplicate-check\n@gittensory miner-context\n@gittensory next-action\n@gittensory reviewability\n@gittensory repo-fit\n@gittensory packet";

export const MAINTAINER_COMMAND_LIST =
"@gittensory queue-summary\n@gittensory confirmed-miners\n@gittensory review-now\n@gittensory needs-author\n@gittensory duplicate-clusters\n@gittensory burden-forecast\n@gittensory intake-health\n@gittensory outcome-patterns\n@gittensory noise-report";
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";

import { DocsPage } from "@/components/site/docs-page";
import { Callout, CodeBlock, FeatureRow } from "@/components/site/primitives";
import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";

export const Route = createFileRoute("/docs/maintainer-install-trust")({
head: () => ({
Expand Down Expand Up @@ -137,14 +138,12 @@ POST /v1/repos/:owner/:repo/settings-preview`}
Maintainer commands should be treated like privileged review actions. Use them to fetch
context on demand, not to create always-on public scoring.
</p>
<CodeBlock
code={`@gittensory help
@gittensory preflight
@gittensory blockers
@gittensory duplicate-check
@gittensory miner-context
@gittensory next-action`}
/>
<CodeBlock code={PUBLIC_COMMAND_LIST} />
<p>
A separate maintainer-only queue-digest command family defaults to maintainers and
collaborators only:
</p>
<CodeBlock code={MAINTAINER_COMMAND_LIST} />
<p>
If a command would include private reviewability, private scoreability, duplicate-risk, or
contributor-history context, the result must stay in maintainer-visible surfaces. Public
Expand Down
15 changes: 7 additions & 8 deletions apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { DocsPage } from "@/components/site/docs-page";
import { CodeBlock, Callout } from "@/components/site/primitives";
import { WorkflowMirror, type MirroredStep } from "@/components/site/workflow-mirror";
import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";

export const Route = createFileRoute("/docs/maintainer-workflow")({
head: () => ({
Expand Down Expand Up @@ -149,14 +150,12 @@ GET /v1/repos/:owner/:repo/registration-readiness`}
Maintainers (and only maintainers) can trigger context with a comment. Output is scoped to
maintainer-visible packets when appropriate.
</p>
<CodeBlock
code={`@gittensory help
@gittensory preflight
@gittensory blockers
@gittensory duplicate-check
@gittensory miner-context
@gittensory next-action`}
/>
<CodeBlock code={PUBLIC_COMMAND_LIST} />
<p>
A separate maintainer-only queue-digest family defaults to maintainers and collaborators
only:
</p>
<CodeBlock code={MAINTAINER_COMMAND_LIST} />

<Callout variant="safety">
Public-facing comments are sanitized before they leave the Worker. Private scoring, reward,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"selfhost:postgres:migrate": "tsx scripts/migrate-selfhost-sqlite-to-postgres.ts",
"selfhost:env-reference": "node scripts/gen-selfhost-env-reference.mjs",
"selfhost:env-reference:check": "node scripts/gen-selfhost-env-reference.mjs --check",
"command-reference": "node scripts/gen-command-reference.mjs",
"command-reference:check": "node scripts/gen-command-reference.mjs --check",
"selfhost:validate-observability": "node scripts/validate-observability-configs.mjs",
"selfhost:config-lint": "tsx scripts/gittensory-config-lint.ts",
"cf-typegen": "wrangler types && perl -pi -e 's/[[:blank:]]+$//' worker-configuration.d.ts",
Expand Down Expand Up @@ -69,7 +71,7 @@
"test:smoke:observability": "node scripts/smoke-observability-traces.mjs",
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci && npm run changelog:check:mcp",
"test:watch": "vitest",
Expand Down
39 changes: 39 additions & 0 deletions scripts/gen-command-reference.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export type CommandCatalogEntry = {
id: string;
title: string;
description: string;
};

export type CommandCatalogOptions = {
rootDir?: string;
sourcePath?: string;
};

export type WriteCommandReferenceOptions = CommandCatalogOptions & {
outputPath?: string;
check?: boolean;
};

export declare const DEFAULT_SOURCE_PATH: string;
export declare const DEFAULT_OUTPUT_PATH: string;

export declare function extractCatalogEntries(sourceText: string, catalogConstName: string): CommandCatalogEntry[];

export declare function renderCommandList(entries: CommandCatalogEntry[]): string;

export declare function collectCommandCatalogs(options?: CommandCatalogOptions): {
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
};

export declare function renderCommandReferenceModule(catalogs: {
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
}): string;

export declare function writeCommandReference(options?: WriteCommandReferenceOptions): {
changed: boolean;
outputPath: string;
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
};
102 changes: 102 additions & 0 deletions scripts/gen-command-reference.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node
// Generates the @gittensory command reference from its single source of truth --
// src/github/commands.ts's PUBLIC_MENTION_COMMAND_CATALOG and MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG
// -- so docs pages render the real command list instead of hand-copying it (a hand-copy is exactly how
// the 9-command maintainer-only queue-digest family went completely undocumented; see #3046). Mirrors
// scripts/gen-selfhost-env-reference.mjs's generate/--check dual-mode convention.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";

export const DEFAULT_SOURCE_PATH = "src/github/commands.ts";
export const DEFAULT_OUTPUT_PATH = "apps/gittensory-ui/src/lib/command-reference.ts";

const PUBLIC_CATALOG_NAME = "PUBLIC_MENTION_COMMAND_CATALOG";
const MAINTAINER_CATALOG_NAME = "MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG";

// Self-defense floor: 10 public + 9 maintainer-only commands exist today. If extraction ever finds fewer
// than 15 total entries across both catalogs, the extraction regex is almost certainly broken -- fail
// loudly instead of silently writing an empty/tiny reference (mirrors check-docs-drift.mjs's own floor).
const MIN_TOTAL_COMMANDS = 15;

/** Find the array literal assigned to `const <catalogConstName> = [ ... ] as const;` (non-greedy up to
* the FIRST `] as const;` after the const name -- catalogs in commands.ts never nest another
* `] as const;` inside themselves, so the first close is always the right one) and extract every
* `{ id: "...", title: "...", description: "..." }` entry from within that slice, in source order.
* Scoped to the named catalog's own slice so two catalogs in the same file never bleed into each
* other's entries. */
export function extractCatalogEntries(sourceText, catalogConstName) {
const catalogPattern = new RegExp(`const\\s+${catalogConstName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s*const;`);
const catalogMatch = catalogPattern.exec(sourceText);
if (!catalogMatch) return [];
const entryPattern = /id:\s*"([^"]+)"\s*,\s*title:\s*"([^"]+)"\s*,\s*description:\s*"([^"]+)"/g;
return [...catalogMatch[1].matchAll(entryPattern)].map((match) => ({
id: match[1],
title: match[2],
description: match[3],
}));
}

/** Renders a catalog's entries as the same `@gittensory <id>` line format the docs pages already
* hand-write, newline-joined, in catalog order. */
export function renderCommandList(entries) {
return entries.map((entry) => `@gittensory ${entry.id}`).join("\n");
}

export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = DEFAULT_SOURCE_PATH } = {}) {
const sourceText = readFileSync(resolve(rootDir, sourcePath), "utf8");
const publicCommands = extractCatalogEntries(sourceText, PUBLIC_CATALOG_NAME);
const maintainerCommands = extractCatalogEntries(sourceText, MAINTAINER_CATALOG_NAME);
const total = publicCommands.length + maintainerCommands.length;
if (total < MIN_TOTAL_COMMANDS) {
throw new Error(
`gen-command-reference: extraction found only ${total} total @gittensory command(s) across both catalogs in ${sourcePath} -- expected ${MIN_TOTAL_COMMANDS}+; the extraction regex may be broken`,
);
}
return { publicCommands, maintainerCommands };
}

export function renderCommandReferenceModule({ publicCommands, maintainerCommands }) {
return `// Generated by scripts/gen-command-reference.mjs. Do not edit manually.
// Regenerate via \`npm run command-reference\`.
export const PUBLIC_COMMAND_LIST =
${JSON.stringify(renderCommandList(publicCommands))};

export const MAINTAINER_COMMAND_LIST =
${JSON.stringify(renderCommandList(maintainerCommands))};
`;
}

export function writeCommandReference({
rootDir = process.cwd(),
sourcePath = DEFAULT_SOURCE_PATH,
outputPath = DEFAULT_OUTPUT_PATH,
check = false,
} = {}) {
const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir, sourcePath });
const output = renderCommandReferenceModule({ publicCommands, maintainerCommands });
const absOutput = resolve(rootDir, outputPath);
const current = existsSync(absOutput) ? readFileSync(absOutput, "utf8") : null;
const changed = current !== output;
if (!check && changed) {
mkdirSync(dirname(absOutput), { recursive: true });
writeFileSync(absOutput, output);
}
return { changed, outputPath, publicCommands, maintainerCommands };
}

function main(argv) {
const check = argv.includes("--check");
const result = writeCommandReference({ check });
if (check && result.changed) {
process.stderr.write(`gen-command-reference: ${result.outputPath} is stale; run npm run command-reference.\n`);
process.exit(1);
}
process.stdout.write(
`gen-command-reference: ${check ? "checked" : "wrote"} ${result.publicCommands.length} public + ${result.maintainerCommands.length} maintainer-only command references in ${result.outputPath}\n`,
);
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main(process.argv.slice(2));
}
157 changes: 157 additions & 0 deletions test/unit/gen-command-reference-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import {
type CommandCatalogEntry,
collectCommandCatalogs,
DEFAULT_OUTPUT_PATH,
extractCatalogEntries,
renderCommandList,
renderCommandReferenceModule,
} from "../../scripts/gen-command-reference.mjs";

function fixtureRoot(commandsSource: string): string {
const root = mkdtempSync(join(tmpdir(), "gt-command-reference-"));
mkdirSync(join(root, "src", "github"), { recursive: true });
writeFileSync(join(root, "src", "github", "commands.ts"), commandsSource);
return root;
}

describe("gen-command-reference script (#3046)", () => {
describe("extractCatalogEntries", () => {
const fixture = `
const PUBLIC_MENTION_COMMAND_CATALOG = [
{ id: "help", title: "Help", description: "Show help." },
{ id: "ask", title: "Ask", description: "Answer a question." },
] as const;

const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [
{ id: "queue-summary", title: "Queue summary", description: "Post a queue digest." },
] as const;
`;

it("extracts only the entries from the named catalog, not the other one, in source order", () => {
expect(extractCatalogEntries(fixture, "PUBLIC_MENTION_COMMAND_CATALOG")).toEqual([
{ id: "help", title: "Help", description: "Show help." },
{ id: "ask", title: "Ask", description: "Answer a question." },
]);
expect(extractCatalogEntries(fixture, "MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG")).toEqual([
{ id: "queue-summary", title: "Queue summary", description: "Post a queue digest." },
]);
});

it("returns an empty array when the named catalog does not exist", () => {
expect(extractCatalogEntries(fixture, "MISSING_CATALOG")).toEqual([]);
});
});

describe("renderCommandList", () => {
it("renders one @gittensory <id> line per entry, newline-joined, in order", () => {
expect(
renderCommandList([
{ id: "help", title: "Help", description: "Show help." },
{ id: "ask", title: "Ask", description: "Answer a question." },
]),
).toBe("@gittensory help\n@gittensory ask");
});

it("renders an empty string for an empty catalog", () => {
expect(renderCommandList([])).toBe("");
});
});

describe("collectCommandCatalogs", () => {
it("self-defends against a broken extraction regex (fewer than 15 total commands)", () => {
const root = fixtureRoot(`
const PUBLIC_MENTION_COMMAND_CATALOG = [{ id: "only-one", title: "Only", description: "One." }] as const;
const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [] as const;
`);

expect(() => collectCommandCatalogs({ rootDir: root })).toThrow(/extraction regex may be broken/);
});

it("extracts a fixture with exactly 15 total commands without throwing", () => {
const publicIds = Array.from({ length: 10 }, (_, i) => `{ id: "public-${i}", title: "Public ${i}", description: "Desc ${i}." },`).join("\n");
const maintainerIds = Array.from({ length: 5 }, (_, i) => `{ id: "maint-${i}", title: "Maint ${i}", description: "Desc ${i}." },`).join("\n");
const root = fixtureRoot(`
const PUBLIC_MENTION_COMMAND_CATALOG = [
${publicIds}
] as const;
const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [
${maintainerIds}
] as const;
`);

const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir: root });
expect(publicCommands).toHaveLength(10);
expect(maintainerCommands).toHaveLength(5);
});

it("extracts the real 10 public + 9 maintainer-only commands from the real repo source", () => {
const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir: process.cwd() });

expect(publicCommands).toHaveLength(10);
expect(maintainerCommands).toHaveLength(9);
expect(publicCommands.map((c: CommandCatalogEntry) => c.id)).toEqual([
"help",
"ask",
"preflight",
"blockers",
"duplicate-check",
"miner-context",
"next-action",
"reviewability",
"repo-fit",
"packet",
]);
expect(maintainerCommands.map((c: CommandCatalogEntry) => c.id)).toEqual([
"queue-summary",
"confirmed-miners",
"review-now",
"needs-author",
"duplicate-clusters",
"burden-forecast",
"intake-health",
"outcome-patterns",
"noise-report",
]);
});
});

describe("renderCommandReferenceModule", () => {
it("renders a generated-file header plus both exported string constants", () => {
const output = renderCommandReferenceModule({
publicCommands: [{ id: "help", title: "Help", description: "Show help." }],
maintainerCommands: [{ id: "queue-summary", title: "Queue summary", description: "Post a digest." }],
});

expect(output).toContain("// Generated by scripts/gen-command-reference.mjs. Do not edit manually.");
expect(output).toContain("npm run command-reference");
expect(output).toContain('export const PUBLIC_COMMAND_LIST =\n "@gittensory help";');
expect(output).toContain('export const MAINTAINER_COMMAND_LIST =\n "@gittensory queue-summary";');
});
});

// Most important regression test in this file: proves the committed generated file on disk is
// byte-identical to what the generator's pure function would produce right now from the real
// src/github/commands.ts. If this fails, either the generated file is stale (run
// `npm run command-reference` and commit it) or the extraction logic changed -- either way, the
// generated file must not be hand-edited to make this test pass.
it("the committed generated file matches what the generator would produce right now (regression guard)", () => {
const rootDir = process.cwd();
const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir });
const expected = renderCommandReferenceModule({ publicCommands, maintainerCommands });

const actual = readFileSync(resolve(rootDir, DEFAULT_OUTPUT_PATH), "utf8");

expect(actual).toBe(expected);
});

it("prints a clean summary and exits 0 for the real repo state when run as a subprocess with --check", () => {
const output = execFileSync("node", ["scripts/gen-command-reference.mjs", "--check"], { encoding: "utf8" });

expect(output).toMatch(/gen-command-reference: checked \d+ public \+ \d+ maintainer-only command references in/);
});
});
Loading