docs: localize browser evidence boundary comments

This commit is contained in:
2026-10-29 09:34:30 +09:00
parent 2141672310
commit dd312b71ee
4 changed files with 24 additions and 25 deletions
@@ -15,11 +15,11 @@ if (
throw new Error("usage: server.ts <port> <build-root> [section-count: 3..100]");
}
// Opt-in keeps the existing Memory scenarios and their pagination unchanged.
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
const permissionsMode = Deno.args[3] === "permissions";
if (permissionsMode) {
// Permission evidence must serve a parent-supplied actual production build.
// Do not make /health succeed for a missing build or substitute fixture HTML.
// 権限の証拠は親から渡された実製品buildを使って採取する。
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
await Deno.stat(join(buildRoot, "index.html"));
await Deno.stat(join(buildRoot, "_app/immutable"));
}
@@ -1,5 +1,5 @@
#!/usr/bin/env -S deno run -A
/** Capture the actual built Subject detail before UI changes; never builds Web. */
/** 変更前の実Web buildでSubject詳細を撮影する。Web自体はbuildしない。 */
import { dirname, fromFileUrl, join, resolve } from "@std/path";
import { capture } from "../src/capture.ts";
import type { Scenario } from "../src/types.ts";
@@ -10,7 +10,7 @@ const outputRoot = join(repositoryRoot, "target/web-ux/t-802");
const buildRoot = resolve(Deno.args[0] ?? join(repositoryRoot, "web/workspace/build"));
const runPrefix = Deno.args[1] ?? `before-${new Date().toISOString().replaceAll(/[:.]/g, "-")}`;
if (!/^[a-zA-Z0-9._-]+$/.test(runPrefix)) throw new Error("run id must be a safe filename");
// Preflight is mandatory. A missing build must not be replaced by fixture HTML.
// Preflightは必須。buildの欠落をfixture HTMLで代替しない。
for (const filename of ["index.html", "_app/immutable"]) {
await Deno.stat(join(buildRoot, filename)).catch(() => {
throw new Error(
@@ -1,7 +1,6 @@
/**
* Synthetic HTTP state for T-802 browser UX only. NOT the real Backend,
* authentication, authorization, operation catalog, or cancellation implementation.
* Keep real Backend integration evidence separate from these browser fixtures.
* Browser UXだけの合成HTTP状態。実Backend、認証、認可、操作catalog、取消の実装ではない。
* 実Backendの結合証拠は、このbrowser fixtureと分けて保持する。
*/
export const permissionFixtureSubjects = [
["permissions-empty", "New Subject — permissions not configured"],
@@ -31,7 +30,7 @@ export const fixtureLegacyPermission: FixturePermission = {
operation: "worker.notify",
resource: { kind: "worker", runtime_id: "legacy-runtime", worker_id: "legacy-recipient" },
};
// Deliberately a small synthetic catalog, not a copy/second authority for production.
// 小さな合成catalogに限定する。製品catalogの複製や第二正本ではない。
export const fixturePermissionOperations: {
operation: string;
label: string;
@@ -105,8 +104,8 @@ export function createSubjectPermissionsFixture() {
return async (request: Request): Promise<Response | null> => {
const url = new URL(request.url);
if (url.pathname === "/api/auth/whoami") {
// Exercise the real Web's admission gate with a synthetic HTTP session projection.
// No cookies/tokens or real Backend session are created or bypassed.
// 合成HTTP session projectionで実Webの画面入場判定を通す。
// 実Backendのsessionやcookie/tokenは作成・迂回しない。
return json({
actor: {
user_id: "synthetic-owner",
@@ -156,7 +155,7 @@ export function createSubjectPermissionsFixture() {
requests.push({ subject_id: id, request: payload, outcome: "conflict" });
return error("Policy changed in another browser; reload and review", 409);
}
// Reject only the combinations used by this fixture. This is NOT Backend validation evidence.
// fixtureで使う組合せだけを拒否する。Backend検証の証拠にはしない。
for (const p of [...payload.execution, ...payload.delegation]) {
const legacy = JSON.stringify(p) === JSON.stringify(fixtureLegacyPermission) &&
id === "permissions-legacy";
@@ -1,5 +1,5 @@
#!/usr/bin/env -S deno run -A
/** Actual built Web + synthetic HTTP. No real Backend auth/Worker success claim. */
/** 実Web buildを合成HTTPで操作する。実Backend認証やWorker動作の証拠ではない。 */
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join, resolve } from "@std/path";
import { chromium, type Page } from "playwright";
@@ -11,7 +11,7 @@ const root = resolve(dirname(fromFileUrl(import.meta.url)), "../../..");
const buildRoot = resolve(Deno.args[0] ?? join(root, "web/workspace/build"));
const runId = Deno.args[1] ?? `after-${new Date().toISOString().replaceAll(/[:.]/g, "-")}`;
if (!/^[a-zA-Z0-9._-]+$/.test(runId)) throw new Error("run id must be a safe filename");
// Fail fast; the parent owns building Web. Never replace it with a synthetic shell.
// Web buildは親の責務。欠落時は即失敗させ、合成shellで代替しない。
for (const name of ["index.html", "_app/immutable"]) {
await Deno.stat(join(buildRoot, name)).catch(() => {
throw new Error(`Actual Web build missing: ${join(buildRoot, name)}; parent must supply it.`);
@@ -19,7 +19,7 @@ for (const name of ["index.html", "_app/immutable"]) {
}
const output = join(root, "target/web-ux/t-802", runId);
await Deno.mkdir(dirname(output), { recursive: true });
await Deno.mkdir(output); // A repeated run ID must not overwrite prior evidence.
await Deno.mkdir(output); // 同じrun IDで既存の証拠を上書きしない。
const sourceCommit = await new Deno.Command("git", {
args: ["rev-parse", "HEAD"],
cwd: root,
@@ -113,12 +113,12 @@ function observe(page: Page) {
});
}
async function capturePoint(page: Page, id: string, expectedErrors: string[] = []) {
// Show the decision being captured, without changing product CSS or layout.
// 製品CSSやlayoutを変えず、確認対象の判断が見える位置へ移動する。
if (id.includes("preview")) await page.locator(".diff").scrollIntoViewIfNeeded();
else if (/saved|re-read|unknown|recovered|conflict/.test(id)) {
await page.locator(".permissions > header").scrollIntoViewIfNeeded();
}
// Always retain the image first, even when an overflow assertion will fail.
// overflow検査が失敗する場合も、先に画像を保存して根拠を残す。
const image = join(output, `${id}.png`);
const snapshot = join(output, `${id}.aria.txt`);
await page.screenshot({ path: image });
@@ -169,7 +169,7 @@ async function capturePoint(page: Page, id: string, expectedErrors: string[] = [
imageSha256: await sha256File(image),
accessibilitySnapshot: snapshot,
expectedErrors,
// Per-page errors are cumulative so reload does not erase recovery evidence.
// 再読込みで復旧の証拠を消さないよう、pageごとにerrorを累積する。
errors: [...diagnostics.get(page) ?? []],
errorSummary: {
observed: errorCounts.get(page) ?? 0,
@@ -206,7 +206,7 @@ async function add(
) {
await page.getByRole("combobox", { name: "Permission scope", exact: true }).selectOption(lane);
await page.getByRole("combobox", { name: "Operation", exact: true }).selectOption(operation);
// Exercise the actual name/description/resource input UI, not internal JSON entry.
// 内部JSON入力ではなく、実UIの名称・説明・資源入力を操作する。
await page.getByText(
operation === "ticket.read"
? "Read the selected ticket. Does not allow editing or closing it."
@@ -267,7 +267,7 @@ try {
try {
const page = await context.newPage();
observe(page);
// Enter through the ordinary Subject detail navigation.
// 通常のSubject詳細から管理導線へ進む。
await page.goto(`${base}/w/memory-review/memory/permissions-empty`);
await page.locator(".subject-overview").waitFor();
await page.locator('main a[href$="/permissions"]').click();
@@ -282,7 +282,7 @@ try {
`${prefix}-operation-description-resource-input`,
);
await add(page, "delegation", "ticket.comment", "T-802");
// Use named lane scopes so one grant cannot silently appear in both lanes.
// 名前付きlaneで検査し、一つのgrantが両方へ黙って追加されないことを確認する。
assertEquals(
await page.locator('.lane[aria-label="Direct operations"]').getByText("Read ticket", {
exact: true,
@@ -310,7 +310,7 @@ try {
await page.locator("[data-permission-state=ready]").waitFor();
await capturePoint(page, `${prefix}-re-read`);
// Real browser transport loss AFTER the synthetic HTTP fixture accepts the POST.
// 合成HTTP fixtureがPOSTを受け付けた後、実browserで応答を消失させる。
await add(page, "execution", "ticket.comment", "T-803");
let lost = false;
await page.route("**/subjektiv/subjects/permissions-empty/permissions", async (route) => {
@@ -323,7 +323,7 @@ try {
});
await save(page, "unknown");
assert(lost);
// Fault injection is one-shot; restore ordinary browser networking before evidence capture.
// 障害注入は一度だけとし、撮影前に通常のbrowser通信へ戻す。
await page.unroute("**/subjektiv/subjects/permissions-empty/permissions");
await capturePoint(page, `${prefix}-unknown`, [
"Intentional POST response loss after synthetic save",
@@ -343,7 +343,7 @@ try {
"Intentional earlier POST response loss",
]);
// Two browser pages observe the same synthetic change identity before either save.
// 二つのbrowser画面で同じ合成change identityを観測してから保存する。
const other = await context.newPage();
observe(other);
await open(page, base, "permissions-edit");
@@ -397,7 +397,7 @@ try {
await capturePoint(page, `${prefix}-legacy-preserved`, [
"Intentional earlier service unavailable GET",
]);
// Revoke a supported direct permission; this fixture proves UI intent, not real cancellation.
// 直接操作の失効を確認する。ここで証明するのはUIの入力であり、実取消ではない。
await page.getByRole("button", {
name: "Remove Read ticket from direct operations",
exact: true,