Этап 4 / Волна 10: корректировка settlement-кейса — доменная фиксация синтеза, честное покрытие, удержание фокуса / Этап 4 / Волна 11: бизнес-якоря, доменное заземление и устранение утечки дебага

This commit is contained in:
2026-03-28 02:17:19 +03:00
parent 914843a8ba
commit a06e575be4
367 changed files with 432257 additions and 3627 deletions
@@ -0,0 +1,265 @@
import {
P0_DEFAULT_ACCEPTANCE_THRESHOLDS,
P0_DEFAULT_QUALITY_GAP_THRESHOLDS,
type P0AcceptanceThresholds,
type P0EvalMetricVector,
type P0QualityGapMetricVector,
type P0QualityGapThresholds
} from "./p0_metric_definitions";
export type P0AcceptanceVerdict = "P0_ACCEPTED" | "P0_ACCEPTED_WITH_LIMITATIONS" | "P0_NOT_ACCEPTED";
export type P0BaselineStabilityVerdict = "P0_BASELINE_STABLE" | "P0_BASELINE_STABLE_WITH_OPEN_QUALITY_GAPS";
interface MetricCheck {
metric: keyof P0EvalMetricVector;
value: number;
threshold: number;
comparator: ">=" | "<=";
passed: boolean;
severity: "blocking" | "quality";
}
interface QualityGapMetricCheck {
metric: keyof P0QualityGapMetricVector;
value: number;
threshold: number;
comparator: ">=" | "<=";
passed: boolean;
}
export interface P0AcceptanceResult {
verdict: P0AcceptanceVerdict;
passed_all: boolean;
blocking_failures: MetricCheck[];
quality_failures: MetricCheck[];
metric_checks: MetricCheck[];
rationale: string[];
}
export interface P0BaselineStabilityResult {
verdict: P0BaselineStabilityVerdict;
baseline_integrity_passed: boolean;
quality_gaps_open: boolean;
blocking_regressions: MetricCheck[];
legacy_quality_failures: MetricCheck[];
quality_gap_failures: QualityGapMetricCheck[];
quality_gap_checks: QualityGapMetricCheck[];
rationale: string[];
}
function buildMetricChecks(input: {
metrics: P0EvalMetricVector;
thresholds: P0AcceptanceThresholds;
}): MetricCheck[] {
const { metrics, thresholds } = input;
return [
{
metric: "problem_first_answer_rate",
value: metrics.problem_first_answer_rate,
threshold: thresholds.problem_first_answer_rate_min,
comparator: ">=",
passed: metrics.problem_first_answer_rate >= thresholds.problem_first_answer_rate_min,
severity: "blocking"
},
{
metric: "entity_leakage_rate",
value: metrics.entity_leakage_rate,
threshold: thresholds.entity_leakage_rate_max,
comparator: "<=",
passed: metrics.entity_leakage_rate <= thresholds.entity_leakage_rate_max,
severity: "blocking"
},
{
metric: "route_correctness_rate",
value: metrics.route_correctness_rate,
threshold: thresholds.route_correctness_rate_min,
comparator: ">=",
passed: metrics.route_correctness_rate >= thresholds.route_correctness_rate_min,
severity: "blocking"
},
{
metric: "domain_purity_rate",
value: metrics.domain_purity_rate,
threshold: thresholds.domain_purity_rate_min,
comparator: ">=",
passed: metrics.domain_purity_rate >= thresholds.domain_purity_rate_min,
severity: "blocking"
},
{
metric: "mechanism_coherence_score",
value: metrics.mechanism_coherence_score,
threshold: thresholds.mechanism_coherence_score_min,
comparator: ">=",
passed: metrics.mechanism_coherence_score >= thresholds.mechanism_coherence_score_min,
severity: "quality"
},
{
metric: "accountant_actionability_score",
value: metrics.accountant_actionability_score,
threshold: thresholds.accountant_actionability_score_min,
comparator: ">=",
passed: metrics.accountant_actionability_score >= thresholds.accountant_actionability_score_min,
severity: "quality"
},
{
metric: "limitation_honesty_rate",
value: metrics.limitation_honesty_rate,
threshold: thresholds.limitation_honesty_rate_min,
comparator: ">=",
passed: metrics.limitation_honesty_rate >= thresholds.limitation_honesty_rate_min,
severity: "quality"
},
{
metric: "top_problem_unit_match_rate",
value: metrics.top_problem_unit_match_rate,
threshold: thresholds.top_problem_unit_match_rate_min,
comparator: ">=",
passed: metrics.top_problem_unit_match_rate >= thresholds.top_problem_unit_match_rate_min,
severity: "quality"
}
];
}
function toFailureLine(check: MetricCheck): string {
return `${check.metric}: ${check.value.toFixed(2)} ${check.comparator} ${check.threshold.toFixed(2)} (failed)`;
}
function toQualityGapFailureLine(check: QualityGapMetricCheck): string {
return `${check.metric}: ${check.value.toFixed(2)} ${check.comparator} ${check.threshold.toFixed(2)} (failed)`;
}
function buildQualityGapChecks(input: {
metrics: P0QualityGapMetricVector;
thresholds: P0QualityGapThresholds;
}): QualityGapMetricCheck[] {
const { metrics, thresholds } = input;
return [
{
metric: "generic_explanation_rate",
value: metrics.generic_explanation_rate,
threshold: thresholds.generic_explanation_rate_max,
comparator: "<=",
passed: metrics.generic_explanation_rate <= thresholds.generic_explanation_rate_max
},
{
metric: "false_confidence_rate",
value: metrics.false_confidence_rate,
threshold: thresholds.false_confidence_rate_max,
comparator: "<=",
passed: metrics.false_confidence_rate <= thresholds.false_confidence_rate_max
},
{
metric: "mechanism_specificity_score",
value: metrics.mechanism_specificity_score,
threshold: thresholds.mechanism_specificity_score_min,
comparator: ">=",
passed: metrics.mechanism_specificity_score >= thresholds.mechanism_specificity_score_min
},
{
metric: "followup_context_retention_score",
value: metrics.followup_context_retention_score,
threshold: thresholds.followup_context_retention_score_min,
comparator: ">=",
passed: metrics.followup_context_retention_score >= thresholds.followup_context_retention_score_min
}
];
}
export function evaluateP0AcceptanceGate(input: {
metrics: P0EvalMetricVector;
thresholds?: Partial<P0AcceptanceThresholds>;
}): P0AcceptanceResult {
const thresholds: P0AcceptanceThresholds = {
...P0_DEFAULT_ACCEPTANCE_THRESHOLDS,
...(input.thresholds ?? {})
};
const checks = buildMetricChecks({
metrics: input.metrics,
thresholds
});
const blockingFailures = checks.filter((item) => item.severity === "blocking" && !item.passed);
const qualityFailures = checks.filter((item) => item.severity === "quality" && !item.passed);
let verdict: P0AcceptanceVerdict = "P0_NOT_ACCEPTED";
if (blockingFailures.length === 0 && qualityFailures.length === 0) {
verdict = "P0_ACCEPTED";
} else if (blockingFailures.length === 0) {
verdict = "P0_ACCEPTED_WITH_LIMITATIONS";
}
const rationale: string[] = [];
if (blockingFailures.length > 0) {
rationale.push("Blocking metrics failed:");
rationale.push(...blockingFailures.map((item) => `- ${toFailureLine(item)}`));
}
if (qualityFailures.length > 0) {
rationale.push("Quality metrics failed:");
rationale.push(...qualityFailures.map((item) => `- ${toFailureLine(item)}`));
}
if (rationale.length === 0) {
rationale.push("All acceptance thresholds are satisfied.");
}
return {
verdict,
passed_all: checks.every((item) => item.passed),
blocking_failures: blockingFailures,
quality_failures: qualityFailures,
metric_checks: checks,
rationale
};
}
export function evaluateP0BaselineStabilityGate(input: {
metrics: P0EvalMetricVector;
qualityGapMetrics: P0QualityGapMetricVector;
acceptanceThresholds?: Partial<P0AcceptanceThresholds>;
qualityGapThresholds?: Partial<P0QualityGapThresholds>;
}): P0BaselineStabilityResult {
const acceptance = evaluateP0AcceptanceGate({
metrics: input.metrics,
thresholds: input.acceptanceThresholds
});
const qualityGapThresholds: P0QualityGapThresholds = {
...P0_DEFAULT_QUALITY_GAP_THRESHOLDS,
...(input.qualityGapThresholds ?? {})
};
const qualityGapChecks = buildQualityGapChecks({
metrics: input.qualityGapMetrics,
thresholds: qualityGapThresholds
});
const qualityGapFailures = qualityGapChecks.filter((item) => !item.passed);
const baselineIntegrityPassed = acceptance.blocking_failures.length === 0;
const qualityGapsOpen = acceptance.quality_failures.length > 0 || qualityGapFailures.length > 0;
const verdict: P0BaselineStabilityVerdict =
baselineIntegrityPassed && !qualityGapsOpen ? "P0_BASELINE_STABLE" : "P0_BASELINE_STABLE_WITH_OPEN_QUALITY_GAPS";
const rationale: string[] = [];
if (!baselineIntegrityPassed) {
rationale.push("Blocking baseline regressions detected:");
rationale.push(...acceptance.blocking_failures.map((item) => `- ${toFailureLine(item)}`));
}
if (acceptance.quality_failures.length > 0) {
rationale.push("Legacy quality thresholds still below full acceptance:");
rationale.push(...acceptance.quality_failures.map((item) => `- ${toFailureLine(item)}`));
}
if (qualityGapFailures.length > 0) {
rationale.push("Wave 9 quality-gap thresholds still open:");
rationale.push(...qualityGapFailures.map((item) => `- ${toQualityGapFailureLine(item)}`));
}
if (rationale.length === 0) {
rationale.push("Baseline is stable and Wave 9 quality-gap thresholds are satisfied.");
}
return {
verdict,
baseline_integrity_passed: baselineIntegrityPassed,
quality_gaps_open: qualityGapsOpen,
blocking_regressions: acceptance.blocking_failures,
legacy_quality_failures: acceptance.quality_failures,
quality_gap_failures: qualityGapFailures,
quality_gap_checks: qualityGapChecks,
rationale
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,425 @@
import type { ProblemUnitType } from "../types/stage2ProblemUnits";
export type P0DomainId = "settlements_60_62" | "vat_document_register_book" | "month_close_costs_20_44";
export type P0QueryClass =
| "symptom_first"
| "lifecycle_first"
| "causal_query"
| "mixed_ambiguity"
| "followup_investigation"
| "noisy_input"
| "translit_noisy"
| "multi_intent";
export type P0RouteExpected = "hybrid_store_plus_live" | "store_feature_risk" | "store_canonical" | "batch_refresh_then_store" | "live_mcp_drilldown" | "no_route";
export interface P0ExpectedAnnotations {
domain_expected: P0DomainId;
route_expected: P0RouteExpected | P0RouteExpected[];
problem_unit_type_expected: ProblemUnitType;
mechanism_class_expected: string;
evidence_expectation: string;
must_show_limitation: boolean;
must_not_leak_internal: boolean;
first_check_action_expected: string;
}
export interface P0EvalCase {
case_id: string;
domain: P0DomainId;
user_query_raw: string;
query_class: P0QueryClass;
expected_problem_unit_type: ProblemUnitType;
expected_mechanism_summary: string;
expected_first_check: string;
must_include_limitations: boolean;
forbidden_leakage_tokens: string[];
followup_seed_query?: string;
followup_expected_context_tokens?: string[];
notes: string;
expected_annotations: P0ExpectedAnnotations;
}
export interface P0EvalCorpusFile {
suite_id: string;
suite_version: string;
schema_version: string;
scenario_count: number;
case_ids: string[];
cases: P0EvalCase[];
}
export interface P0EvalMetricVector {
problem_first_answer_rate: number;
mechanism_coherence_score: number;
entity_leakage_rate: number;
accountant_actionability_score: number;
route_correctness_rate: number;
domain_purity_rate: number;
limitation_honesty_rate: number;
top_problem_unit_match_rate: number;
}
export type P0MetricName = keyof P0EvalMetricVector;
export interface P0QualityGapMetricVector {
generic_explanation_rate: number;
false_confidence_rate: number;
mechanism_specificity_score: number;
followup_context_retention_score: number;
}
export type P0QualityGapMetricName = keyof P0QualityGapMetricVector;
export interface P0MetricDefinition {
description: string;
unit: "rate" | "score";
direction: "higher_is_better" | "lower_is_better";
}
export interface P0QualityGapMetricDefinition {
description: string;
unit: "rate" | "score";
direction: "higher_is_better" | "lower_is_better";
}
export const P0_METRIC_DEFINITIONS: Record<P0MetricName, P0MetricDefinition> = {
problem_first_answer_rate: {
description: "Share of cases where answer is problem-first and avoids entity-dump framing.",
unit: "rate",
direction: "higher_is_better"
},
mechanism_coherence_score: {
description: "Average 0..5 score for mechanism clarity and alignment with expected defect class.",
unit: "score",
direction: "higher_is_better"
},
entity_leakage_rate: {
description: "Share of cases with internal/technical leakage in user-facing answer.",
unit: "rate",
direction: "lower_is_better"
},
accountant_actionability_score: {
description: "Average 0..5 score for first-check concreteness and practical accounting actionability.",
unit: "score",
direction: "higher_is_better"
},
route_correctness_rate: {
description: "Share of cases routed to expected path class for query type.",
unit: "rate",
direction: "higher_is_better"
},
domain_purity_rate: {
description: "Share of cases where top-3 retrieval stays inside expected P0 domain boundary.",
unit: "rate",
direction: "higher_is_better"
},
limitation_honesty_rate: {
description: "Share of cases where mandatory limitations are explicit when evidence is incomplete.",
unit: "rate",
direction: "higher_is_better"
},
top_problem_unit_match_rate: {
description: "Share of cases where top problem unit type matches expected annotation.",
unit: "rate",
direction: "higher_is_better"
}
};
export const P0_QUALITY_GAP_METRIC_DEFINITIONS: Record<P0QualityGapMetricName, P0QualityGapMetricDefinition> = {
generic_explanation_rate: {
description: "Share of cases where direct answer stays generic and mechanism/actionability specificity is insufficient.",
unit: "rate",
direction: "lower_is_better"
},
false_confidence_rate: {
description: "Share of cases with overconfident framing not supported by evidence/limitation state.",
unit: "rate",
direction: "lower_is_better"
},
mechanism_specificity_score: {
description: "Average 0..5 score of mechanism-level specificity in accountant-facing answer.",
unit: "score",
direction: "higher_is_better"
},
followup_context_retention_score: {
description: "Rate of correct context retention in follow-up investigation cases.",
unit: "rate",
direction: "higher_is_better"
}
};
export const P0_DEFAULT_FORBIDDEN_LEAKAGE_TOKENS = [
"graph_",
"domain_scope",
"relation_patterns",
"account_scope",
"semantic_profile",
"route",
"profile",
"store_canonical",
"hybrid_store_plus_live",
"deterministic_v2",
"candidate_evidence",
"problem_unit",
"raw_entities",
"graph_domain_scope"
];
export interface P0AcceptanceThresholds {
problem_first_answer_rate_min: number;
mechanism_coherence_score_min: number;
entity_leakage_rate_max: number;
accountant_actionability_score_min: number;
route_correctness_rate_min: number;
domain_purity_rate_min: number;
limitation_honesty_rate_min: number;
top_problem_unit_match_rate_min: number;
}
export interface P0QualityGapThresholds {
generic_explanation_rate_max: number;
false_confidence_rate_max: number;
mechanism_specificity_score_min: number;
followup_context_retention_score_min: number;
}
export const P0_DEFAULT_ACCEPTANCE_THRESHOLDS: P0AcceptanceThresholds = {
problem_first_answer_rate_min: 0.8,
mechanism_coherence_score_min: 3.5,
entity_leakage_rate_max: 0.08,
accountant_actionability_score_min: 3.5,
route_correctness_rate_min: 0.85,
domain_purity_rate_min: 0.9,
limitation_honesty_rate_min: 0.9,
top_problem_unit_match_rate_min: 0.65
};
export const P0_DEFAULT_QUALITY_GAP_THRESHOLDS: P0QualityGapThresholds = {
generic_explanation_rate_max: 0.2,
false_confidence_rate_max: 0.12,
mechanism_specificity_score_min: 3.0,
followup_context_retention_score_min: 0.75
};
function isProblemUnitType(value: unknown): value is ProblemUnitType {
return (
value === "document_conflict" ||
value === "broken_chain_segment" ||
value === "lifecycle_anomaly_node" ||
value === "unresolved_settlement_cluster" ||
value === "period_risk_cluster" ||
value === "cross_branch_inconsistency_cluster"
);
}
function isP0DomainId(value: unknown): value is P0DomainId {
return value === "settlements_60_62" || value === "vat_document_register_book" || value === "month_close_costs_20_44";
}
function isP0QueryClass(value: unknown): value is P0QueryClass {
return (
value === "symptom_first" ||
value === "lifecycle_first" ||
value === "causal_query" ||
value === "mixed_ambiguity" ||
value === "followup_investigation" ||
value === "noisy_input" ||
value === "translit_noisy" ||
value === "multi_intent"
);
}
function isRouteExpected(value: unknown): value is P0RouteExpected {
return (
value === "hybrid_store_plus_live" ||
value === "store_feature_risk" ||
value === "store_canonical" ||
value === "batch_refresh_then_store" ||
value === "live_mcp_drilldown" ||
value === "no_route"
);
}
function assertString(value: unknown, fieldPath: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`Invalid corpus field '${fieldPath}': non-empty string is required.`);
}
return value.trim();
}
function assertBoolean(value: unknown, fieldPath: string): boolean {
if (typeof value !== "boolean") {
throw new Error(`Invalid corpus field '${fieldPath}': boolean is required.`);
}
return value;
}
function assertStringArray(value: unknown, fieldPath: string): string[] {
if (!Array.isArray(value)) {
throw new Error(`Invalid corpus field '${fieldPath}': string[] is required.`);
}
const output = value.map((item, index) => assertString(item, `${fieldPath}[${index}]`));
return output;
}
function assertRouteExpected(value: unknown, fieldPath: string): P0RouteExpected | P0RouteExpected[] {
if (Array.isArray(value)) {
const routes = value.map((item, index) => {
if (!isRouteExpected(item)) {
throw new Error(`Invalid corpus field '${fieldPath}[${index}]': unsupported route '${String(item)}'.`);
}
return item;
});
if (routes.length === 0) {
throw new Error(`Invalid corpus field '${fieldPath}': route list must not be empty.`);
}
return routes;
}
if (!isRouteExpected(value)) {
throw new Error(`Invalid corpus field '${fieldPath}': unsupported route '${String(value)}'.`);
}
return value;
}
function parseCase(raw: unknown, index: number): P0EvalCase {
if (!raw || typeof raw !== "object") {
throw new Error(`Invalid corpus case at index ${index}: object is required.`);
}
const row = raw as Record<string, unknown>;
const expected = row.expected_annotations;
if (!expected || typeof expected !== "object") {
throw new Error(`Invalid corpus case '${String(row.case_id ?? index)}': expected_annotations is required.`);
}
const annotations = expected as Record<string, unknown>;
const domain = row.domain;
if (!isP0DomainId(domain)) {
throw new Error(`Invalid corpus case '${String(row.case_id ?? index)}': unsupported domain '${String(domain)}'.`);
}
const queryClass = row.query_class;
if (!isP0QueryClass(queryClass)) {
throw new Error(`Invalid corpus case '${String(row.case_id ?? index)}': unsupported query_class '${String(queryClass)}'.`);
}
const expectedProblemType = row.expected_problem_unit_type;
if (!isProblemUnitType(expectedProblemType)) {
throw new Error(
`Invalid corpus case '${String(row.case_id ?? index)}': expected_problem_unit_type '${String(expectedProblemType)}' is not supported.`
);
}
const annotationProblemType = annotations.problem_unit_type_expected;
if (!isProblemUnitType(annotationProblemType)) {
throw new Error(
`Invalid corpus case '${String(row.case_id ?? index)}': expected_annotations.problem_unit_type_expected '${String(annotationProblemType)}' is not supported.`
);
}
const annotationDomain = annotations.domain_expected;
if (!isP0DomainId(annotationDomain)) {
throw new Error(
`Invalid corpus case '${String(row.case_id ?? index)}': expected_annotations.domain_expected '${String(annotationDomain)}' is not supported.`
);
}
const followupSeedQuery =
typeof row.followup_seed_query === "string" && row.followup_seed_query.trim() ? row.followup_seed_query.trim() : undefined;
const followupExpectedContextTokens = Array.isArray(row.followup_expected_context_tokens)
? assertStringArray(row.followup_expected_context_tokens, `cases[${index}].followup_expected_context_tokens`)
: undefined;
if (queryClass === "followup_investigation" && !followupSeedQuery) {
throw new Error(
`Invalid corpus case '${String(row.case_id ?? index)}': followup_investigation requires followup_seed_query.`
);
}
return {
case_id: assertString(row.case_id, `cases[${index}].case_id`),
domain,
user_query_raw: assertString(row.user_query_raw, `cases[${index}].user_query_raw`),
query_class: queryClass,
expected_problem_unit_type: expectedProblemType,
expected_mechanism_summary: assertString(row.expected_mechanism_summary, `cases[${index}].expected_mechanism_summary`),
expected_first_check: assertString(row.expected_first_check, `cases[${index}].expected_first_check`),
must_include_limitations: assertBoolean(row.must_include_limitations, `cases[${index}].must_include_limitations`),
forbidden_leakage_tokens: assertStringArray(row.forbidden_leakage_tokens, `cases[${index}].forbidden_leakage_tokens`),
followup_seed_query: followupSeedQuery,
followup_expected_context_tokens: followupExpectedContextTokens,
notes: assertString(row.notes, `cases[${index}].notes`),
expected_annotations: {
domain_expected: annotationDomain,
route_expected: assertRouteExpected(annotations.route_expected, `cases[${index}].expected_annotations.route_expected`),
problem_unit_type_expected: annotationProblemType,
mechanism_class_expected: assertString(
annotations.mechanism_class_expected,
`cases[${index}].expected_annotations.mechanism_class_expected`
),
evidence_expectation: assertString(annotations.evidence_expectation, `cases[${index}].expected_annotations.evidence_expectation`),
must_show_limitation: assertBoolean(annotations.must_show_limitation, `cases[${index}].expected_annotations.must_show_limitation`),
must_not_leak_internal: assertBoolean(
annotations.must_not_leak_internal,
`cases[${index}].expected_annotations.must_not_leak_internal`
),
first_check_action_expected: assertString(
annotations.first_check_action_expected,
`cases[${index}].expected_annotations.first_check_action_expected`
)
}
};
}
export function validateP0EvalCorpus(input: unknown): P0EvalCorpusFile {
if (!input || typeof input !== "object") {
throw new Error("Invalid P0 corpus: root object is required.");
}
const parsed = input as Record<string, unknown>;
const casesRaw = parsed.cases;
if (!Array.isArray(casesRaw)) {
throw new Error("Invalid P0 corpus: cases[] is required.");
}
const cases = casesRaw.map((item, index) => parseCase(item, index));
if (cases.length < 30 || cases.length > 150) {
throw new Error(`Invalid P0 corpus: expected 30..150 cases, got ${cases.length}.`);
}
const caseIds = assertStringArray(parsed.case_ids, "case_ids");
const declaredIds = [...caseIds].sort();
const actualIds = cases.map((item) => item.case_id).sort();
const idsMatch = declaredIds.length === actualIds.length && declaredIds.every((value, index) => value === actualIds[index]);
if (!idsMatch) {
throw new Error("Invalid P0 corpus: case_ids[] does not match cases[].");
}
const scenarioCount = Number(parsed.scenario_count ?? 0);
if (!Number.isFinite(scenarioCount) || scenarioCount !== cases.length) {
throw new Error("Invalid P0 corpus: scenario_count must match cases.length.");
}
const perDomain = cases.reduce<Record<P0DomainId, number>>(
(acc, item) => {
acc[item.domain] += 1;
return acc;
},
{
settlements_60_62: 0,
vat_document_register_book: 0,
month_close_costs_20_44: 0
}
);
for (const [domain, count] of Object.entries(perDomain)) {
if (count < 10) {
throw new Error(`Invalid P0 corpus: domain '${domain}' must contain at least 10 cases, got ${count}.`);
}
}
return {
suite_id: assertString(parsed.suite_id, "suite_id"),
suite_version: assertString(parsed.suite_version, "suite_version"),
schema_version: assertString(parsed.schema_version, "schema_version"),
scenario_count: scenarioCount,
case_ids: caseIds,
cases
};
}
export function normalizeExpectedRoutes(value: P0RouteExpected | P0RouteExpected[]): P0RouteExpected[] {
return Array.isArray(value) ? value : [value];
}