fix: complete state-contract naming cleanup
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
checkFindings,
|
||||
digest,
|
||||
type ExceptionRule,
|
||||
findNames,
|
||||
forbiddenWord,
|
||||
} from "./check-state-contract-names.ts";
|
||||
function assert(condition: boolean, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
const spelling = `${forbiddenWord.toUpperCase()}Conflict`;
|
||||
|
||||
Deno.test("case and compound names are rejected in content and file names", async () => {
|
||||
const found = await findNames(
|
||||
`src/${spelling}.rs`,
|
||||
`enum Error { ${spelling} }`,
|
||||
);
|
||||
assert(found.length === 2, "file name and content must both be inspected");
|
||||
assert(
|
||||
checkFindings(found, []).unexpected.length === 2,
|
||||
"both uses need review",
|
||||
);
|
||||
});
|
||||
Deno.test("saved-data exceptions match an exact line in exactly one file", async () => {
|
||||
const line = `SELECT old_${forbiddenWord} FROM archived_rows;`;
|
||||
const rules: ExceptionRule[] = [{
|
||||
path: "migration.sql",
|
||||
kind: "saved_data",
|
||||
reason: "Read the persisted old column before dropping it",
|
||||
lines: [{ sha256: await digest(line), count: 1 }],
|
||||
}];
|
||||
assert(
|
||||
checkFindings(await findNames("migration.sql", line), rules).unexpected
|
||||
.length === 0,
|
||||
"reviewed boundary should pass",
|
||||
);
|
||||
const copied = checkFindings(await findNames("current.sql", line), rules);
|
||||
assert(
|
||||
copied.unexpected.length === 1 && copied.stale.length === 1,
|
||||
"copying an allowed line elsewhere must fail",
|
||||
);
|
||||
const changed = checkFindings(
|
||||
await findNames("migration.sql", `${line} -- changed`),
|
||||
rules,
|
||||
);
|
||||
assert(
|
||||
changed.unexpected.length === 1 && changed.stale.length === 1,
|
||||
"changed boundary requires another review",
|
||||
);
|
||||
});
|
||||
Deno.test("duplicate uses cannot borrow the same exception and deleted uses leave stale exceptions", async () => {
|
||||
const line = `old_${forbiddenWord}`;
|
||||
const rules: ExceptionRule[] = [{
|
||||
path: "wire.rs",
|
||||
kind: "saved_data",
|
||||
reason: "Deserialize a saved key",
|
||||
lines: [{ sha256: await digest(line), count: 1 }],
|
||||
}];
|
||||
assert(
|
||||
checkFindings(await findNames("wire.rs", `${line}\n${line}`), rules)
|
||||
.unexpected.length === 1,
|
||||
"extra copy must fail",
|
||||
);
|
||||
assert(
|
||||
checkFindings([], rules).stale.length === 1,
|
||||
"unused exceptions must be removed",
|
||||
);
|
||||
});
|
||||
Deno.test("exceptions require a classified rationale", () => {
|
||||
let rejected = false;
|
||||
try {
|
||||
checkFindings([], [{
|
||||
path: "x",
|
||||
kind: "saved_data",
|
||||
reason: "",
|
||||
lines: [],
|
||||
}]);
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
assert(rejected, "a blank waiver cannot be accepted");
|
||||
});
|
||||
Deno.test("generated binary bytes are not excluded from name detection", async () => {
|
||||
const binary = new TextEncoder().encode(`\0asm\0${spelling}\0`);
|
||||
assert(
|
||||
(await findNames("module.wasm", new TextDecoder().decode(binary)))
|
||||
.length === 1,
|
||||
"binary names must be audited too",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
// The sole spelling here defines the prohibited vocabulary, not a state model.
|
||||
export const forbiddenWord = "revision";
|
||||
|
||||
export type ExceptionKind = "saved_data" | "rejection_test" | "naming_policy";
|
||||
export interface ExceptionRule {
|
||||
path: string;
|
||||
kind: ExceptionKind;
|
||||
reason: string;
|
||||
lines: { sha256: string; count: number }[];
|
||||
}
|
||||
export interface Finding {
|
||||
path: string;
|
||||
line: number;
|
||||
text: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export async function digest(text: string): Promise<string> {
|
||||
const bytes = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(text),
|
||||
);
|
||||
return Array.from(
|
||||
new Uint8Array(bytes),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
}
|
||||
|
||||
export async function findNames(
|
||||
path: string,
|
||||
content: string,
|
||||
): Promise<Finding[]> {
|
||||
const pattern = new RegExp(forbiddenWord, "i");
|
||||
const findings: Finding[] = [];
|
||||
if (pattern.test(path)) {
|
||||
findings.push({ path, line: 0, text: path, sha256: await digest(path) });
|
||||
}
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (pattern.test(lines[i])) {
|
||||
findings.push({
|
||||
path,
|
||||
line: i + 1,
|
||||
text: lines[i],
|
||||
sha256: await digest(lines[i].trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
export function checkFindings(findings: Finding[], rules: ExceptionRule[]) {
|
||||
const remaining = new Map<string, number>();
|
||||
for (const rule of rules) {
|
||||
if (
|
||||
!rule.reason.trim() ||
|
||||
!["saved_data", "rejection_test", "naming_policy"].includes(rule.kind)
|
||||
) {
|
||||
throw new Error(`Missing classified rationale for ${rule.path}`);
|
||||
}
|
||||
for (const line of rule.lines) {
|
||||
const key = `${rule.path}\0${line.sha256}`;
|
||||
if (
|
||||
remaining.has(key) || !Number.isSafeInteger(line.count) ||
|
||||
line.count < 1
|
||||
) {
|
||||
throw new Error(`Invalid or duplicate exception in ${rule.path}`);
|
||||
}
|
||||
remaining.set(key, line.count);
|
||||
}
|
||||
}
|
||||
const unexpected: Finding[] = [];
|
||||
for (const finding of findings) {
|
||||
const key = `${finding.path}\0${finding.sha256}`;
|
||||
const count = remaining.get(key) ?? 0;
|
||||
if (count === 0) unexpected.push(finding);
|
||||
else remaining.set(key, count - 1);
|
||||
}
|
||||
const stale = [...remaining].filter(([, count]) => count !== 0).map(([key]) =>
|
||||
key.replace("\0", ":")
|
||||
);
|
||||
return { unexpected, stale };
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const result = await new Deno.Command("git", {
|
||||
args: ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).output();
|
||||
if (!result.success) throw new Error(new TextDecoder().decode(result.stderr));
|
||||
const paths = [
|
||||
...new Set(
|
||||
new TextDecoder().decode(result.stdout).split("\0").filter(Boolean),
|
||||
),
|
||||
].sort();
|
||||
const findings: Finding[] = [];
|
||||
for (const path of paths) {
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = await Deno.readFile(path);
|
||||
} catch (error) {
|
||||
if (error instanceof Deno.errors.NotFound) continue;
|
||||
throw error;
|
||||
}
|
||||
// Include generated binaries in the audit; decoding is only for finding ASCII
|
||||
// names, never for rewriting data. No directory or extension is exempted.
|
||||
findings.push(...await findNames(path, new TextDecoder().decode(bytes)));
|
||||
}
|
||||
const rules: ExceptionRule[] = JSON.parse(
|
||||
await Deno.readTextFile("tools/state-contract-name-exceptions.json"),
|
||||
);
|
||||
const { unexpected, stale } = checkFindings(findings, rules);
|
||||
if (Deno.args.includes("--report")) {
|
||||
for (const finding of findings) {
|
||||
console.log(`${finding.path}:${finding.line}: ${finding.text}`);
|
||||
}
|
||||
for (const rule of rules) {
|
||||
console.log(`[${rule.kind}] ${rule.path}: ${rule.reason}`);
|
||||
}
|
||||
}
|
||||
for (const finding of unexpected) {
|
||||
console.error(
|
||||
`UNAPPROVED ${finding.path}:${finding.line}: ${finding.text}`,
|
||||
);
|
||||
}
|
||||
for (const key of stale) console.error(`STALE EXCEPTION ${key}`);
|
||||
console.log(
|
||||
`${findings.length} matching lines; ${unexpected.length} unapproved; ${stale.length} stale exceptions`,
|
||||
);
|
||||
Deno.exit(unexpected.length || stale.length ? 1 : 0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -214,7 +214,7 @@ confirmation. Interaction screenshots are saved under `target/web-ux/settings-in
|
||||
`queue`, `notifications`, `legacy`, or `empty`. Each state includes a task for typography comparison.
|
||||
It captures 1440px, 768px, and 390px; `browser-tests/console_pending_shell_test.ts` additionally checks
|
||||
320px and both themes, independent group visibility, full-width single groups, uniform 16px rows,
|
||||
monospace inheritance, truncation, and retained-session cancellation gating. Live revision-fenced
|
||||
monospace inheritance, truncation, and retained-session cancellation gating. Live ordered-FIFO-content-fenced
|
||||
cancellation and state transitions are covered by Console component tests. No real Worker is used.
|
||||
|
||||
## Console live Feature invocation fixture
|
||||
|
||||
@@ -367,7 +367,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
).count(),
|
||||
1,
|
||||
);
|
||||
assertEquals(await paginationPage.getByText("Store revision", { exact: true }).count(), 0);
|
||||
assertEquals(await paginationPage.getByText("Memory fingerprint", { exact: true }).count(), 0);
|
||||
assert((await paginationPage.getByText("Not connected", { exact: true }).count()) > 0);
|
||||
assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0);
|
||||
await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole(
|
||||
|
||||
Reference in New Issue
Block a user