chore: sync the frontend template from a0fbafb to 5434760
Carries eight template commits: the provider sandbox actually running, release
admission to a named environment, the product feature manifest with its runtime
kill switch, architecture and documentation rules that match what is enforced,
the removability fixtures, and the browser, visual and performance evidence.
Product identity is unchanged. `package.json` keeps `tech-log-frontend` and the
catalog keeps the Tech Log naming; the home page was not in the delta. The
visual baselines are this product's own — the template's were excluded from the
transplant and these were regenerated here, where the only difference is the
platform overview's new product-feature section.
What this repository gains operationally: `config/runtime/{local,development,
staging,production}.json` with `FE-GATE-027` refusing an artifact whose runtime
document does not match the environment it is being admitted to, and
`FEATURE_OVERRIDES` for taking an installed feature out of service without a
rebuild.
Verified here: eight gates green, build green, visual 5/5, and 1,858 of 1,859
tests in the suites that do not need a sandbox — the one failure passes in
isolation and is a jsdom lazy-chunk timeout under parallel load. The provider
suites cannot run on this machine at all: `kernel.apparmor_restrict_unprivileged
_userns=1` makes `bwrap --unshare-net` fail, reproducible without any code from
either repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
325a2a0843
commit
bdee07a93b
@@ -0,0 +1,107 @@
|
||||
export const COMPATIBILITY_TUPLE_FIELDS = Object.freeze([
|
||||
"buildId",
|
||||
"configSchemaVersion",
|
||||
"apiContractVersion",
|
||||
"assetManifestHash",
|
||||
"releaseId",
|
||||
] as const);
|
||||
|
||||
export type CompatibilityTupleField =
|
||||
(typeof COMPATIBILITY_TUPLE_FIELDS)[number];
|
||||
|
||||
export type CompatibilityTuple = Readonly<
|
||||
Record<CompatibilityTupleField, string>
|
||||
>;
|
||||
|
||||
export type NumericVersion = Readonly<{
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
}>;
|
||||
|
||||
export function parseNumericVersion(version: string): NumericVersion | null {
|
||||
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(version);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2] ?? 0),
|
||||
patch: Number(match[3] ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function isVersionCompatible(
|
||||
supported: string,
|
||||
actual: string,
|
||||
): boolean {
|
||||
const expected = parseNumericVersion(supported);
|
||||
const candidate = parseNumericVersion(actual);
|
||||
if (!expected || !candidate) return false;
|
||||
return (
|
||||
expected.major === candidate.major &&
|
||||
candidate.minor >= expected.minor
|
||||
);
|
||||
}
|
||||
|
||||
export function verifyCompatibilityTuple(input: Readonly<{
|
||||
frontend: CompatibilityTuple;
|
||||
runtime: CompatibilityTuple;
|
||||
}>) {
|
||||
const mismatches: CompatibilityTupleField[] = [];
|
||||
if (input.frontend.buildId !== input.runtime.buildId) mismatches.push("buildId");
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
input.frontend.configSchemaVersion,
|
||||
input.runtime.configSchemaVersion,
|
||||
)
|
||||
) {
|
||||
mismatches.push("configSchemaVersion");
|
||||
}
|
||||
if (
|
||||
!isVersionCompatible(
|
||||
input.frontend.apiContractVersion,
|
||||
input.runtime.apiContractVersion,
|
||||
)
|
||||
) {
|
||||
mismatches.push("apiContractVersion");
|
||||
}
|
||||
if (input.frontend.assetManifestHash !== input.runtime.assetManifestHash) {
|
||||
mismatches.push("assetManifestHash");
|
||||
}
|
||||
|
||||
const releaseWarning: "releaseId" | null =
|
||||
input.frontend.releaseId === input.runtime.releaseId
|
||||
? null
|
||||
: "releaseId";
|
||||
return Object.freeze({
|
||||
compatible: mismatches.length === 0,
|
||||
mismatches: Object.freeze(mismatches),
|
||||
warnings: Object.freeze(releaseWarning ? [releaseWarning] : []),
|
||||
});
|
||||
}
|
||||
|
||||
export type ObjectSchemaShape = Readonly<{
|
||||
required?: readonly string[];
|
||||
properties?: Readonly<Record<string, unknown>>;
|
||||
}>;
|
||||
|
||||
export type SchemaChangeClassification = "breaking" | "additive" | "none";
|
||||
|
||||
export function classifyObjectSchemaChange(
|
||||
before: ObjectSchemaShape,
|
||||
after: ObjectSchemaShape,
|
||||
): SchemaChangeClassification {
|
||||
const beforeRequired = new Set(before.required ?? []);
|
||||
const afterRequired = new Set(after.required ?? []);
|
||||
const removedProperties = Object.keys(before.properties ?? {}).filter(
|
||||
(key) => !(key in (after.properties ?? {})),
|
||||
);
|
||||
const addedRequired = [...afterRequired].filter(
|
||||
(key) => !beforeRequired.has(key),
|
||||
);
|
||||
if (removedProperties.length > 0 || addedRequired.length > 0) return "breaking";
|
||||
|
||||
const addedProperties = Object.keys(after.properties ?? {}).filter(
|
||||
(key) => !(key in (before.properties ?? {})),
|
||||
);
|
||||
return addedProperties.length > 0 ? "additive" : "none";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Result } from "../application/result.ts";
|
||||
import type { Result } from "./result.ts";
|
||||
|
||||
export type CursorPage<Value> = Readonly<{
|
||||
items: readonly Value[];
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { RuntimeConfigArtifact } from "./release-artifacts.ts";
|
||||
|
||||
/**
|
||||
* §6.4. Which environment an artifact is allowed to be deployed to.
|
||||
*
|
||||
* Release coherence answers "do these artifacts describe each other?". It does
|
||||
* not answer "is this the artifact production should receive?", and the two are
|
||||
* not the same question: a build whose runtime document says `APP_ENV: local`,
|
||||
* `AUTH_MODE: demo` and `API_BASE_URL: http://localhost:8080/` is perfectly
|
||||
* coherent with itself. Without an admission step such a build is a valid
|
||||
* release candidate, and the only thing standing between it and production is
|
||||
* that nobody happened to promote it.
|
||||
*
|
||||
* Admission is therefore a separate, declared decision: a caller states the
|
||||
* target it intends, and this module says whether the artifact may go there.
|
||||
* Every rule below is a refusal, so an unrecognised target or an unreadable
|
||||
* field fails closed rather than passing by omission.
|
||||
*/
|
||||
|
||||
export const DEPLOYMENT_TARGETS = Object.freeze([
|
||||
"local",
|
||||
"development",
|
||||
"staging",
|
||||
"production",
|
||||
] as const);
|
||||
|
||||
export type DeploymentTarget = (typeof DEPLOYMENT_TARGETS)[number];
|
||||
|
||||
/**
|
||||
* Targets that serve real users over the public internet. They carry the full
|
||||
* rule set; `local` and `development` only have to be honest about what they
|
||||
* are.
|
||||
*/
|
||||
const PUBLIC_TARGETS: ReadonlySet<DeploymentTarget> = new Set([
|
||||
"staging",
|
||||
"production",
|
||||
]);
|
||||
|
||||
/** Placeholder identifiers a developer build emits when nothing supplied one. */
|
||||
const PLACEHOLDER_IDENTIFIERS: ReadonlySet<string> = new Set([
|
||||
"local-build",
|
||||
"local-release",
|
||||
"local",
|
||||
"dev",
|
||||
"unknown",
|
||||
]);
|
||||
|
||||
export type AdmissionViolation = Readonly<{ field: string; reason: string }>;
|
||||
|
||||
export type AdmissionInput = RuntimeConfigArtifact &
|
||||
Readonly<{ BUILD_ID?: string; RELEASE_ID?: string }>;
|
||||
|
||||
export function isDeploymentTarget(value: unknown): value is DeploymentTarget {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
(DEPLOYMENT_TARGETS as readonly string[]).includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every reason this artifact may not be deployed to `target`. An empty list is
|
||||
* the only admission.
|
||||
*/
|
||||
export function findAdmissionViolations(
|
||||
target: DeploymentTarget,
|
||||
config: AdmissionInput,
|
||||
): readonly AdmissionViolation[] {
|
||||
const violations: AdmissionViolation[] = [];
|
||||
if (config.APP_ENV !== target) {
|
||||
violations.push({
|
||||
field: "APP_ENV",
|
||||
reason: `artifact declares ${config.APP_ENV} but is being admitted to ${target}`,
|
||||
});
|
||||
}
|
||||
if (!PUBLIC_TARGETS.has(target)) return Object.freeze(violations);
|
||||
|
||||
if (config.AUTH_MODE !== "external") {
|
||||
violations.push({
|
||||
field: "AUTH_MODE",
|
||||
reason: `${target} requires an external identity provider, not ${config.AUTH_MODE}`,
|
||||
});
|
||||
}
|
||||
violations.push(...publicEndpointViolations("API_BASE_URL", config.API_BASE_URL));
|
||||
if (config.TELEMETRY_ENABLED && !config.TELEMETRY_ENDPOINT) {
|
||||
violations.push({
|
||||
field: "TELEMETRY_ENDPOINT",
|
||||
reason: "telemetry is enabled without an endpoint",
|
||||
});
|
||||
}
|
||||
if (config.TELEMETRY_ENDPOINT) {
|
||||
violations.push(
|
||||
...publicEndpointViolations("TELEMETRY_ENDPOINT", config.TELEMETRY_ENDPOINT),
|
||||
);
|
||||
}
|
||||
for (const field of ["BUILD_ID", "RELEASE_ID"] as const) {
|
||||
const value = config[field];
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
violations.push({ field, reason: `${target} requires a build identity` });
|
||||
continue;
|
||||
}
|
||||
if (PLACEHOLDER_IDENTIFIERS.has(value.toLowerCase())) {
|
||||
violations.push({
|
||||
field,
|
||||
reason: `${value} is a developer placeholder, not a released identity`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Object.freeze(violations);
|
||||
}
|
||||
|
||||
function publicEndpointViolations(
|
||||
field: string,
|
||||
value: string,
|
||||
): readonly AdmissionViolation[] {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return [{ field, reason: "is not an absolute URL" }];
|
||||
}
|
||||
const violations: AdmissionViolation[] = [];
|
||||
if (url.protocol !== "https:") {
|
||||
violations.push({ field, reason: `${url.protocol} is not permitted; use https` });
|
||||
}
|
||||
if (isNonPublicHost(url.hostname)) {
|
||||
violations.push({
|
||||
field,
|
||||
reason: `${url.hostname} is not reachable from a user's browser`,
|
||||
});
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts that only resolve inside the machine or network that built the
|
||||
* artifact. A deployment pointing at one of these is a developer configuration
|
||||
* that escaped, not a production endpoint.
|
||||
*/
|
||||
function isNonPublicHost(hostname: string): boolean {
|
||||
const host = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host.endsWith(".localhost") ||
|
||||
host === "::1" ||
|
||||
host === "0.0.0.0" ||
|
||||
host === "::"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const octets = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(host);
|
||||
if (!octets) return false;
|
||||
const [first, second] = [Number(octets[1]), Number(octets[2])];
|
||||
return (
|
||||
first === 127 ||
|
||||
first === 10 ||
|
||||
(first === 192 && second === 168) ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 169 && second === 254)
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,11 @@ export const ENV_REGISTRY = Object.freeze({
|
||||
RELEASE_MANIFEST_URL: runtime("public", true, "/release-manifest.json"),
|
||||
// §3.5: overrides may only disable an installed capability, never enable one.
|
||||
CAPABILITY_OVERRIDES: runtime("public", false, null),
|
||||
// §3.5: likewise for features — subtractive, keyed by installed feature id.
|
||||
FEATURE_OVERRIDES: runtime("public", false, null),
|
||||
// §3.5: build-time narrowing of the product manifest. A feature left out
|
||||
// here is not imported by any registry and never reaches the bundle.
|
||||
VITE_PRODUCT_FEATURES: build("compile-time", false, null),
|
||||
});
|
||||
|
||||
function build(
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* §3.5 / §6.1. Which product features this build contains, and which of them a
|
||||
* deployment is allowed to switch off.
|
||||
*
|
||||
* Two different questions, deliberately answered by two different inputs:
|
||||
*
|
||||
* - **Installed** is a build-time decision. `VITE_PRODUCT_FEATURES` selects
|
||||
* from the features this source tree declares; a feature left out contributes
|
||||
* no route, no operation, no schema, no codec and no adapter, so nothing can
|
||||
* reach it. It does *not* shrink the bundle: a static import cannot be undone
|
||||
* by a value, and building the import graph from a configuration string is
|
||||
* exactly what §3.5 forbids. Physical removal is FE-GATE-020's job — delete
|
||||
* the feature directory and rebuild, which that gate proves still works.
|
||||
* - **Active** is a runtime decision. `FEATURE_OVERRIDES` in the runtime config
|
||||
* may take an installed feature out of service without a rebuild.
|
||||
*
|
||||
* Both directions are subtractive, and that is the invariant this module
|
||||
* exists to hold: neither input can ever turn on a feature whose source is
|
||||
* absent. A configuration document that could name a feature into existence
|
||||
* would be a configuration document that chooses which code runs, and no
|
||||
* dynamic import path is ever built from one.
|
||||
*/
|
||||
|
||||
export type ProductFeatureOverride = "DEFAULT" | "DISABLED";
|
||||
|
||||
export type ProductFeatureState =
|
||||
/** Compiled in and not disabled: the feature serves traffic. */
|
||||
| "ACTIVE"
|
||||
/** Compiled in, switched off by the runtime document. */
|
||||
| "DISABLED_BY_CONFIG"
|
||||
/** Not selected at build time; not in the bundle. */
|
||||
| "NOT_INSTALLED";
|
||||
|
||||
export type ProductFeatureOverrideMap = Readonly<
|
||||
Record<string, ProductFeatureOverride>
|
||||
>;
|
||||
|
||||
export type ProductFeatureStatus = Readonly<{
|
||||
featureId: string;
|
||||
state: ProductFeatureState;
|
||||
}>;
|
||||
|
||||
/** The shape every feature contract exposes to the manifest. */
|
||||
export type SelectableProductFeature = Readonly<{ featureId: string }>;
|
||||
|
||||
/**
|
||||
* The explicit "no product features" selection.
|
||||
*
|
||||
* A blank value cannot mean it: an unset CI variable expands to a blank string
|
||||
* far too easily, and a build that silently shipped no features would be a very
|
||||
* expensive way to learn that. Selecting nothing has to be something you typed.
|
||||
*/
|
||||
export const NO_PRODUCT_FEATURES = "none";
|
||||
|
||||
/**
|
||||
* Applies the build-time selection to the features this source tree declares.
|
||||
*
|
||||
* An empty or absent declaration keeps everything, so an ordinary build needs
|
||||
* no environment at all. A declaration naming something that is not compiled is
|
||||
* refused rather than ignored: silently accepting it would let a deployment
|
||||
* believe it had enabled a feature that does not exist.
|
||||
*/
|
||||
export function selectCompiledProductFeatures<
|
||||
Feature extends SelectableProductFeature,
|
||||
>(
|
||||
compiled: readonly Feature[],
|
||||
declared: string | undefined,
|
||||
): readonly Feature[] {
|
||||
const compiledIds = compiled.map((feature) => feature.featureId);
|
||||
assertUniqueFeatureIds(compiledIds);
|
||||
if (declared === undefined || declared.trim() === "") {
|
||||
return Object.freeze([...compiled]);
|
||||
}
|
||||
if (declared.trim() === NO_PRODUCT_FEATURES) return Object.freeze([]);
|
||||
const requested = declared
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
if (requested.length === 0) {
|
||||
throw new Error(
|
||||
`VITE_PRODUCT_FEATURES is set to ${JSON.stringify(declared)}, which names ` +
|
||||
`no feature. Use "${NO_PRODUCT_FEATURES}" to select none, or leave it ` +
|
||||
`unset to keep ${compiledIds.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
const unknown = requested.filter((id) => !compiledIds.includes(id));
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(
|
||||
`VITE_PRODUCT_FEATURES names features this build does not contain: ${unknown.join(
|
||||
", ",
|
||||
)}. Selection can only remove from ${compiledIds.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
return Object.freeze(
|
||||
compiled.filter((feature) => requested.includes(feature.featureId)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of every feature the source tree declares, given what was compiled
|
||||
* and what the runtime document says.
|
||||
*
|
||||
* `compiledIds` is the full declared set rather than the installed one so a
|
||||
* build that dropped a feature still reports it as `NOT_INSTALLED` instead of
|
||||
* omitting it. An operator looking at the platform overview needs to see the
|
||||
* difference between "off" and "never heard of it".
|
||||
*/
|
||||
export function resolveProductFeatures(
|
||||
compiledIds: readonly string[],
|
||||
installedIds: readonly string[],
|
||||
overrides: ProductFeatureOverrideMap = {},
|
||||
): readonly ProductFeatureStatus[] {
|
||||
assertUniqueFeatureIds(compiledIds);
|
||||
return Object.freeze(
|
||||
[...compiledIds].sort().map((featureId) =>
|
||||
Object.freeze({
|
||||
featureId,
|
||||
state: !installedIds.includes(featureId)
|
||||
? ("NOT_INSTALLED" as const)
|
||||
: overrides[featureId] === "DISABLED"
|
||||
? ("DISABLED_BY_CONFIG" as const)
|
||||
: ("ACTIVE" as const),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** The feature ids serving traffic right now. */
|
||||
export function activeProductFeatureIds(
|
||||
statuses: readonly ProductFeatureStatus[],
|
||||
): readonly string[] {
|
||||
return Object.freeze(
|
||||
statuses
|
||||
.filter((status) => status.state === "ACTIVE")
|
||||
.map((status) => status.featureId),
|
||||
);
|
||||
}
|
||||
|
||||
function assertUniqueFeatureIds(ids: readonly string[]): void {
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
throw new Error(`duplicate product feature id: ${ids.join(", ")}`);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,23 @@ export const capabilityOverrideArtifactSchema = z
|
||||
OFFLINE_COMMANDS: "DEFAULT",
|
||||
});
|
||||
|
||||
/**
|
||||
* §3.5 / §6.1. A runtime switch that can take an installed feature out of
|
||||
* service without a rebuild.
|
||||
*
|
||||
* Values are `DEFAULT | DISABLED` for the same reason `CAPABILITY_OVERRIDES`
|
||||
* is: a configuration document may subtract from what the build installed and
|
||||
* may never add to it. Keys are feature ids; naming a feature this build does
|
||||
* not contain is inert rather than an error, so a shared configuration
|
||||
* document can cover several builds.
|
||||
*/
|
||||
export const featureOverrideArtifactSchema = z
|
||||
.record(
|
||||
z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u, "feature id is invalid"),
|
||||
z.enum(["DEFAULT", "DISABLED"]),
|
||||
)
|
||||
.default({});
|
||||
|
||||
type RuntimeConfigArtifactDraft = Readonly<{
|
||||
APP_ENV: "local" | "development" | "staging" | "production";
|
||||
API_BASE_URL: string;
|
||||
@@ -138,6 +155,7 @@ export const runtimeConfigV2ArtifactSchema = z
|
||||
...runtimeConfigArtifactFields,
|
||||
CONFIG_SCHEMA_VERSION: z.literal("2.0"),
|
||||
CAPABILITY_OVERRIDES: capabilityOverrideArtifactSchema,
|
||||
FEATURE_OVERRIDES: featureOverrideArtifactSchema,
|
||||
})
|
||||
.strict()
|
||||
.superRefine(runtimeConfigArtifactInvariants);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyCompatibilityTuple } from "../application/policies/compatibility.ts";
|
||||
import { verifyCompatibilityTuple } from "./compatibility.ts";
|
||||
|
||||
export const RELEASE_TOKEN_REGISTRY = Object.freeze({
|
||||
appVersion: token("appVersion", "manifest", "human release label"),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { AppFailure } from "./errors.ts";
|
||||
|
||||
/**
|
||||
* The single success/failure carrier used across application input boundaries.
|
||||
* Adapters map technology-specific errors to an application failure before
|
||||
* constructing this value.
|
||||
*
|
||||
* It lives in `src/contracts` because it is shared vocabulary rather than
|
||||
* application behaviour: contracts modules describe results too, and reaching
|
||||
* up into `src/application` for the shape made the dependency between the two
|
||||
* packages point both ways. `src/application/result.ts` re-exports it.
|
||||
*/
|
||||
export type Result<Value, Failure = AppFailure> =
|
||||
| Readonly<{ ok: true; value: Value }>
|
||||
| Readonly<{ ok: false; error: Failure }>;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Result } from "../application/result.ts";
|
||||
import type { Result } from "./result.ts";
|
||||
import type { QueryInvalidationTopic } from "./query-invalidation.ts";
|
||||
import {
|
||||
createBoundQueryKey,
|
||||
|
||||
Reference in New Issue
Block a user