Files
mdpolish/src/mdpolish/pipeline.py
T
Bepr4 3edeeaf30e 实现第一版内存清洗核心
落实不可变数据契约、组件基类、原子修改执行器与顺序流水线。补充稳定性复查、审计记录、测试和当前机制文档。
2026-08-22 01:03:03 +08:00

304 lines
12 KiB
Python

"""Sequential orchestration and final stability review."""
from __future__ import annotations
from collections.abc import Iterable
from mdpolish.component import Component, ComponentContractError
from mdpolish.edits import apply_component_batch, validate_component_batch
from mdpolish.models import (
Change,
ComponentInfo,
DocumentSnapshot,
ErrorStage,
ProposalReference,
ProposedChange,
ResidualProposal,
RunError,
RunStatus,
TransformResult,
)
class PipelineContractError(ValueError):
"""A pipeline composition does not satisfy the approved contract."""
class Pipeline:
"""Run selected components once, then review the final snapshot for stability."""
def __init__(self, components: Iterable[Component]) -> None:
self._components = tuple(components)
@property
def components(self) -> tuple[Component, ...]:
return self._components
def transform(self, markdown: str) -> TransformResult:
input_snapshot = DocumentSnapshot(markdown)
component_infos, preflight_error = self._preflight_components()
if preflight_error is not None:
return self._failed_result(
input_snapshot=input_snapshot,
current_snapshot=input_snapshot,
component_infos=component_infos,
changes=(),
errors=(preflight_error,),
)
current_snapshot = input_snapshot
changes: list[Change] = []
for position, (component, expected_info) in enumerate(zip(self._components, component_infos, strict=True)):
current_info, metadata_error = self._current_component_info(
component=component,
expected_info=expected_info,
position=position,
stage=ErrorStage.TRANSFORM,
)
if metadata_error is not None:
return self._failed_result(
input_snapshot=input_snapshot,
current_snapshot=current_snapshot,
component_infos=component_infos,
changes=tuple(changes),
errors=(metadata_error,),
)
proposals, proposal_error = self._proposals(
component=component,
component_info=current_info,
position=position,
stage=ErrorStage.TRANSFORM,
snapshot=current_snapshot,
)
if proposal_error is not None:
return self._failed_result(
input_snapshot=input_snapshot,
current_snapshot=current_snapshot,
component_infos=component_infos,
changes=tuple(changes),
errors=(proposal_error,),
)
try:
applied_batch = apply_component_batch(
snapshot=current_snapshot,
proposals=proposals,
component=current_info,
component_position=position,
)
except Exception as error:
return self._failed_result(
input_snapshot=input_snapshot,
current_snapshot=current_snapshot,
component_infos=component_infos,
changes=tuple(changes),
errors=(
self._run_error(
component_info=current_info,
position=position,
stage=ErrorStage.TRANSFORM,
error=error,
unexpected_message="component edit batch does not satisfy the edit contract",
),
),
)
current_snapshot = applied_batch.snapshot
changes.extend(applied_batch.changes)
review_errors: list[RunError] = []
residual_proposals: list[ResidualProposal] = []
for position, (component, expected_info) in enumerate(zip(self._components, component_infos, strict=True)):
current_info, metadata_error = self._current_component_info(
component=component,
expected_info=expected_info,
position=position,
stage=ErrorStage.FINAL_REVIEW,
)
if metadata_error is not None:
review_errors.append(metadata_error)
continue
proposals, proposal_error = self._proposals(
component=component,
component_info=current_info,
position=position,
stage=ErrorStage.FINAL_REVIEW,
snapshot=current_snapshot,
)
if proposal_error is not None:
review_errors.append(proposal_error)
continue
try:
validate_component_batch(current_snapshot, proposals)
except Exception as error:
review_errors.append(
self._run_error(
component_info=current_info,
position=position,
stage=ErrorStage.FINAL_REVIEW,
error=error,
unexpected_message="component edit batch does not satisfy the edit contract",
)
)
continue
residual_proposals.extend(
ResidualProposal(
component_id=current_info.component_id,
component_version=current_info.version,
component_position=position,
proposal_ref=ProposalReference(
component_position=position,
snapshot_sha256=current_snapshot.sha256,
proposal_index=proposal_index,
),
proposal=proposal,
)
for proposal_index, proposal in enumerate(proposals)
)
if review_errors:
return self._failed_result(
input_snapshot=input_snapshot,
current_snapshot=current_snapshot,
component_infos=component_infos,
changes=tuple(changes),
errors=tuple(review_errors),
residual_proposals=tuple(residual_proposals),
)
if residual_proposals:
return TransformResult(
status=RunStatus.UNSTABLE,
input_sha256=input_snapshot.sha256,
current_sha256=current_snapshot.sha256,
components=component_infos,
changes=tuple(changes),
residual_proposals=tuple(residual_proposals),
partial_markdown=current_snapshot.markdown,
)
return TransformResult(
status=RunStatus.SUCCESS,
input_sha256=input_snapshot.sha256,
current_sha256=current_snapshot.sha256,
components=component_infos,
changes=tuple(changes),
output_markdown=current_snapshot.markdown,
)
def _preflight_components(self) -> tuple[tuple[ComponentInfo, ...], RunError | None]:
component_infos: list[ComponentInfo] = []
seen_ids: set[str] = set()
for position, component in enumerate(self._components):
if not isinstance(component, Component):
error = ComponentContractError("pipeline entries must be Component instances")
return tuple(component_infos), self._run_error(
component_info=None,
position=position,
stage=ErrorStage.TRANSFORM,
error=error,
unexpected_message="a pipeline entry is not a Component instance",
)
try:
component_info = component._component_info()
except Exception as error:
return tuple(component_infos), self._run_error(
component_info=None,
position=position,
stage=ErrorStage.TRANSFORM,
error=error,
unexpected_message="component metadata does not satisfy the component contract",
)
component_infos.append(component_info)
if component_info.component_id in seen_ids:
duplicate_error = PipelineContractError("pipeline component_id values must be unique")
return tuple(component_infos), self._run_error(
component_info=component_info,
position=position,
stage=ErrorStage.TRANSFORM,
error=duplicate_error,
unexpected_message="pipeline component_id values must be unique",
)
seen_ids.add(component_info.component_id)
return tuple(component_infos), None
def _current_component_info(
self,
component: Component,
expected_info: ComponentInfo,
position: int,
stage: ErrorStage,
) -> tuple[ComponentInfo, RunError | None]:
try:
current_info = component._component_info()
if current_info != expected_info:
raise ComponentContractError("component metadata changed during pipeline execution")
except Exception as error:
return expected_info, self._run_error(
component_info=expected_info,
position=position,
stage=stage,
error=error,
unexpected_message="component metadata does not satisfy the component contract",
)
return current_info, None
def _proposals(
self,
component: Component,
component_info: ComponentInfo,
position: int,
stage: ErrorStage,
snapshot: DocumentSnapshot,
) -> tuple[tuple[ProposedChange, ...], RunError | None]:
try:
proposals = component._collect_proposals(snapshot)
except Exception as error:
return (), self._run_error(
component_info=component_info,
position=position,
stage=stage,
error=error,
unexpected_message="component could not produce contract-valid proposals",
)
return proposals, None
@staticmethod
def _run_error(
component_info: ComponentInfo | None,
position: int,
stage: ErrorStage,
error: Exception,
unexpected_message: str,
) -> RunError:
return RunError(
component_id=component_info.component_id if component_info is not None else "<invalid>",
component_version=component_info.version if component_info is not None else "<invalid>",
component_position=position,
stage=stage,
error_type=type(error).__name__,
message=unexpected_message,
)
@staticmethod
def _failed_result(
input_snapshot: DocumentSnapshot,
current_snapshot: DocumentSnapshot,
component_infos: tuple[ComponentInfo, ...],
changes: tuple[Change, ...],
errors: tuple[RunError, ...],
residual_proposals: tuple[ResidualProposal, ...] = (),
) -> TransformResult:
return TransformResult(
status=RunStatus.FAILED,
input_sha256=input_snapshot.sha256,
current_sha256=current_snapshot.sha256,
components=component_infos,
changes=changes,
errors=errors,
residual_proposals=residual_proposals,
partial_markdown=current_snapshot.markdown,
)