auto-sync: tudo-para-ia-mais-humana 2026-05-02 03:04:41
This commit is contained in:
@@ -20,6 +20,7 @@ from .mcp_contract import (
|
|||||||
same_source_validation_payload,
|
same_source_validation_payload,
|
||||||
ui_renderer_policy_markdown,
|
ui_renderer_policy_markdown,
|
||||||
)
|
)
|
||||||
|
from .mcp_admin_route_acceptance import run_admin_route_acceptance
|
||||||
from .mcp_gateway_access_policy import run_access_policy_gate
|
from .mcp_gateway_access_policy import run_access_policy_gate
|
||||||
from .mcp_publication_gate import run_publication_gate
|
from .mcp_publication_gate import run_publication_gate
|
||||||
from .mcp_transit_ledger import build_mcp_transit_ledger, mcp_transit_csv, mcp_transit_ledger_compact_json, mcp_transit_markdown
|
from .mcp_transit_ledger import build_mcp_transit_ledger, mcp_transit_csv, mcp_transit_ledger_compact_json, mcp_transit_markdown
|
||||||
@@ -128,6 +129,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
sync_audit.add_argument("--central-repo-root", default="G:/_codex-git/nucleo-gestao-operacional")
|
sync_audit.add_argument("--central-repo-root", default="G:/_codex-git/nucleo-gestao-operacional")
|
||||||
sync_audit.add_argument("--central-platform-folder", default="")
|
sync_audit.add_argument("--central-platform-folder", default="")
|
||||||
sync_audit.add_argument("--fetch", action="store_true")
|
sync_audit.add_argument("--fetch", action="store_true")
|
||||||
|
admin_acceptance = sub.add_parser("mcp-admin-route-acceptance", help="Write MCP-only administration route acceptance artifacts.")
|
||||||
|
admin_acceptance.add_argument("--project-root", default="G:/_codex-git/tudo-para-ia-mais-humana")
|
||||||
|
admin_acceptance.add_argument("--central-platform-folder", default="")
|
||||||
|
admin_acceptance.add_argument("--platform-id", default="")
|
||||||
|
admin_acceptance.add_argument("--profile-id", default="")
|
||||||
|
admin_acceptance.add_argument("--operation", default="")
|
||||||
|
admin_acceptance.add_argument("--status", default="")
|
||||||
|
admin_acceptance.add_argument("--limit", type=int, default=120)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -540,6 +549,25 @@ def command_targeted_sync_audit(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def command_mcp_admin_route_acceptance(args: argparse.Namespace) -> int:
|
||||||
|
central_platform_folder = Path(args.central_platform_folder) if args.central_platform_folder else None
|
||||||
|
report, records = run_admin_route_acceptance(
|
||||||
|
project_root=Path(args.project_root),
|
||||||
|
central_platform_folder=central_platform_folder,
|
||||||
|
platform_id=args.platform_id or None,
|
||||||
|
profile_id=args.profile_id or None,
|
||||||
|
operation=args.operation or None,
|
||||||
|
status=args.status or None,
|
||||||
|
limit=int(args.limit),
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"report": report.to_dict(),
|
||||||
|
"generatedFiles": [record.path for record in records],
|
||||||
|
}
|
||||||
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
@@ -579,6 +607,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return command_workspace_hygiene(args)
|
return command_workspace_hygiene(args)
|
||||||
if args.command == "targeted-sync-audit":
|
if args.command == "targeted-sync-audit":
|
||||||
return command_targeted_sync_audit(args)
|
return command_targeted_sync_audit(args)
|
||||||
|
if args.command == "mcp-admin-route-acceptance":
|
||||||
|
return command_mcp_admin_route_acceptance(args)
|
||||||
parser.error(f"unknown command: {args.command}")
|
parser.error(f"unknown command: {args.command}")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|||||||
673
src/mais_humana/mcp_admin_route_acceptance.py
Normal file
673
src/mais_humana/mcp_admin_route_acceptance.py
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
"""Acceptance catalog for MCP-only administration routes.
|
||||||
|
|
||||||
|
The control contracts already define the administrative routes that must pass
|
||||||
|
through ``tudo-para-ia-mcps-internos-plataform``. This module turns the
|
||||||
|
generated contracts into a compact, executable acceptance layer: the gateway,
|
||||||
|
GPT, UI renderer, central reports, and service-order closeout can all read the
|
||||||
|
same case list without traversing the large contract object graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Mapping, Sequence
|
||||||
|
|
||||||
|
from .mcp_contract import MCP_CONTROL_PLANE_ID, PROVIDER_ID, stable_hash
|
||||||
|
from .models import GeneratedFile, as_plain_data, merge_unique, utc_now
|
||||||
|
from .redaction import redact_sensitive_text
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .generated_mcp_admin_route_acceptance import (
|
||||||
|
ADMIN_ROUTE_ACCEPTANCE_CASES as GENERATED_ADMIN_ROUTE_ACCEPTANCE_CASES,
|
||||||
|
SOURCE_HASH as GENERATED_SOURCE_HASH,
|
||||||
|
)
|
||||||
|
except ImportError: # pragma: no cover - exercised before the generator runs.
|
||||||
|
GENERATED_ADMIN_ROUTE_ACCEPTANCE_CASES: tuple[Mapping[str, Any], ...] = ()
|
||||||
|
GENERATED_SOURCE_HASH = ""
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRouteAcceptanceStatus(str, Enum):
|
||||||
|
"""Acceptance status for one generated route case."""
|
||||||
|
|
||||||
|
READY = "ready"
|
||||||
|
PARTIAL = "partial"
|
||||||
|
BLOCKED = "blocked"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRouteOperation(str, Enum):
|
||||||
|
"""Canonical administration operation names."""
|
||||||
|
|
||||||
|
CONSULTA = "consulta"
|
||||||
|
DIAGNOSTICO = "diagnostico"
|
||||||
|
ACAO = "acao"
|
||||||
|
AUDITORIA = "auditoria"
|
||||||
|
EXPLICACAO = "explicacao"
|
||||||
|
|
||||||
|
|
||||||
|
MANDATORY_TRANSIT_FIELDS = (
|
||||||
|
"origin",
|
||||||
|
"destination",
|
||||||
|
"tool",
|
||||||
|
"payload",
|
||||||
|
"actor",
|
||||||
|
"permission",
|
||||||
|
"result",
|
||||||
|
"traceId",
|
||||||
|
"auditId",
|
||||||
|
"timestamp",
|
||||||
|
)
|
||||||
|
|
||||||
|
MANDATORY_PAYLOAD_FIELDS = (
|
||||||
|
"adminRouteId",
|
||||||
|
"adminRouteKind",
|
||||||
|
"sourceEndpoint",
|
||||||
|
"sourceToolId",
|
||||||
|
"sourcePayloadHash",
|
||||||
|
"sourceRecordsHash",
|
||||||
|
"truthState",
|
||||||
|
"panelReady",
|
||||||
|
"gptExplainable",
|
||||||
|
"humanNextAction",
|
||||||
|
"permissionScope",
|
||||||
|
"mcpOnlyAdministration",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AdminRouteAcceptanceCase:
|
||||||
|
"""One MCP-only administration route acceptance case."""
|
||||||
|
|
||||||
|
route_id: str
|
||||||
|
platform_id: str
|
||||||
|
profile_id: str
|
||||||
|
operation: str
|
||||||
|
tool_id: str
|
||||||
|
source_tool_id: str
|
||||||
|
permission_scope: str
|
||||||
|
source_endpoint: str
|
||||||
|
source_payload_hash: str
|
||||||
|
source_records_hash: str
|
||||||
|
evidence_id: str
|
||||||
|
truth_state: str
|
||||||
|
panel_ready: bool
|
||||||
|
gpt_explainable: bool
|
||||||
|
same_source: bool
|
||||||
|
approval_required: bool
|
||||||
|
dry_run_supported: bool
|
||||||
|
required_transit_fields: tuple[str, ...]
|
||||||
|
required_payload_fields: tuple[str, ...]
|
||||||
|
validation_steps: tuple[str, ...]
|
||||||
|
redaction_requirements: tuple[str, ...]
|
||||||
|
pending_if_missing: str
|
||||||
|
order_ids: tuple[str, ...]
|
||||||
|
policy_tags: tuple[str, ...]
|
||||||
|
maturity_level: int
|
||||||
|
status: AdminRouteAcceptanceStatus
|
||||||
|
blocker_reasons: tuple[str, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def route_key(self) -> str:
|
||||||
|
return f"{self.platform_id}/{self.profile_id}/{self.operation}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready(self) -> bool:
|
||||||
|
return self.status == AdminRouteAcceptanceStatus.READY
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return as_plain_data(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AdminRouteAcceptanceSummary:
|
||||||
|
"""Aggregated acceptance result for all generated administration routes."""
|
||||||
|
|
||||||
|
generated_at: str
|
||||||
|
provider_id: str
|
||||||
|
control_plane_id: str
|
||||||
|
source_hash: str
|
||||||
|
total_cases: int
|
||||||
|
ready_cases: int
|
||||||
|
partial_cases: int
|
||||||
|
blocked_cases: int
|
||||||
|
platforms_count: int
|
||||||
|
profiles_count: int
|
||||||
|
operations: Mapping[str, int]
|
||||||
|
platforms: Mapping[str, int]
|
||||||
|
blockers: tuple[str, ...]
|
||||||
|
evidence_id: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> AdminRouteAcceptanceStatus:
|
||||||
|
if self.blocked_cases:
|
||||||
|
return AdminRouteAcceptanceStatus.BLOCKED
|
||||||
|
if self.partial_cases:
|
||||||
|
return AdminRouteAcceptanceStatus.PARTIAL
|
||||||
|
return AdminRouteAcceptanceStatus.READY if self.total_cases else AdminRouteAcceptanceStatus.BLOCKED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready_ratio(self) -> float:
|
||||||
|
if not self.total_cases:
|
||||||
|
return 0.0
|
||||||
|
return self.ready_cases / self.total_cases
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
data = as_plain_data(self)
|
||||||
|
data["status"] = self.status.value
|
||||||
|
data["readyRatio"] = self.ready_ratio
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AdminRouteAcceptanceReport:
|
||||||
|
"""Full acceptance report with a bounded sample and summary."""
|
||||||
|
|
||||||
|
report_id: str
|
||||||
|
generated_at: str
|
||||||
|
summary: AdminRouteAcceptanceSummary
|
||||||
|
returned_cases: tuple[AdminRouteAcceptanceCase, ...]
|
||||||
|
sample_limit: int
|
||||||
|
filters: Mapping[str, str]
|
||||||
|
generated_files: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> AdminRouteAcceptanceStatus:
|
||||||
|
return self.summary.status
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
data = as_plain_data(self)
|
||||||
|
data["status"] = self.status.value
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _tuple(value: object) -> tuple[str, ...]:
|
||||||
|
if value is None:
|
||||||
|
return ()
|
||||||
|
if isinstance(value, str):
|
||||||
|
return (value,)
|
||||||
|
if isinstance(value, Iterable):
|
||||||
|
return tuple(str(item) for item in value)
|
||||||
|
return (str(value),)
|
||||||
|
|
||||||
|
|
||||||
|
def _bool(value: object) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
return str(value).strip().lower() in {"1", "true", "yes", "sim"}
|
||||||
|
|
||||||
|
|
||||||
|
def _status(value: object) -> AdminRouteAcceptanceStatus:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
for status in AdminRouteAcceptanceStatus:
|
||||||
|
if status.value == text:
|
||||||
|
return status
|
||||||
|
return AdminRouteAcceptanceStatus.BLOCKED
|
||||||
|
|
||||||
|
|
||||||
|
def case_from_mapping(row: Mapping[str, Any]) -> AdminRouteAcceptanceCase:
|
||||||
|
"""Convert generated plain data into a typed acceptance case."""
|
||||||
|
|
||||||
|
return AdminRouteAcceptanceCase(
|
||||||
|
route_id=str(row.get("routeId") or row.get("route_id") or ""),
|
||||||
|
platform_id=str(row.get("platformId") or row.get("platform_id") or ""),
|
||||||
|
profile_id=str(row.get("profileId") or row.get("profile_id") or ""),
|
||||||
|
operation=str(row.get("operation") or ""),
|
||||||
|
tool_id=str(row.get("toolId") or row.get("tool_id") or ""),
|
||||||
|
source_tool_id=str(row.get("sourceToolId") or row.get("source_tool_id") or ""),
|
||||||
|
permission_scope=str(row.get("permissionScope") or row.get("permission_scope") or ""),
|
||||||
|
source_endpoint=str(row.get("sourceEndpoint") or row.get("source_endpoint") or ""),
|
||||||
|
source_payload_hash=str(row.get("sourcePayloadHash") or row.get("source_payload_hash") or ""),
|
||||||
|
source_records_hash=str(row.get("sourceRecordsHash") or row.get("source_records_hash") or ""),
|
||||||
|
evidence_id=str(row.get("evidenceId") or row.get("evidence_id") or ""),
|
||||||
|
truth_state=str(row.get("truthState") or row.get("truth_state") or ""),
|
||||||
|
panel_ready=_bool(row.get("panelReady") or row.get("panel_ready")),
|
||||||
|
gpt_explainable=_bool(row.get("gptExplainable") or row.get("gpt_explainable")),
|
||||||
|
same_source=_bool(row.get("sameSource") or row.get("same_source")),
|
||||||
|
approval_required=_bool(row.get("approvalRequired") or row.get("approval_required")),
|
||||||
|
dry_run_supported=_bool(row.get("dryRunSupported") or row.get("dry_run_supported")),
|
||||||
|
required_transit_fields=_tuple(row.get("requiredTransitFields") or row.get("required_transit_fields")),
|
||||||
|
required_payload_fields=_tuple(row.get("requiredPayloadFields") or row.get("required_payload_fields")),
|
||||||
|
validation_steps=_tuple(row.get("validationSteps") or row.get("validation_steps")),
|
||||||
|
redaction_requirements=_tuple(row.get("redactionRequirements") or row.get("redaction_requirements")),
|
||||||
|
pending_if_missing=str(row.get("pendingIfMissing") or row.get("pending_if_missing") or ""),
|
||||||
|
order_ids=_tuple(row.get("orderIds") or row.get("order_ids")),
|
||||||
|
policy_tags=_tuple(row.get("policyTags") or row.get("policy_tags")),
|
||||||
|
maturity_level=int(row.get("maturityLevel") or row.get("maturity_level") or 0),
|
||||||
|
status=_status(row.get("status")),
|
||||||
|
blocker_reasons=_tuple(row.get("blockerReasons") or row.get("blocker_reasons")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_admin_route_acceptance_cases(
|
||||||
|
*,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
profile_id: str | None = None,
|
||||||
|
operation: str | None = None,
|
||||||
|
status: str | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
) -> tuple[AdminRouteAcceptanceCase, ...]:
|
||||||
|
"""Return generated acceptance cases, optionally filtered."""
|
||||||
|
|
||||||
|
output: list[AdminRouteAcceptanceCase] = []
|
||||||
|
for raw in GENERATED_ADMIN_ROUTE_ACCEPTANCE_CASES:
|
||||||
|
case = case_from_mapping(raw)
|
||||||
|
if platform_id and case.platform_id != platform_id:
|
||||||
|
continue
|
||||||
|
if profile_id and case.profile_id != profile_id:
|
||||||
|
continue
|
||||||
|
if operation and case.operation != operation:
|
||||||
|
continue
|
||||||
|
if status and case.status.value != status:
|
||||||
|
continue
|
||||||
|
output.append(case)
|
||||||
|
if limit is not None and len(output) >= max(0, int(limit)):
|
||||||
|
break
|
||||||
|
return tuple(output)
|
||||||
|
|
||||||
|
|
||||||
|
def _count_by(cases: Sequence[AdminRouteAcceptanceCase], attr: str) -> dict[str, int]:
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
for case in cases:
|
||||||
|
value = str(getattr(case, attr))
|
||||||
|
counts[value] = counts.get(value, 0) + 1
|
||||||
|
return dict(sorted(counts.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_admin_route_acceptance(cases: Sequence[AdminRouteAcceptanceCase] | None = None) -> AdminRouteAcceptanceSummary:
|
||||||
|
"""Build a compact readiness summary for MCP-only administration routes."""
|
||||||
|
|
||||||
|
case_set = tuple(cases if cases is not None else iter_admin_route_acceptance_cases())
|
||||||
|
blockers = merge_unique(
|
||||||
|
f"{case.route_id}:{reason}"
|
||||||
|
for case in case_set
|
||||||
|
for reason in case.blocker_reasons
|
||||||
|
if case.status != AdminRouteAcceptanceStatus.READY
|
||||||
|
)
|
||||||
|
evidence_id = "evidence-" + stable_hash(
|
||||||
|
{
|
||||||
|
"sourceHash": GENERATED_SOURCE_HASH,
|
||||||
|
"caseCount": len(case_set),
|
||||||
|
"ready": sum(1 for case in case_set if case.status == AdminRouteAcceptanceStatus.READY),
|
||||||
|
"blockers": blockers,
|
||||||
|
}
|
||||||
|
)[:24]
|
||||||
|
return AdminRouteAcceptanceSummary(
|
||||||
|
generated_at=utc_now(),
|
||||||
|
provider_id=PROVIDER_ID,
|
||||||
|
control_plane_id=MCP_CONTROL_PLANE_ID,
|
||||||
|
source_hash=GENERATED_SOURCE_HASH,
|
||||||
|
total_cases=len(case_set),
|
||||||
|
ready_cases=sum(1 for case in case_set if case.status == AdminRouteAcceptanceStatus.READY),
|
||||||
|
partial_cases=sum(1 for case in case_set if case.status == AdminRouteAcceptanceStatus.PARTIAL),
|
||||||
|
blocked_cases=sum(1 for case in case_set if case.status == AdminRouteAcceptanceStatus.BLOCKED),
|
||||||
|
platforms_count=len({case.platform_id for case in case_set}),
|
||||||
|
profiles_count=len({case.profile_id for case in case_set}),
|
||||||
|
operations=_count_by(case_set, "operation"),
|
||||||
|
platforms=_count_by(case_set, "platform_id"),
|
||||||
|
blockers=blockers[:80],
|
||||||
|
evidence_id=evidence_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_admin_route_acceptance_report(
|
||||||
|
*,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
profile_id: str | None = None,
|
||||||
|
operation: str | None = None,
|
||||||
|
status: str | None = None,
|
||||||
|
limit: int = 80,
|
||||||
|
) -> AdminRouteAcceptanceReport:
|
||||||
|
"""Build a report with full summary and bounded returned cases."""
|
||||||
|
|
||||||
|
all_filtered = iter_admin_route_acceptance_cases(
|
||||||
|
platform_id=platform_id,
|
||||||
|
profile_id=profile_id,
|
||||||
|
operation=operation,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
returned = all_filtered[: max(0, int(limit))]
|
||||||
|
summary = summarize_admin_route_acceptance(all_filtered)
|
||||||
|
filter_payload = {
|
||||||
|
"platformId": platform_id or "",
|
||||||
|
"profileId": profile_id or "",
|
||||||
|
"operation": operation or "",
|
||||||
|
"status": status or "",
|
||||||
|
}
|
||||||
|
report_id = "mcp-admin-route-acceptance-" + stable_hash(
|
||||||
|
{
|
||||||
|
"summaryEvidence": summary.evidence_id,
|
||||||
|
"filters": filter_payload,
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
|
)[:16]
|
||||||
|
return AdminRouteAcceptanceReport(
|
||||||
|
report_id=report_id,
|
||||||
|
generated_at=utc_now(),
|
||||||
|
summary=summary,
|
||||||
|
returned_cases=tuple(returned),
|
||||||
|
sample_limit=max(0, int(limit)),
|
||||||
|
filters=filter_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def compact_acceptance_payload(report: AdminRouteAcceptanceReport) -> dict[str, Any]:
|
||||||
|
"""Return a compact payload for GPT/MCP/UI discovery."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reportId": report.report_id,
|
||||||
|
"generatedAt": report.generated_at,
|
||||||
|
"providerId": report.summary.provider_id,
|
||||||
|
"controlPlaneId": report.summary.control_plane_id,
|
||||||
|
"status": report.status.value,
|
||||||
|
"sourceHash": report.summary.source_hash,
|
||||||
|
"totalCases": report.summary.total_cases,
|
||||||
|
"readyCases": report.summary.ready_cases,
|
||||||
|
"partialCases": report.summary.partial_cases,
|
||||||
|
"blockedCases": report.summary.blocked_cases,
|
||||||
|
"readyRatio": report.summary.ready_ratio,
|
||||||
|
"operations": dict(report.summary.operations),
|
||||||
|
"platformsCount": report.summary.platforms_count,
|
||||||
|
"profilesCount": report.summary.profiles_count,
|
||||||
|
"evidenceId": report.summary.evidence_id,
|
||||||
|
"returnedCases": len(report.returned_cases),
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"routeId": case.route_id,
|
||||||
|
"platformId": case.platform_id,
|
||||||
|
"profileId": case.profile_id,
|
||||||
|
"operation": case.operation,
|
||||||
|
"toolId": case.tool_id,
|
||||||
|
"permissionScope": case.permission_scope,
|
||||||
|
"sameSource": case.same_source,
|
||||||
|
"status": case.status.value,
|
||||||
|
"evidenceId": case.evidence_id,
|
||||||
|
"sourcePayloadHash": case.source_payload_hash,
|
||||||
|
"sourceRecordsHash": case.source_records_hash,
|
||||||
|
"humanNextAction": case.pending_if_missing,
|
||||||
|
}
|
||||||
|
for case in report.returned_cases
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def acceptance_csv(report: AdminRouteAcceptanceReport) -> str:
|
||||||
|
"""Render acceptance cases as CSV."""
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
[
|
||||||
|
"route_id",
|
||||||
|
"platform_id",
|
||||||
|
"profile_id",
|
||||||
|
"operation",
|
||||||
|
"tool_id",
|
||||||
|
"permission_scope",
|
||||||
|
"same_source",
|
||||||
|
"status",
|
||||||
|
"approval_required",
|
||||||
|
"dry_run_supported",
|
||||||
|
"evidence_id",
|
||||||
|
"blockers",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
for case in report.returned_cases:
|
||||||
|
rows.append(
|
||||||
|
[
|
||||||
|
case.route_id,
|
||||||
|
case.platform_id,
|
||||||
|
case.profile_id,
|
||||||
|
case.operation,
|
||||||
|
case.tool_id,
|
||||||
|
case.permission_scope,
|
||||||
|
"yes" if case.same_source else "no",
|
||||||
|
case.status.value,
|
||||||
|
"yes" if case.approval_required else "no",
|
||||||
|
"yes" if case.dry_run_supported else "no",
|
||||||
|
case.evidence_id,
|
||||||
|
"; ".join(case.blocker_reasons),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
buffer = io.StringIO()
|
||||||
|
writer = csv.writer(buffer, lineterminator="\n")
|
||||||
|
writer.writerows(rows)
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def acceptance_markdown(report: AdminRouteAcceptanceReport) -> str:
|
||||||
|
"""Render a human-readable route acceptance report."""
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# MCP Admin Route Acceptance",
|
||||||
|
"",
|
||||||
|
f"- report_id: `{report.report_id}`",
|
||||||
|
f"- generated_at: `{report.generated_at}`",
|
||||||
|
f"- provider_id: `{report.summary.provider_id}`",
|
||||||
|
f"- control_plane_id: `{report.summary.control_plane_id}`",
|
||||||
|
f"- status: `{report.status.value}`",
|
||||||
|
f"- source_hash: `{report.summary.source_hash}`",
|
||||||
|
f"- total_cases: `{report.summary.total_cases}`",
|
||||||
|
f"- ready_cases: `{report.summary.ready_cases}`",
|
||||||
|
f"- partial_cases: `{report.summary.partial_cases}`",
|
||||||
|
f"- blocked_cases: `{report.summary.blocked_cases}`",
|
||||||
|
f"- ready_ratio: `{report.summary.ready_ratio:.4f}`",
|
||||||
|
f"- evidence_id: `{report.summary.evidence_id}`",
|
||||||
|
"",
|
||||||
|
"## Operacoes",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
if report.summary.operations:
|
||||||
|
lines.extend(f"- `{name}`: `{count}`" for name, count in sorted(report.summary.operations.items()))
|
||||||
|
else:
|
||||||
|
lines.append("- Nenhuma rota administrativa materializada.")
|
||||||
|
lines.extend(["", "## Plataformas", ""])
|
||||||
|
if report.summary.platforms:
|
||||||
|
lines.extend(f"- `{name}`: `{count}`" for name, count in sorted(report.summary.platforms.items()))
|
||||||
|
else:
|
||||||
|
lines.append("- Nenhuma plataforma materializada.")
|
||||||
|
lines.extend(["", "## Amostra", ""])
|
||||||
|
for case in report.returned_cases[:30]:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"### {case.route_id}",
|
||||||
|
"",
|
||||||
|
f"- route_key: `{case.route_key}`",
|
||||||
|
f"- tool_id: `{case.tool_id}`",
|
||||||
|
f"- source_tool_id: `{case.source_tool_id}`",
|
||||||
|
f"- permission_scope: `{case.permission_scope}`",
|
||||||
|
f"- same_source: `{case.same_source}`",
|
||||||
|
f"- status: `{case.status.value}`",
|
||||||
|
f"- evidence_id: `{case.evidence_id}`",
|
||||||
|
f"- source_payload_hash: `{case.source_payload_hash}`",
|
||||||
|
f"- source_records_hash: `{case.source_records_hash}`",
|
||||||
|
f"- next_action: {case.pending_if_missing}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
lines.extend(["## Bloqueios", ""])
|
||||||
|
if report.summary.blockers:
|
||||||
|
lines.extend(f"- `{redact_sensitive_text(item)}`" for item in report.summary.blockers[:80])
|
||||||
|
else:
|
||||||
|
lines.append("- Nenhum bloqueio de contrato MCP-only nas rotas administrativas geradas.")
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## Regra operacional",
|
||||||
|
"",
|
||||||
|
"- Toda consulta, diagnostico, acao, auditoria e explicacao interplataforma deve transitar pelo MCPs Internos.",
|
||||||
|
"- Cada caso preserva origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.",
|
||||||
|
"- Valores de segredo bruto nao fazem parte deste catalogo; evidencias usam hashes, traceId, auditId e refs opacas.",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return "\n".join(lines).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def acceptance_artifact_records(project_root: Path, central_platform_folder: Path | None = None) -> tuple[GeneratedFile, ...]:
|
||||||
|
"""Return semantic records for acceptance artifacts."""
|
||||||
|
|
||||||
|
records = [
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(project_root / "dados" / "mcp-admin-route-acceptance.json"),
|
||||||
|
description="Catalogo estruturado de aceitacao das rotas administrativas MCP-only.",
|
||||||
|
function="mcp admin route acceptance report",
|
||||||
|
file_type="json",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Materializado catalogo auditavel de rotas administrativas MCP-only.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(project_root / "dados" / "mcp-admin-route-acceptance-compacto.json"),
|
||||||
|
description="Resumo compacto das rotas administrativas para GPT/MCP/UI.",
|
||||||
|
function="mcp admin route acceptance compact payload",
|
||||||
|
file_type="json",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Criado payload compacto com contadores, hashes e amostra de rotas.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(project_root / "matrizes" / "mcp-admin-route-acceptance.csv"),
|
||||||
|
description="Matriz tabular de aceitacao das rotas administrativas MCP-only.",
|
||||||
|
function="mcp admin route acceptance matrix",
|
||||||
|
file_type="csv",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Criada matriz para auditoria de operacao, permissao e same-source.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(project_root / "ecossistema" / "MCP-ADMIN-ROUTE-ACCEPTANCE.md"),
|
||||||
|
description="Relatorio humano de aceitacao das rotas administrativas MCP-only.",
|
||||||
|
function="mcp admin route acceptance human report",
|
||||||
|
file_type="markdown",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Criado relatorio de homologacao local das rotas administrativas.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if central_platform_folder is not None:
|
||||||
|
records.extend(
|
||||||
|
[
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(central_platform_folder / "reports" / "EXECUTADO__mcp-admin-route-acceptance.md"),
|
||||||
|
description="Copia central do relatorio de aceitacao das rotas administrativas.",
|
||||||
|
function="mcp admin route acceptance central report",
|
||||||
|
file_type="markdown",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Registrada homologacao local das rotas administrativas na central.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(central_platform_folder / "indexes" / "mcp-admin-route-acceptance-index.md"),
|
||||||
|
description="Indice central do catalogo de rotas administrativas MCP-only.",
|
||||||
|
function="mcp admin route acceptance central index",
|
||||||
|
file_type="markdown",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Indexado status, contadores e evidencia da aceitacao MCP-only.",
|
||||||
|
relation_to_order="0037_EXECUTIVA__homologar-rotas-administrativas-mcp-no-gateway",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return tuple(records)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_text(path: Path, text: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def write_admin_route_acceptance_artifacts(
|
||||||
|
report: AdminRouteAcceptanceReport,
|
||||||
|
project_root: Path,
|
||||||
|
*,
|
||||||
|
central_platform_folder: Path | None = None,
|
||||||
|
) -> tuple[GeneratedFile, ...]:
|
||||||
|
"""Write project and optional central route acceptance artifacts."""
|
||||||
|
|
||||||
|
full_json = json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
compact_json = json.dumps(compact_acceptance_payload(report), ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
markdown = acceptance_markdown(report)
|
||||||
|
targets: list[tuple[Path, str]] = [
|
||||||
|
(project_root / "dados" / "mcp-admin-route-acceptance.json", full_json),
|
||||||
|
(project_root / "dados" / "mcp-admin-route-acceptance-compacto.json", compact_json),
|
||||||
|
(project_root / "matrizes" / "mcp-admin-route-acceptance.csv", acceptance_csv(report)),
|
||||||
|
(project_root / "ecossistema" / "MCP-ADMIN-ROUTE-ACCEPTANCE.md", markdown),
|
||||||
|
]
|
||||||
|
records = list(acceptance_artifact_records(project_root, central_platform_folder))
|
||||||
|
central_failures: list[dict[str, str]] = []
|
||||||
|
if central_platform_folder is not None:
|
||||||
|
targets.extend(
|
||||||
|
[
|
||||||
|
(central_platform_folder / "reports" / "EXECUTADO__mcp-admin-route-acceptance.md", markdown),
|
||||||
|
(central_platform_folder / "indexes" / "mcp-admin-route-acceptance-index.md", markdown),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for path, content in targets:
|
||||||
|
try:
|
||||||
|
_write_text(path, content)
|
||||||
|
except OSError as exc:
|
||||||
|
if central_platform_folder is not None and central_platform_folder in path.parents:
|
||||||
|
central_failures.append({"path": str(path), "error": f"{type(exc).__name__}: {exc}"})
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
if central_failures:
|
||||||
|
status_path = project_root / "dados" / "mcp-admin-route-acceptance-central-write-status.json"
|
||||||
|
_write_text(
|
||||||
|
status_path,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"generatedAt": utc_now(),
|
||||||
|
"ok": False,
|
||||||
|
"centralPlatformFolder": str(central_platform_folder),
|
||||||
|
"failures": central_failures,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
records.append(
|
||||||
|
GeneratedFile(
|
||||||
|
path=str(status_path),
|
||||||
|
description="Status de escrita central do catalogo de rotas administrativas.",
|
||||||
|
function="mcp admin route acceptance central write status",
|
||||||
|
file_type="json",
|
||||||
|
changed_by="mais_humana.mcp_admin_route_acceptance",
|
||||||
|
change_summary="Registrada falha de escrita central sem abortar artefatos do projeto real.",
|
||||||
|
relation_to_order="0040_EXECUTIVA__materializar-escrita-central-e-sql-semantico-sem-permissionerror",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(records)
|
||||||
|
|
||||||
|
|
||||||
|
def run_admin_route_acceptance(
|
||||||
|
*,
|
||||||
|
project_root: Path,
|
||||||
|
central_platform_folder: Path | None = None,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
profile_id: str | None = None,
|
||||||
|
operation: str | None = None,
|
||||||
|
status: str | None = None,
|
||||||
|
limit: int = 120,
|
||||||
|
) -> tuple[AdminRouteAcceptanceReport, tuple[GeneratedFile, ...]]:
|
||||||
|
"""Build and write route acceptance artifacts."""
|
||||||
|
|
||||||
|
report = build_admin_route_acceptance_report(
|
||||||
|
platform_id=platform_id,
|
||||||
|
profile_id=profile_id,
|
||||||
|
operation=operation,
|
||||||
|
status=status,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
records = write_admin_route_acceptance_artifacts(report, project_root, central_platform_folder=central_platform_folder)
|
||||||
|
report_with_files = AdminRouteAcceptanceReport(
|
||||||
|
report_id=report.report_id,
|
||||||
|
generated_at=report.generated_at,
|
||||||
|
summary=report.summary,
|
||||||
|
returned_cases=report.returned_cases,
|
||||||
|
sample_limit=report.sample_limit,
|
||||||
|
filters=report.filters,
|
||||||
|
generated_files=tuple(record.path for record in records),
|
||||||
|
)
|
||||||
|
return report_with_files, records
|
||||||
207
tools/generate_mcp_admin_route_acceptance.py
Normal file
207
tools/generate_mcp_admin_route_acceptance.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
"""Generate a compact Python acceptance catalog for MCP admin routes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC = ROOT / "src"
|
||||||
|
OUTPUT = SRC / "mais_humana" / "generated_mcp_admin_route_acceptance.py"
|
||||||
|
|
||||||
|
if str(SRC) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC))
|
||||||
|
|
||||||
|
from mais_humana.mcp_contract import McpContractKind, iter_contracts, stable_hash # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _operation(contract_id: str) -> str:
|
||||||
|
parts = contract_id.split(".")
|
||||||
|
if len(parts) >= 4 and parts[-1] == "administration-route":
|
||||||
|
return parts[-2]
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _permission(policy_tags: tuple[str, ...]) -> str:
|
||||||
|
for tag in reversed(policy_tags):
|
||||||
|
if tag.startswith("mcp.admin."):
|
||||||
|
return tag
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _case_status(row: dict[str, Any]) -> tuple[str, tuple[str, ...]]:
|
||||||
|
blockers: list[str] = []
|
||||||
|
required_transit = tuple(row["requiredTransitFields"])
|
||||||
|
required_payload = tuple(row["requiredPayloadFields"])
|
||||||
|
for field in (
|
||||||
|
"origin",
|
||||||
|
"destination",
|
||||||
|
"tool",
|
||||||
|
"payload",
|
||||||
|
"actor",
|
||||||
|
"permission",
|
||||||
|
"result",
|
||||||
|
"traceId",
|
||||||
|
"auditId",
|
||||||
|
"timestamp",
|
||||||
|
):
|
||||||
|
if field not in required_transit:
|
||||||
|
blockers.append(f"missing_transit:{field}")
|
||||||
|
for field in (
|
||||||
|
"adminRouteId",
|
||||||
|
"adminRouteKind",
|
||||||
|
"sourceEndpoint",
|
||||||
|
"sourceToolId",
|
||||||
|
"sourcePayloadHash",
|
||||||
|
"sourceRecordsHash",
|
||||||
|
"truthState",
|
||||||
|
"panelReady",
|
||||||
|
"gptExplainable",
|
||||||
|
"humanNextAction",
|
||||||
|
"permissionScope",
|
||||||
|
"mcpOnlyAdministration",
|
||||||
|
):
|
||||||
|
if field not in required_payload:
|
||||||
|
blockers.append(f"missing_payload:{field}")
|
||||||
|
if not row["sameSource"]:
|
||||||
|
blockers.append("same_source_false")
|
||||||
|
if not row["permissionScope"]:
|
||||||
|
blockers.append("permission_scope_missing")
|
||||||
|
if row["operation"] == "acao" and not (row["approvalRequired"] or row["dryRunSupported"]):
|
||||||
|
blockers.append("mutable_action_without_approval_or_dry_run")
|
||||||
|
if blockers:
|
||||||
|
return "blocked", tuple(blockers)
|
||||||
|
if row["maturityLevel"] < 8:
|
||||||
|
return "partial", ("maturity_below_8",)
|
||||||
|
return "ready", ()
|
||||||
|
|
||||||
|
|
||||||
|
def _case_from_contract(contract: Any) -> dict[str, Any]:
|
||||||
|
operation = _operation(contract.contract_id)
|
||||||
|
permission = _permission(tuple(contract.policy_tags))
|
||||||
|
required_payload = tuple(contract.required_payload_fields)
|
||||||
|
row = {
|
||||||
|
"routeId": contract.contract_id,
|
||||||
|
"platformId": contract.platform_id,
|
||||||
|
"profileId": contract.profile_id,
|
||||||
|
"operation": operation,
|
||||||
|
"toolId": contract.tool_id,
|
||||||
|
"sourceToolId": contract.source_tool_id,
|
||||||
|
"permissionScope": permission,
|
||||||
|
"sourceEndpoint": contract.source_endpoint,
|
||||||
|
"sourcePayloadHash": contract.source_payload_hash,
|
||||||
|
"sourceRecordsHash": contract.source_records_hash,
|
||||||
|
"evidenceId": f"evidence-{contract.source_records_hash[:24]}",
|
||||||
|
"truthState": contract.truth_state.value,
|
||||||
|
"panelReady": bool(contract.panel_ready),
|
||||||
|
"gptExplainable": bool(contract.gpt_explainable),
|
||||||
|
"sameSource": bool(contract.same_source_ready),
|
||||||
|
"approvalRequired": "approvalRequired" in required_payload,
|
||||||
|
"dryRunSupported": "dryRunSupported" in required_payload,
|
||||||
|
"requiredTransitFields": tuple(contract.required_transit_fields),
|
||||||
|
"requiredPayloadFields": required_payload,
|
||||||
|
"validationSteps": tuple(contract.validation_steps),
|
||||||
|
"redactionRequirements": tuple(contract.redaction_requirements),
|
||||||
|
"pendingIfMissing": contract.pending_if_missing,
|
||||||
|
"orderIds": tuple(contract.order_ids),
|
||||||
|
"policyTags": tuple(contract.policy_tags),
|
||||||
|
"maturityLevel": int(contract.maturity_level),
|
||||||
|
}
|
||||||
|
status, blockers = _case_status(row)
|
||||||
|
row["status"] = status
|
||||||
|
row["blockerReasons"] = blockers
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _format_value(value: Any) -> str:
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_case(row: dict[str, Any]) -> list[str]:
|
||||||
|
lines = [" {"]
|
||||||
|
for key in (
|
||||||
|
"routeId",
|
||||||
|
"platformId",
|
||||||
|
"profileId",
|
||||||
|
"operation",
|
||||||
|
"toolId",
|
||||||
|
"sourceToolId",
|
||||||
|
"permissionScope",
|
||||||
|
"sourceEndpoint",
|
||||||
|
"sourcePayloadHash",
|
||||||
|
"sourceRecordsHash",
|
||||||
|
"evidenceId",
|
||||||
|
"truthState",
|
||||||
|
"panelReady",
|
||||||
|
"gptExplainable",
|
||||||
|
"sameSource",
|
||||||
|
"approvalRequired",
|
||||||
|
"dryRunSupported",
|
||||||
|
"requiredTransitFields",
|
||||||
|
"requiredPayloadFields",
|
||||||
|
"validationSteps",
|
||||||
|
"redactionRequirements",
|
||||||
|
"pendingIfMissing",
|
||||||
|
"orderIds",
|
||||||
|
"policyTags",
|
||||||
|
"maturityLevel",
|
||||||
|
"status",
|
||||||
|
"blockerReasons",
|
||||||
|
):
|
||||||
|
lines.append(f" {key!r}: {_format_value(row[key])},")
|
||||||
|
lines.append(" },")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def build_cases() -> tuple[dict[str, Any], ...]:
|
||||||
|
contracts = [contract for contract in iter_contracts() if contract.kind == McpContractKind.ADMINISTRATION_ROUTE]
|
||||||
|
rows = [_case_from_contract(contract) for contract in sorted(contracts, key=lambda item: item.contract_id)]
|
||||||
|
return tuple(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def render_module(cases: tuple[dict[str, Any], ...]) -> str:
|
||||||
|
source_hash = stable_hash(cases)
|
||||||
|
lines = [
|
||||||
|
'"""Generated MCP administration route acceptance catalog.',
|
||||||
|
"",
|
||||||
|
"Do not edit by hand. Regenerate with:",
|
||||||
|
"python tools/generate_mcp_admin_route_acceptance.py",
|
||||||
|
'"""',
|
||||||
|
"",
|
||||||
|
"from __future__ import annotations",
|
||||||
|
"",
|
||||||
|
f"SOURCE_HASH = {source_hash!r}",
|
||||||
|
f"CASES_COUNT = {len(cases)!r}",
|
||||||
|
"",
|
||||||
|
"ADMIN_ROUTE_ACCEPTANCE_CASES = (",
|
||||||
|
]
|
||||||
|
for row in cases:
|
||||||
|
lines.extend(_render_case(row))
|
||||||
|
lines.extend([")", ""])
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
cases = build_cases()
|
||||||
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.write_text(render_module(cases), encoding="utf-8")
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"output": str(OUTPUT),
|
||||||
|
"cases": len(cases),
|
||||||
|
"sourceHash": stable_hash(cases),
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user