Optimize cutover compare/monitor per the 2026-10-12 plan.

Correctness (A): normalize port-map names with the template rules (snapshot v2,
v1 replays unchanged); protect mappings/templates that are referenced; stop
dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated;
validate baseline ownership on project create; one canonical key form; fix
built-in template tolerances and upgrade old packaged values in place.

Cross-vendor identity (B): single identity pipeline for every path, per-side and
per-vendor interface rules, value maps, filters after normalization, mapping
pre-flight for compare and cutover projects, SQL eligibility fixes.

Data quality (V): no_data / suspect_parse / not_collected sheet states, command
slice coverage, directional compare modes, value-sorted duplicate pairing,
before-side raw names in results and CSV.

Robustness (H): bounded auto-run history, background evaluate/finish with
polling, manual route tables out of the acceptance gate, baseline row cache,
scoped duplicate-key failures, merged HF tasks with a per-NE session cap,
same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red
tickets.

Frontend: pre-flight report, mapping table view, template editor support for the
new rules, data-state banners. Docs: canonical field contract.

Not included: Huawei status parsers (no real samples), SQL-path route compare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 11:51:55 +08:00
parent fff7d63b9b
commit 315539e0fd
59 changed files with 6483 additions and 487 deletions

View file

@ -549,6 +549,12 @@ class FieldRuleIn(BaseModel):
normalize: str = ""
ignore: bool = False
tolerance: float | None = None
# Cross-vendor identity rules (see identity_pipeline).
value_map: dict[str, str] | None = None
side: str = ""
apply_to_key: bool | None = None
# Order for compare=not_worse: earlier entries are better.
rank: list[str] | None = None
class RowFilterIn(BaseModel):
@ -575,6 +581,9 @@ class TemplateMetricIn(BaseModel):
row_filters: list[dict[str, Any]] = Field(default_factory=list)
field_rules: list[FieldRuleIn] = Field(default_factory=list)
ignore_port_changes: bool | None = None
unmapped_port_policy: str = ""
duplicate_pairing: str = ""
builtin_vendor_canonical: bool | None = None
class TemplateIn(BaseModel):
@ -623,6 +632,16 @@ class MappingIn(BaseModel):
rows: list[MappingRowIn] = Field(default_factory=list)
class PreflightIn(BaseModel):
template_id: str = ""
enabled_sheet_ids: list[str] = Field(default_factory=list)
mapping_id: str = ""
# Unsaved editor rows win over ``mapping_id`` when given.
mapping_rows: list[MappingRowIn] | None = None
before_batch_id: str
after_batch_id: str
class ValidateMappingIn(BaseModel):
mapping_id: str
before_batch_id: str
@ -711,6 +730,11 @@ def api_validate_mapping(body: ValidateMappingIn, db: Session = Depends(get_db))
)
@router.post("/compare/preflight")
def api_compare_preflight(body: PreflightIn, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.preflight_compare(db, body.model_dump())
@router.get("/compare/jobs")
def api_list_jobs(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_jobs(db)}