217 lines
12 KiB
TypeScript
217 lines
12 KiB
TypeScript
const readyQueryResults = Object.freeze(["durable_readiness_ready", "runtime_durable_ready", "query_ready", "readiness_ready", "ready", "ok", "pass", "passed"]);
|
|
const blockedQueryResults = Object.freeze<Record<string, string>>({
|
|
driver_missing: "runtime_durable_adapter_driver_missing",
|
|
ssl_negotiation_blocked: "runtime_durable_adapter_ssl_blocked",
|
|
auth_blocked: "runtime_durable_adapter_auth_blocked",
|
|
schema_blocked: "runtime_durable_adapter_schema_blocked",
|
|
migration_blocked: "runtime_durable_adapter_migration_blocked",
|
|
query_blocked: "runtime_durable_adapter_query_blocked",
|
|
not_ready: "runtime_durable_adapter_query_blocked"
|
|
});
|
|
const gateLayers = Object.freeze(["ssl", "auth", "schema", "migration", "durability"]);
|
|
|
|
export function classifyRuntimeDurableReadiness(payload: Record<string, unknown> = {}, options: { requirePostgresAdapter?: boolean } = {}) {
|
|
const runtime = record(payload.runtime);
|
|
const readiness = record(payload.readiness);
|
|
const durability = record(readiness.durability);
|
|
const db = record(payload.db);
|
|
const dbRuntimeReadiness = record(db.runtimeReadiness);
|
|
const gates = record(runtime.gates ?? durability.gates);
|
|
const gateBlocker = firstBlockedRuntimeGate(gates);
|
|
const queryResult = firstNonEmpty(record(runtime.connection).queryResult, durability.queryResult, dbRuntimeReadiness.queryResult);
|
|
const queryBlocker = runtimeDurableBlockerFromQueryResult(queryResult);
|
|
const readyQueryResult = isRuntimeDurableReadyQueryResult(queryResult);
|
|
const explicitDurabilityReady = durabilityReadySignal({ runtime, durability, dbRuntimeReadiness, gateBlocker, queryResult, readyQueryResult, queryBlocker });
|
|
const directBlocker = firstNonEmpty(
|
|
explicitDurabilityReady ? "" : runtime.blocker,
|
|
durability.blocker,
|
|
dbRuntimeReadiness.blocker,
|
|
explicitDurabilityReady ? "" : blockerCodeStartingWith(payload, "runtime_durable_adapter_")
|
|
);
|
|
const reasonCode = firstNonEmpty(directBlocker, gateBlocker?.blocker, queryBlocker);
|
|
const blockedLayer = firstNonEmpty(
|
|
record(runtime.durabilityContract).blockedLayer,
|
|
durability.blockedLayer,
|
|
dbRuntimeReadiness.blockedLayer,
|
|
gateBlocker?.layer,
|
|
runtimeDurableLayerFromBlocker(reasonCode),
|
|
runtimeDurableLayerFromQueryResult(queryResult)
|
|
);
|
|
const adapter = firstNonEmpty(runtime.adapter, durability.adapter, dbRuntimeReadiness.adapter, "unknown");
|
|
const runtimeDurable = runtime.durable === true || durability.durable === true || dbRuntimeReadiness.durable === true;
|
|
const runtimeExplicitlyNonDurable = runtime.durable === false || durability.durable === false || dbRuntimeReadiness.durable === false;
|
|
const runtimeReady = runtime.ready === true || durability.runtimeReady === true || dbRuntimeReadiness.ready === true;
|
|
const durabilityReady = durability.ready === true || dbRuntimeReadiness.ready === true || runtimeReady;
|
|
const liveRuntimeEvidence = runtime.liveRuntimeEvidence === true || durability.liveRuntimeEvidence === true || dbRuntimeReadiness.liveRuntimeEvidence === true;
|
|
const rawStatus = rawStatusFrom(payload);
|
|
const postgresRequired = options.requirePostgresAdapter === true;
|
|
const ready = (!postgresRequired || adapter === "postgres") && runtimeDurable && runtimeReady && durabilityReady && liveRuntimeEvidence && !reasonCode && !blockedLayer && (!queryResult || readyQueryResult);
|
|
|
|
if (ready) {
|
|
return {
|
|
status: "ready",
|
|
ready: true,
|
|
classification: "runtime_durable_ready",
|
|
reasonCode: "runtime_durable_ready",
|
|
reason: `runtime durable ready; adapter=${adapter}; queryResult=${queryResult || "not_observed"}`,
|
|
impact: "HWPOD runtime readiness checks may rely on durable runtime evidence when persistence readiness is green.",
|
|
safeNextAction: "Continue with repo-owned DEV CD postflight; do not read or print Secret values.",
|
|
retryable: true,
|
|
readonly: false,
|
|
evidence: runtimeDurableEvidence({ adapter, runtimeDurable, runtimeReady, durabilityReady, liveRuntimeEvidence, blockedLayer: null, queryResult, readyQueryResult, reasonCode: null, gates, rawStatus, postgresRequired })
|
|
};
|
|
}
|
|
|
|
const nonReadyReasonCode = firstNonEmpty(
|
|
reasonCode,
|
|
postgresRequired && adapter !== "postgres" ? "runtime_durable_adapter_not_postgres" : "",
|
|
runtimeExplicitlyNonDurable ? "runtime_durable_false" : "",
|
|
runtimeDurable ? "" : "runtime_durable_adapter_missing",
|
|
runtimeReady ? "" : "runtime_durable_runtime_not_ready",
|
|
liveRuntimeEvidence ? "" : "runtime_durable_live_evidence_missing",
|
|
blockedRuntimeStatus(rawStatus) ? rawStatus : "",
|
|
"runtime_durable_adapter_not_ready"
|
|
);
|
|
const layer = firstNonEmpty(blockedLayer, runtimeDurableLayerFromBlocker(nonReadyReasonCode), runtimeExplicitlyNonDurable ? "runtime-durable" : "", "runtime-durable");
|
|
return {
|
|
status: "blocked",
|
|
ready: false,
|
|
classification: nonReadyReasonCode,
|
|
reasonCode: nonReadyReasonCode,
|
|
reason: runtimeDurableBlockedReason({ reasonCode: nonReadyReasonCode, adapter, layer, queryResult }),
|
|
impact: "HWPOD runtime readiness remains source-level while durable runtime evidence is blocked.",
|
|
safeNextAction: runtimeDurableSafeNextAction(nonReadyReasonCode, layer),
|
|
retryable: true,
|
|
readonly: true,
|
|
evidence: runtimeDurableEvidence({ adapter, runtimeDurable, runtimeReady, durabilityReady, liveRuntimeEvidence, blockedLayer: layer, queryResult, readyQueryResult, reasonCode: nonReadyReasonCode, gates, rawStatus, postgresRequired })
|
|
};
|
|
}
|
|
|
|
function durabilityReadySignal(input: { runtime: Record<string, unknown>; durability: Record<string, unknown>; dbRuntimeReadiness: Record<string, unknown>; gateBlocker: { layer: string; blocker: string } | null; queryResult: string; readyQueryResult: boolean; queryBlocker: string }): boolean {
|
|
if (input.gateBlocker || input.queryBlocker) return false;
|
|
if (input.queryResult && !input.readyQueryResult) return false;
|
|
const ready = input.durability.ready === true || input.dbRuntimeReadiness.ready === true || record(input.runtime.durabilityContract).ready === true;
|
|
if (!ready) return false;
|
|
return !firstNonEmpty(input.durability.blocker, input.dbRuntimeReadiness.blocker, record(input.runtime.durabilityContract).blocker, input.durability.blockedLayer, input.dbRuntimeReadiness.blockedLayer, record(input.runtime.durabilityContract).blockedLayer);
|
|
}
|
|
|
|
function firstBlockedRuntimeGate(gates: Record<string, unknown>): { layer: string; blocker: string } | null {
|
|
for (const layer of gateLayers) {
|
|
const gate = record(gates[layer]);
|
|
const status = normalized(gate.status);
|
|
if (gate.ready === false || status === "blocked" || status === "failed") return { layer, blocker: firstNonEmpty(gate.blocker, blockerForLayer(layer)) };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function blockerForLayer(layer: string): string {
|
|
if (layer === "ssl") return "runtime_durable_adapter_ssl_blocked";
|
|
if (layer === "auth") return "runtime_durable_adapter_auth_blocked";
|
|
if (layer === "schema") return "runtime_durable_adapter_schema_blocked";
|
|
if (layer === "migration") return "runtime_durable_adapter_migration_blocked";
|
|
return "runtime_durable_adapter_query_blocked";
|
|
}
|
|
|
|
function runtimeDurableLayerFromBlocker(blocker: string): string {
|
|
const text = normalized(blocker);
|
|
if (text.includes("ssl")) return "ssl";
|
|
if (text.includes("auth")) return "auth";
|
|
if (text.includes("schema")) return "schema";
|
|
if (text.includes("migration")) return "migration";
|
|
if (text.includes("query")) return "durability_query";
|
|
if (text.includes("missing") || text.includes("unconfigured") || text.includes("driver")) return "adapter";
|
|
return "";
|
|
}
|
|
|
|
function runtimeDurableLayerFromQueryResult(queryResult: string): string {
|
|
const text = normalized(queryResult);
|
|
if (!text || isRuntimeDurableReadyQueryResult(text)) return "";
|
|
if (text.includes("ssl")) return "ssl";
|
|
if (text.includes("auth")) return "auth";
|
|
if (text.includes("schema")) return "schema";
|
|
if (text.includes("migration")) return "migration";
|
|
if (text.includes("query")) return "durability_query";
|
|
if (text.includes("driver")) return "adapter";
|
|
return "";
|
|
}
|
|
|
|
function runtimeDurableBlockerFromQueryResult(queryResult: string): string {
|
|
const text = normalized(queryResult);
|
|
if (!text || isRuntimeDurableReadyQueryResult(text)) return "";
|
|
return blockedQueryResults[text] ?? "";
|
|
}
|
|
|
|
function isRuntimeDurableReadyQueryResult(queryResult: string): boolean {
|
|
return readyQueryResults.includes(normalized(queryResult));
|
|
}
|
|
|
|
function runtimeDurableEvidence(input: { adapter: string; runtimeDurable: boolean; runtimeReady: boolean; durabilityReady: boolean; liveRuntimeEvidence: boolean; blockedLayer: string | null; queryResult: string; readyQueryResult: boolean; reasonCode: string | null; gates: Record<string, unknown>; rawStatus: unknown; postgresRequired: boolean }) {
|
|
return {
|
|
adapter: input.adapter,
|
|
durable: input.runtimeDurable,
|
|
runtimeReady: input.runtimeReady,
|
|
durabilityReady: input.durabilityReady,
|
|
liveRuntimeEvidence: input.liveRuntimeEvidence,
|
|
blocker: input.reasonCode,
|
|
blockedLayer: input.blockedLayer,
|
|
queryResult: input.queryResult || null,
|
|
readyQueryResult: input.readyQueryResult,
|
|
rawStatus: input.rawStatus || null,
|
|
postgresRequired: input.postgresRequired,
|
|
gates: Object.fromEntries(gateLayers.map((layer) => {
|
|
const gate = record(input.gates[layer]);
|
|
return [layer, { checked: gate.checked === true, ready: gate.ready === true, status: gate.status ?? "unknown", blocker: gate.blocker ?? null }];
|
|
})),
|
|
secretMaterialRead: false
|
|
};
|
|
}
|
|
|
|
function runtimeDurableBlockedReason(input: { reasonCode: string; adapter: string; layer: string; queryResult: string }): string {
|
|
if (input.reasonCode === "runtime_durable_false") return `runtime durable=false; adapter=${input.adapter}; queryResult=${input.queryResult || "not_observed"}`;
|
|
if (input.reasonCode === "runtime_durable_adapter_not_postgres") return `runtime durable adapter is not postgres; adapter=${input.adapter}; queryResult=${input.queryResult || "not_observed"}`;
|
|
return `runtime durable blocked at ${input.layer || "runtime-durable"}; blocker=${input.reasonCode}; queryResult=${input.queryResult || "unknown"}`;
|
|
}
|
|
|
|
function runtimeDurableSafeNextAction(reasonCode: string, layer: string): string {
|
|
const text = normalized(`${reasonCode} ${layer}`);
|
|
if (text.includes("auth")) return "Repair DEV DB auth/SecretRef metadata through repo-owned provisioning, then rerun postflight without printing Secret values.";
|
|
if (text.includes("schema") || text.includes("migration")) return "Run repo-owned DEV runtime provisioning and migration, then rerun postflight.";
|
|
if (text.includes("ssl")) return "Align DEV DB SSL mode with the repo-owned DEV contract, then rerun postflight.";
|
|
if (text.includes("query")) return "Inspect durable runtime query readiness, preserve evidence persistence, and rerun postflight.";
|
|
if (text.includes("postgres")) return "Run DEV with HWLAB_CLOUD_RUNTIME_ADAPTER=postgres and DB URLs injected only through SecretRefs.";
|
|
return "Restore durable runtime readiness through repo-owned DEV CD steps, then rerun postflight.";
|
|
}
|
|
|
|
function rawStatusFrom(payload: Record<string, unknown>): unknown {
|
|
return payload.status ?? record(payload.readiness).status ?? record(payload.runtime).status ?? record(record(payload.readiness).durability).status ?? "";
|
|
}
|
|
|
|
function blockerCodeStartingWith(payload: Record<string, unknown>, prefix: string): string {
|
|
const readiness = record(payload.readiness);
|
|
return [...array(payload.blockerCodes), ...array(readiness.blockerCodes)].map(String).find((code) => code.startsWith(prefix)) ?? "";
|
|
}
|
|
|
|
function blockedRuntimeStatus(value: unknown): boolean {
|
|
return ["error", "failed", "failure", "blocked", "unavailable", "rejected", "degraded"].includes(normalized(value));
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function array(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
function firstNonEmpty(...values: unknown[]): string {
|
|
for (const value of values) {
|
|
const text = String(value ?? "").trim();
|
|
if (text) return text;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function normalized(value: unknown): string {
|
|
return String(value ?? "").trim().toLowerCase();
|
|
}
|