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
34 changes: 26 additions & 8 deletions .github/scripts/arch-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { err, ok, Result } from "neverthrow";
import { type Node, parseSync } from "oxc-parser";
import { parseDocument } from "yaml";
import { z } from "zod";
import { countNoun } from "../../src/text.js";
import { countNoun, quote } from "../../src/text.js";

export const ARCHITECTURE_PATH = "architecture.yml";

Expand Down Expand Up @@ -83,15 +83,33 @@ export function parseArchitecture(root: string): Result<Architecture, string[]>
return err(doc.error);
}
const parsed = ARCHITECTURE.safeParse(doc.value);
return parsed.success
? ok(parsed.data)
: err(located(parsed.error.issues.flatMap((issue) => describeIssue(doc.value, issue))));
if (!parsed.success) {
return err(located(parsed.error.issues.flatMap((issue) => describeIssue(doc.value, issue))));
}
const merged = mergedMermaidNodes(parsed.data);
return merged.length > 0 ? err(located(merged)) : ok(parsed.data);
}

function located(problems: readonly string[]): string[] {
return problems.map((problem) => `${ARCHITECTURE_PATH}: ${problem}`);
}

/** A hyphen in a layer name is edge syntax to mermaid, so ids swap it for an underscore. */
function mermaidId(layer: string): string {
return layer.replace(/-/g, "_");
}

/** The id mapping is not injective, so two layer names can share a node; the declaration refuses that pair. */
function mergedMermaidNodes(arch: Architecture): string[] {
return [...Map.groupBy(Object.keys(arch.layers), mermaidId)].flatMap(([id, layers]) =>
layers.length > 1
? [
`layers ${layers.map(quote).join(" and ")} share the mermaid id ${id}, which would draw them as one node of the module map; rename all but one`,
]
: [],
);
}

/** For callers that render rather than lint (docs, tests): a malformed declaration is fatal to them. */
export function readArchitecture(root: string): Architecture {
return parseArchitecture(root).match(
Expand Down Expand Up @@ -279,14 +297,14 @@ export function lintArchitecture(root: string, arch = readArchitecture(root)): s
return problems;
}

/** A hyphen in a layer name is edge syntax to mermaid, so ids swap it for an underscore. */
export function renderArchitectureMermaid(arch: Architecture): string {
const id = (layer: string): string => layer.replace(/-/g, "_");
return [
"graph TD",
...Object.entries(arch.layers).map(([name, paths]) => ` ${id(name)}["${paths.join("<br>")}"]`),
...Object.entries(arch.layers).map(
([name, paths]) => ` ${mermaidId(name)}["${paths.join("<br>")}"]`,
),
...Object.entries(arch.edges).flatMap(([from, targets]) =>
targets.map((to) => ` ${id(from)} --> ${id(to)}`),
targets.map((to) => ` ${mermaidId(from)} --> ${mermaidId(to)}`),
),
].join("\n");
}
Expand Down
7 changes: 4 additions & 3 deletions .github/scripts/lib/count-word.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* The written-out counts docs prose uses ("Fifteen sections list ..."), shared by the generators
* and the docs pins. The loud throw is the tripwire: a derived list outgrowing the words fails
* the build instead of the prose silently falling back to a numeral.
* and the docs pins. Every caller counts a list the code derives (a registry, a constant), never a
* number read from a page, so a count past the words is a BUG: invariant: the derived list outgrew
* the table and the build fails instead of the prose silently falling back to a numeral.
*/

const COUNT_WORDS = [
Expand Down Expand Up @@ -32,7 +33,7 @@ export function countWord(n: number): string {
const word = COUNT_WORDS[n];
if (word === undefined) {
throw new Error(
`extend COUNT_WORDS (.github/scripts/lib/count-word.ts): no word for count ${n}`,
`BUG: extend COUNT_WORDS (.github/scripts/lib/count-word.ts): no word for count ${n}`,
);
}
return word;
Expand Down
9 changes: 9 additions & 0 deletions test/architecture/architecture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ describe("parseArchitecture", () => {
"Flow sequence in block collection must be sufficiently indented and end with a ] at line 4, column 1",
],
],
[
// Mermaid reads a hyphen as edge syntax, so the renderer swaps it for an underscore; two names one id would
// merge into a single node of the module map, silently, unless the declaration refuses them first.
"two layers the mermaid map would draw as one node",
"layers: {plain-data: [src/plain-data.ts], plain_data: [src/plain_data.ts]}\nexclude: []\nedges: {}",
[
'layers "plain-data" and "plain_data" share the mermaid id plain_data, which would draw them as one node of the module map; rename all but one',
],
],
])("%s fails naming the key", (_case, document, problems) =>
withTempDir("arch-lint-parse-", (dir) => {
expect(parse(dir, document)).toEqual(err(problems.map((p) => `${ARCHITECTURE_PATH}: ${p}`)));
Expand Down
9 changes: 6 additions & 3 deletions test/scripts/count-word.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/** The written-out counts (.github/scripts/lib/count-word.ts): the tripwire past the range, never a numeral fallback. */
/**
* The written-out counts (.github/scripts/lib/count-word.ts): the tripwire past the range is a BUG: invariant,
* never a numeral fallback, so a derived list outgrowing the words fails the build as a programming error.
*/

import { expect, test } from "bun:test";
import { countWord } from "../../.github/scripts/lib/count-word.js";

test("a count with no word throws naming the list to extend, whatever shape the count has", () => {
test("a count with no word throws a BUG: invariant naming the list to extend, whatever shape the count has", () => {
for (const count of [21, -1, 1.5, Number.NaN]) {
expect(() => countWord(count)).toThrow(
`extend COUNT_WORDS (.github/scripts/lib/count-word.ts): no word for count ${count}`,
`BUG: extend COUNT_WORDS (.github/scripts/lib/count-word.ts): no word for count ${count}`,
);
}
});
Loading