chore: sync the frontend template from 4dc033c to 8157ad4

The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-15 12:04:58 +09:00
co-authored by Claude Opus 5
parent 002ba3624e
commit 4bff9ca151
142 changed files with 23010 additions and 1544 deletions
+207
View File
@@ -1,3 +1,7 @@
import {
createReadOnlyRegistry,
type ReadOnlyRegistry,
} from "./read-only-registry.ts";
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
@@ -327,6 +331,209 @@ export function composeBrowserRpcRequestEncoderRegistry(
);
}
/**
* RPC-RR-03. Read facades, never `Map`s. `Object.freeze(new Map(...))` leaves
* `set`, `delete` and `clear` working, so an installed registry could still be
* emptied or re-pointed after the snapshot was validated.
*/
export type InstalledBrowserRpcContractBindings = Readonly<{
operations: ReadOnlyRegistry<string, BrowserRpcOperationV3>;
profiles: ReadOnlyRegistry<string, BrowserRpcProviderProfile>;
schemaCodecs: ReadOnlyRegistry<string, RuntimeSchemaCodec>;
mappers: ReadOnlyRegistry<string, InstalledBoundaryMapper>;
requestEncoders: ReadOnlyRegistry<string, BrowserRpcRequestEncoder>;
runtimeBindings: ReadOnlyRegistry<string, BrowserRpcRuntimeBindingIdentity>;
}>;
/**
* R-04. Parse → validate → install.
*
* `Readonly` is a TypeScript annotation, not a runtime guarantee, and a source
* registry can be mutated after validation so replay policy, deadlines, byte
* ceilings or transport selection differ from what was checked. Every row is
* therefore copied once into a frozen null-prototype snapshot built from exact
* own data properties. A getter, an extra or symbol key, a malformed descriptor
* or a revoked proxy is a composition-time `TypeError`, and the runtime reads
* only the snapshot afterwards.
*/
function installRegistrySnapshot<Value extends object>(
source: Readonly<Record<string, Value>>,
label: string,
allowedKeys: readonly string[],
): ReadOnlyRegistry<string, Value> {
let ownKeys: string[];
let symbols: readonly symbol[];
let prototype: object | null;
try {
// RPC-03. Own *names*, not just enumerable keys: a non-enumerable own entry
// is as much a smuggled row as an inherited one, and `Object.keys` never
// saw either.
ownKeys = Object.getOwnPropertyNames(source);
symbols = Object.getOwnPropertySymbols(source);
prototype = Reflect.getPrototypeOf(source);
} catch {
throw new TypeError(`Browser RPC ${label} registry is unreadable.`);
}
if (symbols.length > 0) {
throw new TypeError(`Browser RPC ${label} registry has symbol keys.`);
}
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError(`Browser RPC ${label} registry has a custom prototype.`);
}
const installed = new Map<string, Value>();
for (const key of ownKeys) {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (!descriptor || !("value" in descriptor)) {
throw new TypeError(
`Browser RPC ${label} registry entry is not a data property: ${key}`,
);
}
installed.set(
key,
installRowSnapshot(descriptor.value as Value, `${label}.${key}`, allowedKeys),
);
}
return createReadOnlyRegistry(installed);
}
function installRowSnapshot<Value extends object>(
row: Value,
label: string,
allowedKeys: readonly string[],
): Value {
if (!row || typeof row !== "object") {
throw new TypeError(`Browser RPC ${label} row is not an object.`);
}
let ownKeys: string[];
let symbols: readonly symbol[];
let prototype: object | null;
try {
ownKeys = Object.getOwnPropertyNames(row);
symbols = Object.getOwnPropertySymbols(row);
prototype = Reflect.getPrototypeOf(row);
} catch {
throw new TypeError(`Browser RPC ${label} row is unreadable.`);
}
if (symbols.length > 0) {
throw new TypeError(`Browser RPC ${label} row has symbol keys.`);
}
// RPC-03. A custom prototype carries fields the name sweep never sees and
// stays live after installation, so the installed row would not be the row
// that was checked.
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError(`Browser RPC ${label} row has a custom prototype.`);
}
const snapshot = Object.create(null) as Record<string, unknown>;
for (const key of ownKeys) {
if (!allowedKeys.includes(key)) {
throw new TypeError(
`Browser RPC ${label} row has an unexpected key: ${key}`,
);
}
const descriptor = Object.getOwnPropertyDescriptor(row, key);
// Reading an accessor would invoke a getter; refuse without calling it.
if (!descriptor || !("value" in descriptor)) {
throw new TypeError(
`Browser RPC ${label} row key is not a data property: ${key}`,
);
}
const value = descriptor.value as unknown;
snapshot[key] = Array.isArray(value)
? Object.freeze([...value])
: value;
}
return Object.freeze(snapshot) as Value;
}
const OPERATION_KEYS = Object.freeze([
"contractVersion", "operationId", "owner", "protocol", "semantics",
"replayPolicy", "idempotencyKeyPolicy", "idempotencyLevel",
"dataClassification", "runtimeProfileId", "providerId",
"fullyQualifiedService", "method", "rpcKind", "requestMessageId",
"responseMessageId", "descriptorArtifactId", "descriptorDigest",
"requestSchemaId", "responseSchemaId", "requestEncoderId", "mapperId",
"authProfileId", "csrfProfileId", "errorProfileId", "deadlineProfileId",
"retryProfileId", "serverStateProfileId", "maxRequestMessageBytes",
"maxResponseMessageBytes", "maxResponseMessages", "maxTotalResponseBytes",
"maxBufferedBytes", "idleDeadlineMs", "totalDeadlineMs",
] as const);
const PROFILE_KEYS = Object.freeze([
"runtimeProfileId", "providerId", "fixedBaseUrl", "runtimeId",
"runtimeVersion", "runtimeDigest", "protocol", "runtimeKind",
"clientApiKind", "rpcKind", "messageEncoding", "framing", "requestMethod",
"descriptorArtifactId", "descriptorDigest", "allowedProcedures",
"authProfileId", "csrfProfileId", "corsProfileId", "errorProfileId",
"deadlineProfileId", "retryProfileId", "retryOwner", "maxAttempts",
"backoffMs", "retryableFailures", "maxRetryAfterMs", "deadlineDialect",
"cancelDialect", "rawByteCeilingOwner", "streamMessageCompression",
] as const);
const SCHEMA_KEYS = Object.freeze(["schemaId", "parse"] as const);
const MAPPER_KEYS = Object.freeze([
"mapperId", "mapperVersion", "inputSchemaId", "outputContractId", "owner",
"maxOutputItems", "map",
] as const);
const ENCODER_KEYS = Object.freeze([
"encoderId", "operationId", "encode",
] as const);
const RUNTIME_BINDING_KEYS = Object.freeze([
"runtimeProfileId", "providerId", "protocol", "rpcKind",
] as const);
export function installBrowserRpcContractBindings(
bindings: BrowserRpcContractBindings,
): InstalledBrowserRpcContractBindings {
// Parse first. Snapshotting from own data descriptors rejects accessors
// without ever invoking them, so a hostile getter cannot observe validation
// or return a different value to it than to the runtime.
const operations = installRegistrySnapshot(
bindings.operations,
"operation",
OPERATION_KEYS,
);
const profiles = installRegistrySnapshot(
bindings.profiles,
"profile",
PROFILE_KEYS,
);
const schemaCodecs = installRegistrySnapshot(
bindings.schemaCodecs,
"schema",
SCHEMA_KEYS,
);
const mappers = installRegistrySnapshot(
bindings.mappers,
"mapper",
MAPPER_KEYS,
);
const requestEncoders = installRegistrySnapshot(
bindings.requestEncoders,
"encoder",
ENCODER_KEYS,
);
const runtimeBindings = installRegistrySnapshot(
bindings.runtimeBindings ?? {},
"runtime",
RUNTIME_BINDING_KEYS,
);
// Then validate the snapshot, so what was checked is exactly what installs.
validateBrowserRpcContractBindings({
operations: Object.fromEntries(operations),
profiles: Object.fromEntries(profiles),
schemaCodecs: Object.fromEntries(schemaCodecs),
mappers: Object.fromEntries(mappers),
requestEncoders: Object.fromEntries(requestEncoders),
runtimeBindings: Object.fromEntries(runtimeBindings),
});
return Object.freeze({
operations,
profiles,
schemaCodecs,
mappers,
requestEncoders,
runtimeBindings,
});
}
export function validateBrowserRpcContractBindings(
bindings: BrowserRpcContractBindings,
): true {