56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
/**
|
|
* Emergency unified derivation — floor + dataset section from same source sheet.
|
|
* Ensures no drift (ARCH 10.3, ARCH 13.1).
|
|
* Trace: ADR-007, SPIKE-06, IMPLEMENTATION-CONTRACT.md §14, ARCHITECTURE-DESIGN.md:345, §10.3
|
|
*/
|
|
import type { EmergencySource } from "./types.js";
|
|
import type { EmergencyFloor } from "../src/emergency-baseline/types.js";
|
|
import { FLOOR_SIZE_BUDGET } from "../src/emergency-baseline/types.js";
|
|
import { canonicalJson } from "./canonical-json.js";
|
|
import { sha256HexOfString } from "./hash.js";
|
|
import { checkFloorSize } from "./budgets.js";
|
|
|
|
export function deriveEmergencyFloor(
|
|
source: EmergencySource,
|
|
opts: { floorVersion: string; generatedAt: string },
|
|
): { floor: EmergencyFloor; bytes: number; sha256: string } {
|
|
// Summaries per ARCH 13.1 T1 floor contents — derive from same source
|
|
const floor: EmergencyFloor = {
|
|
floor: true,
|
|
floorVersion: opts.floorVersion,
|
|
emergencySchemaVersion: source.emergencySchemaVersion,
|
|
generatedAt: opts.generatedAt,
|
|
sourceContentVersion: source.contentVersion,
|
|
services: source.services,
|
|
address: source.address,
|
|
musterPoints: source.locations.musterPoints.map((m) => ({ name: m.name })),
|
|
exits: source.locations.exits.map((e) => ({ name: e.name })),
|
|
aedSummary:
|
|
source.locations.aeds.length > 0
|
|
? `AEDs: ${source.locations.aeds.length} locations`
|
|
: "AED on site",
|
|
procedures: source.procedures.map((p) => ({ id: p.id, title: p.title, steps: [...p.steps] })),
|
|
};
|
|
const json = canonicalJson(floor);
|
|
const bytes = new TextEncoder().encode(json).length;
|
|
const check = checkFloorSize(bytes);
|
|
if (!check.ok) throw new Error(check.reason);
|
|
if (bytes > FLOOR_SIZE_BUDGET) throw new Error(`floor exceeds 16KB budget: ${bytes}`);
|
|
const sha256 = sha256HexOfString(json);
|
|
return { floor, bytes, sha256 };
|
|
}
|
|
|
|
export function validateEmergencySource(
|
|
source: EmergencySource,
|
|
): { ok: true } | { ok: false; reason: string } {
|
|
if (!source) return { ok: false, reason: "emergency source missing" };
|
|
if (!Number.isInteger(source.emergencySchemaVersion) || source.emergencySchemaVersion < 1)
|
|
return { ok: false, reason: "emergencySchemaVersion must be integer >=1" };
|
|
if (!Number.isInteger(source.contentVersion) || source.contentVersion < 1)
|
|
return { ok: false, reason: "contentVersion must be integer >=1" };
|
|
if (typeof source.updatedAt !== "string" || Number.isNaN(Date.parse(source.updatedAt)))
|
|
return { ok: false, reason: "updatedAt must be ISO8601" };
|
|
if (source.section !== "emergency") return { ok: false, reason: "section must be emergency" };
|
|
// services/address/procedures presence checked via runtime validation in gates; keep light here
|
|
return { ok: true };
|
|
}
|