fix(rc06): bind prompt writes to live authority
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
|||||||
|
"""bind RC06 AI transactions to the authority expiry
|
||||||
|
|
||||||
|
Revision ID: 20261007_0032
|
||||||
|
Revises: 20261006_0031
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "20261007_0032"
|
||||||
|
down_revision = "20261006_0031"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _lock_and_assert_empty(action: str) -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
bind.execute(sa.text(
|
||||||
|
"LOCK TABLE rc06_prompt_publications IN ACCESS EXCLUSIVE MODE"
|
||||||
|
))
|
||||||
|
count = bind.execute(sa.text(
|
||||||
|
"SELECT COUNT(*) FROM rc06_prompt_publications"
|
||||||
|
)).scalar_one()
|
||||||
|
if count:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"RC06 Prompt migration 0032 {action} refuses nonempty publication "
|
||||||
|
"journals because authority expiry cannot be inferred or discarded safely; "
|
||||||
|
"back up and explicitly retire those rows first."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
_lock_and_assert_empty("upgrade")
|
||||||
|
with op.batch_alter_table("rc06_prompt_publications") as batch:
|
||||||
|
batch.add_column(sa.Column(
|
||||||
|
"authority_expires_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
))
|
||||||
|
batch.create_check_constraint(
|
||||||
|
"ck_rc06_prompt_publications_authority_window",
|
||||||
|
"authority_expires_at > created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
_lock_and_assert_empty("downgrade")
|
||||||
|
with op.batch_alter_table("rc06_prompt_publications") as batch:
|
||||||
|
batch.drop_constraint(
|
||||||
|
"ck_rc06_prompt_publications_authority_window",
|
||||||
|
type_="check",
|
||||||
|
)
|
||||||
|
batch.drop_column("authority_expires_at")
|
||||||
@@ -194,6 +194,10 @@ class Rc06PromptPublicationModel(Base):
|
|||||||
"compensated_at IS NOT NULL)",
|
"compensated_at IS NOT NULL)",
|
||||||
name="ck_rc06_prompt_publications_compensation_evidence",
|
name="ck_rc06_prompt_publications_compensation_evidence",
|
||||||
),
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"authority_expires_at > created_at",
|
||||||
|
name="ck_rc06_prompt_publications_authority_window",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
transaction_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
@@ -206,6 +210,10 @@ class Rc06PromptPublicationModel(Base):
|
|||||||
batch_manifest_raw_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
batch_manifest_raw_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
batch_manifest_payload_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
batch_manifest_payload_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
approval_receipt_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
approval_receipt_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
authority_expires_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
state: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
state: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
||||||
request_json: Mapped[str] = mapped_column(Text, nullable=False)
|
request_json: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
before_prompts_json: Mapped[str] = mapped_column(Text, nullable=False)
|
before_prompts_json: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
|||||||
+57
-20
@@ -15,13 +15,12 @@ import json
|
|||||||
import re
|
import re
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import FastAPI, Header, HTTPException
|
from fastapi import FastAPI, Header, HTTPException
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, text
|
||||||
|
|
||||||
from yuqei_sdk.prompt_source_baseline import prompt_source_baseline
|
from yuqei_sdk.prompt_source_baseline import prompt_source_baseline
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ from .models import (
|
|||||||
from .packaged_prompt_assets_import import DEFAULT_SOURCE_FILE, DEFAULT_VERSION, load_packaged_prompt_assets
|
from .packaged_prompt_assets_import import DEFAULT_SOURCE_FILE, DEFAULT_VERSION, load_packaged_prompt_assets
|
||||||
from .persistence.prompts import encode_model_json, prompt_template_from_model, prompt_write_transaction
|
from .persistence.prompts import encode_model_json, prompt_template_from_model, prompt_write_transaction
|
||||||
from .prompt_compiler import prompt_content_hash, validate_prompt_content
|
from .prompt_compiler import prompt_content_hash, validate_prompt_content
|
||||||
from .review_scene_prompts import review_modules, scene_review_template
|
from .review_scene_prompts import review_modules, scene_review_template_for_route
|
||||||
from .service_auth import is_valid_service_key
|
from .service_auth import is_valid_service_key
|
||||||
|
|
||||||
|
|
||||||
@@ -123,10 +122,18 @@ class PromptPrepareRequest(BaseModel):
|
|||||||
batch_manifest_raw_sha256: str = Field(pattern=HEX64)
|
batch_manifest_raw_sha256: str = Field(pattern=HEX64)
|
||||||
batch_manifest_payload_sha256: str = Field(pattern=HEX64)
|
batch_manifest_payload_sha256: str = Field(pattern=HEX64)
|
||||||
approval_receipt_sha256: str = Field(pattern=HEX64)
|
approval_receipt_sha256: str = Field(pattern=HEX64)
|
||||||
|
authority_expires_at: datetime
|
||||||
compensation_capability_sha256: str = Field(pattern=HEX64)
|
compensation_capability_sha256: str = Field(pattern=HEX64)
|
||||||
modules: list[PromptModuleInput]
|
modules: list[PromptModuleInput]
|
||||||
runtime_identities: list[RuntimeIdentityInput]
|
runtime_identities: list[RuntimeIdentityInput]
|
||||||
|
|
||||||
|
@field_validator("authority_expires_at")
|
||||||
|
@classmethod
|
||||||
|
def require_aware_authority_expiry(cls, value: datetime) -> datetime:
|
||||||
|
if value.tzinfo is None or value.utcoffset() is None:
|
||||||
|
raise ValueError("authority_expires_at must include a timezone")
|
||||||
|
return value.astimezone(UTC)
|
||||||
|
|
||||||
|
|
||||||
class PromptTransactionStatus(BaseModel):
|
class PromptTransactionStatus(BaseModel):
|
||||||
transaction_id: str
|
transaction_id: str
|
||||||
@@ -137,6 +144,7 @@ class PromptTransactionStatus(BaseModel):
|
|||||||
batch_manifest_raw_sha256: str
|
batch_manifest_raw_sha256: str
|
||||||
batch_manifest_payload_sha256: str
|
batch_manifest_payload_sha256: str
|
||||||
approval_receipt_sha256: str
|
approval_receipt_sha256: str
|
||||||
|
authority_expires_at: datetime
|
||||||
item_results: list[dict[str, Any]] = Field(default_factory=list)
|
item_results: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
runtime_snapshots: list[dict[str, Any]] = Field(default_factory=list)
|
runtime_snapshots: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
write_count: int = 0
|
write_count: int = 0
|
||||||
@@ -203,6 +211,35 @@ def _as_utc(value: datetime | None) -> datetime | None:
|
|||||||
return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
|
return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def _database_wall_clock(session: Any) -> datetime:
|
||||||
|
"""Read a participant-local database clock, never the application clock.
|
||||||
|
|
||||||
|
PostgreSQL's CURRENT_TIMESTAMP is fixed at transaction start, so RC06 uses
|
||||||
|
clock_timestamp() there. SQLite is only supported for local tests and
|
||||||
|
embedded development; each CURRENT_TIMESTAMP statement observes its live
|
||||||
|
database clock.
|
||||||
|
"""
|
||||||
|
statement = (
|
||||||
|
"SELECT clock_timestamp()"
|
||||||
|
if session.get_bind().dialect.name == "postgresql"
|
||||||
|
else "SELECT CURRENT_TIMESTAMP"
|
||||||
|
)
|
||||||
|
value = session.execute(text(statement)).scalar_one()
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = datetime.fromisoformat(value.replace(" ", "T"))
|
||||||
|
if not isinstance(value, datetime):
|
||||||
|
raise Rc06PublicationError("RC06 AI database clock returned an invalid value.")
|
||||||
|
normalized = _as_utc(value)
|
||||||
|
if normalized is None: # pragma: no cover - guarded by the type check above
|
||||||
|
raise Rc06PublicationError("RC06 AI database clock returned no value.")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_authority_active(session: Any, authority_expires_at: datetime) -> None:
|
||||||
|
if _database_wall_clock(session) >= authority_expires_at.astimezone(UTC):
|
||||||
|
raise Rc06Conflict("RC06 publication authority expired before AI persistence.")
|
||||||
|
|
||||||
|
|
||||||
def _prompt_rows(session: Any, names: list[str]) -> list[dict[str, Any]]:
|
def _prompt_rows(session: Any, names: list[str]) -> list[dict[str, Any]]:
|
||||||
rows = session.scalars(
|
rows = session.scalars(
|
||||||
select(PromptTemplateModel).where(PromptTemplateModel.name.in_(names))
|
select(PromptTemplateModel).where(PromptTemplateModel.name.in_(names))
|
||||||
@@ -226,6 +263,7 @@ def _frozen_request(transaction: Rc06PromptPublicationModel) -> PromptPrepareReq
|
|||||||
or payload.batch_manifest_raw_sha256 != transaction.batch_manifest_raw_sha256
|
or payload.batch_manifest_raw_sha256 != transaction.batch_manifest_raw_sha256
|
||||||
or payload.batch_manifest_payload_sha256 != transaction.batch_manifest_payload_sha256
|
or payload.batch_manifest_payload_sha256 != transaction.batch_manifest_payload_sha256
|
||||||
or payload.approval_receipt_sha256 != transaction.approval_receipt_sha256
|
or payload.approval_receipt_sha256 != transaction.approval_receipt_sha256
|
||||||
|
or payload.authority_expires_at != _as_utc(transaction.authority_expires_at)
|
||||||
or payload.compensation_capability_sha256 != transaction.compensation_capability_sha256
|
or payload.compensation_capability_sha256 != transaction.compensation_capability_sha256
|
||||||
):
|
):
|
||||||
raise Rc06PublicationError("Frozen RC06 Prompt deployment identity drifted.")
|
raise Rc06PublicationError("Frozen RC06 Prompt deployment identity drifted.")
|
||||||
@@ -265,22 +303,12 @@ def _source_inventory() -> tuple[list[dict[str, Any]], list[dict[str, Any]], dic
|
|||||||
modules.sort(key=lambda item: (item["category"], item["key"]))
|
modules.sort(key=lambda item: (item["category"], item["key"]))
|
||||||
|
|
||||||
runtime_contents: dict[str, str] = {DRAFT_PROMPT_NAMES[0]: DRAFT_PROMPT_CONTENT}
|
runtime_contents: dict[str, str] = {DRAFT_PROMPT_NAMES[0]: DRAFT_PROMPT_CONTENT}
|
||||||
route_inputs = {
|
for route in reviews:
|
||||||
"contract_review_trade_procurement": ("purchase", "scene_evaluation_v3"),
|
template = scene_review_template_for_route(
|
||||||
"contract_review_services": ("service", "scene_evaluation_v3"),
|
route,
|
||||||
"contract_review_transport_warehousing": ("transport", "scene_evaluation_v4"),
|
review_profile="scene_evaluation_v4",
|
||||||
"contract_review_manufacturing": ("manufacturing", "scene_evaluation_v4"),
|
scene_template_id="rc06-runtime-snapshot",
|
||||||
"contract_review_construction": ("construction", "scene_evaluation_v3"),
|
)
|
||||||
"contract_review_marketing": ("marketing", "scene_evaluation_v4"),
|
|
||||||
"contract_review_consumer_services": ("consumer_service", "scene_evaluation_v3"),
|
|
||||||
"contract_review_notices_amendments": ("amendment", "scene_evaluation_v4"),
|
|
||||||
}
|
|
||||||
for route, (contract_type, profile) in route_inputs.items():
|
|
||||||
template = scene_review_template(SimpleNamespace(
|
|
||||||
contract_type=contract_type,
|
|
||||||
scene_template_id=None,
|
|
||||||
review_profile=profile,
|
|
||||||
))
|
|
||||||
if template.name != route:
|
if template.name != route:
|
||||||
raise Rc06PublicationError(f"Runtime route drift: {route}")
|
raise Rc06PublicationError(f"Runtime route drift: {route}")
|
||||||
runtime_contents[route] = template.content
|
runtime_contents[route] = template.content
|
||||||
@@ -372,6 +400,8 @@ class Rc06PromptPublicationService:
|
|||||||
if session.get(Rc06PromptPublicationModel, payload.transaction_id) is not None:
|
if session.get(Rc06PromptPublicationModel, payload.transaction_id) is not None:
|
||||||
raise Rc06Conflict("Transaction ID was already used with a different idempotency key.")
|
raise Rc06Conflict("Transaction ID was already used with a different idempotency key.")
|
||||||
|
|
||||||
|
_assert_authority_active(session, payload.authority_expires_at)
|
||||||
|
|
||||||
rows = session.scalars(select(PromptTemplateModel).where(PromptTemplateModel.name.in_(names))).all()
|
rows = session.scalars(select(PromptTemplateModel).where(PromptTemplateModel.name.in_(names))).all()
|
||||||
before = sorted((_prompt_row(row) for row in rows), key=lambda item: (item["name"], item["id"]))
|
before = sorted((_prompt_row(row) for row in rows), key=lambda item: (item["name"], item["id"]))
|
||||||
self._assert_domain_dependencies(session, expected_runtime)
|
self._assert_domain_dependencies(session, expected_runtime)
|
||||||
@@ -387,6 +417,7 @@ class Rc06PromptPublicationService:
|
|||||||
batch_manifest_raw_sha256=payload.batch_manifest_raw_sha256,
|
batch_manifest_raw_sha256=payload.batch_manifest_raw_sha256,
|
||||||
batch_manifest_payload_sha256=payload.batch_manifest_payload_sha256,
|
batch_manifest_payload_sha256=payload.batch_manifest_payload_sha256,
|
||||||
approval_receipt_sha256=payload.approval_receipt_sha256,
|
approval_receipt_sha256=payload.approval_receipt_sha256,
|
||||||
|
authority_expires_at=payload.authority_expires_at,
|
||||||
state="prepared",
|
state="prepared",
|
||||||
request_json=_encode(request_payload),
|
request_json=_encode(request_payload),
|
||||||
before_prompts_json=_encode(before),
|
before_prompts_json=_encode(before),
|
||||||
@@ -423,6 +454,7 @@ class Rc06PromptPublicationService:
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
))
|
))
|
||||||
session.flush()
|
session.flush()
|
||||||
|
_assert_authority_active(session, payload.authority_expires_at)
|
||||||
return self._status_row(transaction)
|
return self._status_row(transaction)
|
||||||
|
|
||||||
def commit(self, transaction_id: str) -> PromptTransactionStatus:
|
def commit(self, transaction_id: str) -> PromptTransactionStatus:
|
||||||
@@ -452,6 +484,7 @@ class Rc06PromptPublicationService:
|
|||||||
if transaction.state != "prepared":
|
if transaction.state != "prepared":
|
||||||
raise Rc06Conflict(f"Prompt transaction cannot commit from {transaction.state}.")
|
raise Rc06Conflict(f"Prompt transaction cannot commit from {transaction.state}.")
|
||||||
self._assert_current_deployment(frozen)
|
self._assert_current_deployment(frozen)
|
||||||
|
_assert_authority_active(session, frozen.authority_expires_at)
|
||||||
before = _decode(transaction.before_prompts_json, [])
|
before = _decode(transaction.before_prompts_json, [])
|
||||||
current = _prompt_rows(session, names)
|
current = _prompt_rows(session, names)
|
||||||
if current != before:
|
if current != before:
|
||||||
@@ -482,6 +515,7 @@ class Rc06PromptPublicationService:
|
|||||||
transaction.committed_at = now
|
transaction.committed_at = now
|
||||||
transaction.updated_at = now
|
transaction.updated_at = now
|
||||||
session.flush()
|
session.flush()
|
||||||
|
_assert_authority_active(session, frozen.authority_expires_at)
|
||||||
return self._status_row(transaction)
|
return self._status_row(transaction)
|
||||||
except (KeyError, Rc06PublicationError, Rc06Conflict):
|
except (KeyError, Rc06PublicationError, Rc06Conflict):
|
||||||
raise
|
raise
|
||||||
@@ -512,12 +546,14 @@ class Rc06PromptPublicationService:
|
|||||||
return self._status_row(transaction)
|
return self._status_row(transaction)
|
||||||
if transaction.state != "committed":
|
if transaction.state != "committed":
|
||||||
raise Rc06Conflict(f"Prompt transaction cannot seal from {transaction.state}.")
|
raise Rc06Conflict(f"Prompt transaction cannot seal from {transaction.state}.")
|
||||||
|
_assert_authority_active(session, frozen.authority_expires_at)
|
||||||
self._verify_committed(session, transaction, expected_runtime, expected_modules)
|
self._verify_committed(session, transaction, expected_runtime, expected_modules)
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
transaction.state = "sealed"
|
transaction.state = "sealed"
|
||||||
transaction.sealed_at = now
|
transaction.sealed_at = now
|
||||||
transaction.updated_at = now
|
transaction.updated_at = now
|
||||||
session.flush()
|
session.flush()
|
||||||
|
_assert_authority_active(session, frozen.authority_expires_at)
|
||||||
return self._status_row(transaction)
|
return self._status_row(transaction)
|
||||||
|
|
||||||
def compensate(self, transaction_id: str, capability: str) -> PromptTransactionStatus:
|
def compensate(self, transaction_id: str, capability: str) -> PromptTransactionStatus:
|
||||||
@@ -1472,6 +1508,7 @@ class Rc06PromptPublicationService:
|
|||||||
batch_manifest_raw_sha256=row.batch_manifest_raw_sha256,
|
batch_manifest_raw_sha256=row.batch_manifest_raw_sha256,
|
||||||
batch_manifest_payload_sha256=row.batch_manifest_payload_sha256,
|
batch_manifest_payload_sha256=row.batch_manifest_payload_sha256,
|
||||||
approval_receipt_sha256=row.approval_receipt_sha256,
|
approval_receipt_sha256=row.approval_receipt_sha256,
|
||||||
|
authority_expires_at=_as_utc(row.authority_expires_at),
|
||||||
item_results=results,
|
item_results=results,
|
||||||
runtime_snapshots=snapshots,
|
runtime_snapshots=snapshots,
|
||||||
write_count=len(results),
|
write_count=len(results),
|
||||||
|
|||||||
+20
-4
@@ -108,10 +108,16 @@ def _module_block(item):
|
|||||||
return f'<review-module id="{item["key"]}" version="{item["version"]}" sha256="{item["content_sha256"]}">\n{item["text"]}\n</review-module>'
|
return f'<review-module id="{item["key"]}" version="{item["version"]}" sha256="{item["content_sha256"]}">\n{item["text"]}\n</review-module>'
|
||||||
|
|
||||||
|
|
||||||
def scene_review_template(request) -> PromptTemplate:
|
def scene_review_template_for_route(
|
||||||
from .prompts import contract_review_prompt_candidates
|
route: str,
|
||||||
route = contract_review_prompt_candidates(request.contract_type, request.scene_template_id)[0]
|
*,
|
||||||
|
review_profile: str,
|
||||||
|
scene_template_id: str | None = None,
|
||||||
|
) -> PromptTemplate:
|
||||||
|
"""Render one explicit review route without mutating global routing state."""
|
||||||
modules = review_modules()
|
modules = review_modules()
|
||||||
|
if route not in modules and route not in REVIEW_FOCUS:
|
||||||
|
raise PromptRenderError(f"未知专项审查路由:{route}")
|
||||||
selected = [modules[route]] if route in modules else []
|
selected = [modules[route]] if route in modules else []
|
||||||
focus = REVIEW_FOCUS.get(route, REVIEW_FOCUS["contract_review_default"])
|
focus = REVIEW_FOCUS.get(route, REVIEW_FOCUS["contract_review_default"])
|
||||||
# One (name, version) must have one content identity in the shared registry.
|
# One (name, version) must have one content identity in the shared registry.
|
||||||
@@ -123,7 +129,7 @@ def scene_review_template(request) -> PromptTemplate:
|
|||||||
+ "其他场景不据此推定劳动关系或套用劳动专项结论。")
|
+ "其他场景不据此推定劳动关系或套用劳动专项结论。")
|
||||||
# Lightweight manifest builders created before v3 only carry the route
|
# Lightweight manifest builders created before v3 only carry the route
|
||||||
# fields; absence of an explicit profile must retain the historical v2 asset.
|
# fields; absence of an explicit profile must retain the historical v2 asset.
|
||||||
request_profile = getattr(request, "review_profile", "scene_evaluation")
|
request_profile = review_profile
|
||||||
uses_compact_context = request_profile == "scene_evaluation_v4"
|
uses_compact_context = request_profile == "scene_evaluation_v4"
|
||||||
is_v3 = request_profile in {"scene_evaluation_v3", "scene_evaluation_v4"}
|
is_v3 = request_profile in {"scene_evaluation_v3", "scene_evaluation_v4"}
|
||||||
output_version = REVIEW_CONTRACT_V3_VERSION if is_v3 else REVIEW_CONTRACT_VERSION
|
output_version = REVIEW_CONTRACT_V3_VERSION if is_v3 else REVIEW_CONTRACT_VERSION
|
||||||
@@ -160,6 +166,16 @@ def scene_review_template(request) -> PromptTemplate:
|
|||||||
content=content, status="builtin_evaluation", updated_at=datetime(2026, 8, 28, tzinfo=UTC))
|
content=content, status="builtin_evaluation", updated_at=datetime(2026, 8, 28, tzinfo=UTC))
|
||||||
|
|
||||||
|
|
||||||
|
def scene_review_template(request) -> PromptTemplate:
|
||||||
|
from .prompts import contract_review_prompt_candidates
|
||||||
|
route = contract_review_prompt_candidates(request.contract_type, request.scene_template_id)[0]
|
||||||
|
return scene_review_template_for_route(
|
||||||
|
route,
|
||||||
|
review_profile=getattr(request, "review_profile", "scene_evaluation"),
|
||||||
|
scene_template_id=getattr(request, "scene_template_id", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def capture_review_prompt_snapshot(store, request):
|
def capture_review_prompt_snapshot(store, request):
|
||||||
from .prompts import contract_review_prompt_candidates
|
from .prompts import contract_review_prompt_candidates
|
||||||
if request.review_profile not in {"scene_evaluation", "scene_evaluation_v3", "scene_evaluation_v4"}:
|
if request.review_profile not in {"scene_evaluation", "scene_evaluation_v3", "scene_evaluation_v4"}:
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ RC06_COLUMNS_BY_TABLE = {
|
|||||||
"batch_manifest_raw_sha256",
|
"batch_manifest_raw_sha256",
|
||||||
"batch_manifest_payload_sha256",
|
"batch_manifest_payload_sha256",
|
||||||
"approval_receipt_sha256",
|
"approval_receipt_sha256",
|
||||||
|
"authority_expires_at",
|
||||||
"request_json",
|
"request_json",
|
||||||
"before_prompts_json",
|
"before_prompts_json",
|
||||||
"after_prompts_json",
|
"after_prompts_json",
|
||||||
@@ -171,6 +172,7 @@ RC06_CONSTRAINTS_BY_TABLE = {
|
|||||||
"ux_rc06_prompt_publications_idempotency",
|
"ux_rc06_prompt_publications_idempotency",
|
||||||
"ck_rc06_prompt_publications_state",
|
"ck_rc06_prompt_publications_state",
|
||||||
"ck_rc06_prompt_publications_compensation_evidence",
|
"ck_rc06_prompt_publications_compensation_evidence",
|
||||||
|
"ck_rc06_prompt_publications_authority_window",
|
||||||
),
|
),
|
||||||
"rc06_prompt_module_publications": (
|
"rc06_prompt_module_publications": (
|
||||||
"ux_rc06_prompt_module_publications_transaction_key",
|
"ux_rc06_prompt_module_publications_transaction_key",
|
||||||
|
|||||||
@@ -139,10 +139,14 @@ def test_rc06_schema_contract_includes_durable_compensation_evidence() -> None:
|
|||||||
"removed_item_count",
|
"removed_item_count",
|
||||||
"unchanged_item_count",
|
"unchanged_item_count",
|
||||||
"compensated_at",
|
"compensated_at",
|
||||||
|
"authority_expires_at",
|
||||||
} <= set(RC06_COLUMNS_BY_TABLE["rc06_prompt_publications"])
|
} <= set(RC06_COLUMNS_BY_TABLE["rc06_prompt_publications"])
|
||||||
assert "ck_rc06_prompt_publications_compensation_evidence" in set(
|
assert "ck_rc06_prompt_publications_compensation_evidence" in set(
|
||||||
RC06_CONSTRAINTS_BY_TABLE["rc06_prompt_publications"]
|
RC06_CONSTRAINTS_BY_TABLE["rc06_prompt_publications"]
|
||||||
)
|
)
|
||||||
|
assert "ck_rc06_prompt_publications_authority_window" in set(
|
||||||
|
RC06_CONSTRAINTS_BY_TABLE["rc06_prompt_publications"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_schema_check_errors_for_missing_core_tables(tmp_path) -> None:
|
def test_runtime_schema_check_errors_for_missing_core_tables(tmp_path) -> None:
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ def _hardened_head(connection) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _compensation_head(connection) -> None:
|
||||||
|
_hardened_head(connection)
|
||||||
|
_run(
|
||||||
|
connection,
|
||||||
|
_load("20261006_0031_add_rc06_compensation_evidence.py"),
|
||||||
|
"upgrade",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_old_head_upgrades_and_round_trips_without_rewriting_0029(tmp_path):
|
def test_empty_old_head_upgrades_and_round_trips_without_rewriting_0029(tmp_path):
|
||||||
engine = sa.create_engine(f"sqlite+pysqlite:///{(tmp_path / 'ai-migration.sqlite3').as_posix()}")
|
engine = sa.create_engine(f"sqlite+pysqlite:///{(tmp_path / 'ai-migration.sqlite3').as_posix()}")
|
||||||
migration = _load("20261006_0030_harden_rc06_prompt_publication.py")
|
migration = _load("20261006_0030_harden_rc06_prompt_publication.py")
|
||||||
@@ -294,6 +303,72 @@ def test_compensation_evidence_migration_rejects_active_committed_downgrade(tmp_
|
|||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def test_authority_expiry_migration_requires_empty_journal_and_round_trips(tmp_path):
|
||||||
|
engine = sa.create_engine(
|
||||||
|
f"sqlite+pysqlite:///{(tmp_path / 'ai-authority-expiry.sqlite3').as_posix()}"
|
||||||
|
)
|
||||||
|
migration = _load("20261007_0032_bind_rc06_authority_expiry.py")
|
||||||
|
with engine.begin() as connection:
|
||||||
|
_compensation_head(connection)
|
||||||
|
_run(connection, migration, "upgrade")
|
||||||
|
inspector = sa.inspect(connection)
|
||||||
|
assert "authority_expires_at" in {
|
||||||
|
item["name"]
|
||||||
|
for item in inspector.get_columns("rc06_prompt_publications")
|
||||||
|
}
|
||||||
|
assert "ck_rc06_prompt_publications_authority_window" in {
|
||||||
|
item["name"]
|
||||||
|
for item in inspector.get_check_constraints("rc06_prompt_publications")
|
||||||
|
}
|
||||||
|
_run(connection, migration, "downgrade")
|
||||||
|
assert "authority_expires_at" not in {
|
||||||
|
item["name"]
|
||||||
|
for item in sa.inspect(connection).get_columns("rc06_prompt_publications")
|
||||||
|
}
|
||||||
|
_run(connection, migration, "upgrade")
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def test_authority_expiry_migration_rejects_nonempty_journal_before_schema_change(tmp_path):
|
||||||
|
engine = sa.create_engine(
|
||||||
|
f"sqlite+pysqlite:///{(tmp_path / 'ai-authority-nonempty.sqlite3').as_posix()}"
|
||||||
|
)
|
||||||
|
migration = _load("20261007_0032_bind_rc06_authority_expiry.py")
|
||||||
|
with engine.begin() as connection:
|
||||||
|
_compensation_head(connection)
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
table = sa.Table(
|
||||||
|
"rc06_prompt_publications",
|
||||||
|
sa.MetaData(),
|
||||||
|
autoload_with=connection,
|
||||||
|
)
|
||||||
|
connection.execute(table.insert().values(
|
||||||
|
transaction_id="authority-migration-transaction",
|
||||||
|
idempotency_key="authority-migration-idempotency",
|
||||||
|
request_sha256="1" * 64,
|
||||||
|
candidate_commit="2" * 40,
|
||||||
|
release_image_digest="sha256:" + "3" * 64,
|
||||||
|
compensation_capability_sha256="4" * 64,
|
||||||
|
candidate_tag="authority-migration-candidate",
|
||||||
|
batch_manifest_raw_sha256="5" * 64,
|
||||||
|
batch_manifest_payload_sha256="6" * 64,
|
||||||
|
approval_receipt_sha256="7" * 64,
|
||||||
|
state="prepared",
|
||||||
|
request_json="{}",
|
||||||
|
before_prompts_json="[]",
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
))
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="refuses nonempty publication journals"):
|
||||||
|
_run(connection, migration, "upgrade")
|
||||||
|
assert "authority_expires_at" not in {
|
||||||
|
item["name"]
|
||||||
|
for item in sa.inspect(connection).get_columns("rc06_prompt_publications")
|
||||||
|
}
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
def test_fresh_alembic_upgrade_then_old_head_round_trip(tmp_path, monkeypatch):
|
def test_fresh_alembic_upgrade_then_old_head_round_trip(tmp_path, monkeypatch):
|
||||||
database_path = tmp_path / "ai-fresh-full-chain.sqlite3"
|
database_path = tmp_path / "ai-fresh-full-chain.sqlite3"
|
||||||
monkeypatch.setenv(
|
monkeypatch.setenv(
|
||||||
@@ -308,7 +383,7 @@ def test_fresh_alembic_upgrade_then_old_head_round_trip(tmp_path, monkeypatch):
|
|||||||
with engine.connect() as connection:
|
with engine.connect() as connection:
|
||||||
assert connection.exec_driver_sql(
|
assert connection.exec_driver_sql(
|
||||||
"SELECT version_num FROM ai_platform_alembic_version"
|
"SELECT version_num FROM ai_platform_alembic_version"
|
||||||
).scalar_one() == "20261006_0031"
|
).scalar_one() == "20261007_0032"
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|
||||||
command.downgrade(config, "20261006_0029")
|
command.downgrade(config, "20261006_0029")
|
||||||
@@ -318,6 +393,7 @@ def test_fresh_alembic_upgrade_then_old_head_round_trip(tmp_path, monkeypatch):
|
|||||||
assert {
|
assert {
|
||||||
"sealed_at",
|
"sealed_at",
|
||||||
"compensation_evidence_sha256",
|
"compensation_evidence_sha256",
|
||||||
|
"authority_expires_at",
|
||||||
} <= {
|
} <= {
|
||||||
item["name"] for item in sa.inspect(connection).get_columns("rc06_prompt_publications")
|
item["name"] for item in sa.inspect(connection).get_columns("rc06_prompt_publications")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
@@ -11,6 +11,7 @@ from sqlalchemy.dialects import postgresql, sqlite
|
|||||||
from sqlalchemy.schema import CreateTable
|
from sqlalchemy.schema import CreateTable
|
||||||
|
|
||||||
from yuqei_sdk.prompt_source_baseline import DOMAIN_REVIEW_RUNTIME_IDENTITIES
|
from yuqei_sdk.prompt_source_baseline import DOMAIN_REVIEW_RUNTIME_IDENTITIES
|
||||||
|
import yuqei_ai_platform_api.rc06_publication as rc06_publication
|
||||||
from yuqei_ai_platform_api.config import AiPlatformSettings
|
from yuqei_ai_platform_api.config import AiPlatformSettings
|
||||||
from yuqei_ai_platform_api.main import create_app
|
from yuqei_ai_platform_api.main import create_app
|
||||||
from yuqei_ai_platform_api.models import (
|
from yuqei_ai_platform_api.models import (
|
||||||
@@ -85,6 +86,7 @@ def _request(transaction_id: str = "rc06-ai-transaction-0001") -> PromptPrepareR
|
|||||||
batch_manifest_raw_sha256="d" * 64,
|
batch_manifest_raw_sha256="d" * 64,
|
||||||
batch_manifest_payload_sha256="b" * 64,
|
batch_manifest_payload_sha256="b" * 64,
|
||||||
approval_receipt_sha256="c" * 64,
|
approval_receipt_sha256="c" * 64,
|
||||||
|
authority_expires_at=datetime.now(UTC) + timedelta(days=1),
|
||||||
compensation_capability_sha256=hashlib.sha256(
|
compensation_capability_sha256=hashlib.sha256(
|
||||||
COMPENSATION_CAPABILITY.encode("utf-8")
|
COMPENSATION_CAPABILITY.encode("utf-8")
|
||||||
).hexdigest(),
|
).hexdigest(),
|
||||||
@@ -93,6 +95,65 @@ def _request(transaction_id: str = "rc06-ai-transaction-0001") -> PromptPrepareR
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_rejects_expired_authority_without_journal_or_prompt_writes(tmp_path):
|
||||||
|
repository = _repository(tmp_path)
|
||||||
|
try:
|
||||||
|
_seed_domain_dependencies(repository)
|
||||||
|
service = _service(repository)
|
||||||
|
request = PromptPrepareRequest.model_validate({
|
||||||
|
**_request("rc06-ai-expired-prepare").model_dump(mode="python"),
|
||||||
|
"authority_expires_at": datetime.now(UTC) - timedelta(seconds=1),
|
||||||
|
})
|
||||||
|
|
||||||
|
with pytest.raises(Rc06Conflict, match="authority expired"):
|
||||||
|
service.prepare(request)
|
||||||
|
|
||||||
|
with repository._session_factory() as session:
|
||||||
|
assert session.scalar(select(Rc06PromptPublicationModel)) is None
|
||||||
|
assert session.scalars(select(Rc06PromptModulePublicationModel)).all() == []
|
||||||
|
assert {
|
||||||
|
row.name for row in session.scalars(select(PromptTemplateModel)).all()
|
||||||
|
} == set(DOMAIN_REVIEW_RUNTIME_IDENTITIES)
|
||||||
|
finally:
|
||||||
|
repository._engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def test_commit_rolls_back_all_ai_writes_when_authority_expires_before_precommit(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch,
|
||||||
|
):
|
||||||
|
repository = _repository(tmp_path)
|
||||||
|
try:
|
||||||
|
_seed_domain_dependencies(repository)
|
||||||
|
service = _service(repository)
|
||||||
|
request = _request("rc06-ai-expiry-crossing")
|
||||||
|
service.prepare(request)
|
||||||
|
expiry = request.authority_expires_at
|
||||||
|
readings = iter((expiry - timedelta(seconds=1), expiry))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rc06_publication,
|
||||||
|
"_database_wall_clock",
|
||||||
|
lambda _session: next(readings),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(Rc06Conflict, match="authority expired"):
|
||||||
|
service.commit(request.transaction_id)
|
||||||
|
|
||||||
|
with repository._session_factory() as session:
|
||||||
|
transaction = session.get(Rc06PromptPublicationModel, request.transaction_id)
|
||||||
|
assert transaction.state == "prepared"
|
||||||
|
assert transaction.item_results_json is None
|
||||||
|
assert transaction.runtime_snapshots_json is None
|
||||||
|
modules = session.scalars(select(Rc06PromptModulePublicationModel)).all()
|
||||||
|
assert len(modules) == 14
|
||||||
|
assert {item.state for item in modules} == {"prepared"}
|
||||||
|
assert {
|
||||||
|
row.name for row in session.scalars(select(PromptTemplateModel)).all()
|
||||||
|
} == set(DOMAIN_REVIEW_RUNTIME_IDENTITIES)
|
||||||
|
finally:
|
||||||
|
repository._engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_prompt_participant_is_durable_idempotent_and_compensates(tmp_path):
|
def test_sqlite_prompt_participant_is_durable_idempotent_and_compensates(tmp_path):
|
||||||
repository = _repository(tmp_path)
|
repository = _repository(tmp_path)
|
||||||
try:
|
try:
|
||||||
@@ -106,6 +167,7 @@ def test_sqlite_prompt_participant_is_durable_idempotent_and_compensates(tmp_pat
|
|||||||
assert committed.state == "committed"
|
assert committed.state == "committed"
|
||||||
assert committed.write_count == 14
|
assert committed.write_count == 14
|
||||||
assert committed.runtime_snapshot_count == 14
|
assert committed.runtime_snapshot_count == 14
|
||||||
|
assert committed.authority_expires_at == request.authority_expires_at
|
||||||
assert sum(item["source_kind"] == "composed_drafting_modules" for item in committed.runtime_snapshots) == 1
|
assert sum(item["source_kind"] == "composed_drafting_modules" for item in committed.runtime_snapshots) == 1
|
||||||
assert sum(item["source_kind"] == "new_review_module_composite" for item in committed.runtime_snapshots) == 8
|
assert sum(item["source_kind"] == "new_review_module_composite" for item in committed.runtime_snapshots) == 8
|
||||||
assert sum(item["source_kind"] == "existing_domain_prompt_asset" for item in committed.runtime_snapshots) == 5
|
assert sum(item["source_kind"] == "existing_domain_prompt_asset" for item in committed.runtime_snapshots) == 5
|
||||||
@@ -708,6 +770,8 @@ def test_prompt_journal_ddl_has_same_constraints_on_sqlite_and_postgresql():
|
|||||||
assert "compensation_evidence_sha256 VARCHAR(64)" in transaction_sql
|
assert "compensation_evidence_sha256 VARCHAR(64)" in transaction_sql
|
||||||
assert "restored_item_count INTEGER" in transaction_sql
|
assert "restored_item_count INTEGER" in transaction_sql
|
||||||
assert "ck_rc06_prompt_publications_compensation_evidence" in transaction_sql
|
assert "ck_rc06_prompt_publications_compensation_evidence" in transaction_sql
|
||||||
|
assert "authority_expires_at" in transaction_sql
|
||||||
|
assert "ck_rc06_prompt_publications_authority_window" in transaction_sql
|
||||||
assert "sealed" in transaction_sql
|
assert "sealed" in transaction_sql
|
||||||
assert "ux_rc06_prompt_module_publications_transaction_key" in module_sql
|
assert "ux_rc06_prompt_module_publications_transaction_key" in module_sql
|
||||||
assert "ck_rc06_prompt_module_state" in module_sql
|
assert "ck_rc06_prompt_module_state" in module_sql
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ from yuqei_ai_platform_api.packaged_prompt_assets_import import (
|
|||||||
load_packaged_prompt_assets,
|
load_packaged_prompt_assets,
|
||||||
)
|
)
|
||||||
from yuqei_ai_platform_api.repository import PromptTemplate
|
from yuqei_ai_platform_api.repository import PromptTemplate
|
||||||
from yuqei_ai_platform_api.review_scene_prompts import review_modules, scene_review_template
|
from yuqei_ai_platform_api.review_scene_prompts import (
|
||||||
|
review_modules,
|
||||||
|
scene_review_template,
|
||||||
|
scene_review_template_for_route,
|
||||||
|
)
|
||||||
from yuqei_ai_platform_api.scene_runtime_manifest import build_scene_runtime_manifest
|
from yuqei_ai_platform_api.scene_runtime_manifest import build_scene_runtime_manifest
|
||||||
from yuqei_contract_api.contract_templates import get_contract_template
|
from yuqei_contract_api.contract_templates import get_contract_template
|
||||||
from yuqei_ai_platform_api.scene_evaluation_gate import (
|
from yuqei_ai_platform_api.scene_evaluation_gate import (
|
||||||
@@ -231,23 +235,13 @@ def test_prompt_source_catalog_is_recomputed_from_the_actual_module_composers_an
|
|||||||
for item in REVIEW_MODULES
|
for item in REVIEW_MODULES
|
||||||
] == list(REVIEW_MODULES)
|
] == list(REVIEW_MODULES)
|
||||||
|
|
||||||
route_inputs = {
|
|
||||||
"contract_review_trade_procurement": ("purchase", "scene_evaluation_v3"),
|
|
||||||
"contract_review_services": ("service", "scene_evaluation_v3"),
|
|
||||||
"contract_review_transport_warehousing": ("transport", "scene_evaluation_v4"),
|
|
||||||
"contract_review_manufacturing": ("manufacturing", "scene_evaluation_v4"),
|
|
||||||
"contract_review_construction": ("construction", "scene_evaluation_v3"),
|
|
||||||
"contract_review_marketing": ("marketing", "scene_evaluation_v4"),
|
|
||||||
"contract_review_consumer_services": ("consumer_service", "scene_evaluation_v3"),
|
|
||||||
"contract_review_notices_amendments": ("amendment", "scene_evaluation_v4"),
|
|
||||||
}
|
|
||||||
recomputed_new_review = {}
|
recomputed_new_review = {}
|
||||||
for route, (contract_type, profile) in route_inputs.items():
|
for route in actual_review_modules:
|
||||||
template = scene_review_template(SimpleNamespace(
|
template = scene_review_template_for_route(
|
||||||
contract_type=contract_type,
|
route,
|
||||||
scene_template_id=None,
|
review_profile="scene_evaluation_v4",
|
||||||
review_profile=profile,
|
scene_template_id="rc06-runtime-snapshot",
|
||||||
))
|
)
|
||||||
assert template.name == route
|
assert template.name == route
|
||||||
recomputed_new_review[route] = {
|
recomputed_new_review[route] = {
|
||||||
"name": template.name,
|
"name": template.name,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .review_routing import REVIEW_MODULE_KEYS, SCENE_REVIEW_ROUTES
|
|||||||
|
|
||||||
|
|
||||||
BASELINE_SCHEMA_VERSION = "prompt-source-baseline.v1"
|
BASELINE_SCHEMA_VERSION = "prompt-source-baseline.v1"
|
||||||
BASELINE_VERSION = "rc05-current-source-20261006"
|
BASELINE_VERSION = "rc05-current-source-20261007"
|
||||||
|
|
||||||
DRAFTING_MODULES = (
|
DRAFTING_MODULES = (
|
||||||
{"key": "draft_intake", "version": "tp06-22-20260909-1", "content_sha256": "64519f6e5e95d9f1408e8b3db4feb818dec7cb0386de7983f591947f1cb53983"},
|
{"key": "draft_intake", "version": "tp06-22-20260909-1", "content_sha256": "64519f6e5e95d9f1408e8b3db4feb818dec7cb0386de7983f591947f1cb53983"},
|
||||||
@@ -46,13 +46,13 @@ DRAFT_RUNTIME_IDENTITY = {
|
|||||||
# A route has one release-target identity even when older evaluation evidence is
|
# A route has one release-target identity even when older evaluation evidence is
|
||||||
# still bound to an earlier profile. That evidence is not rewritten here.
|
# still bound to an earlier profile. That evidence is not rewritten here.
|
||||||
NEW_REVIEW_RUNTIME_IDENTITIES = {
|
NEW_REVIEW_RUNTIME_IDENTITIES = {
|
||||||
"contract_review_trade_procurement": {"name": "contract_review_trade_procurement", "version": "tp06-2-20260831-1", "content_sha256": "71956dde40698f3a7488da8f8b8214db8ca7bc8ebeddda6c4b965d74185bb457", "source_kind": "new_review_module_composite"},
|
"contract_review_trade_procurement": {"name": "contract_review_trade_procurement", "version": "tp06-8-20260909-1", "content_sha256": "abd850ec3a28b1c1bc1f636ba3690b84f4e7c5afb56cdd681a77d7d8eb548b2a", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_services": {"name": "contract_review_services", "version": "tp06-2-20260831-1", "content_sha256": "21ce149263a42f8cdd76f11800aedb66f5ee8e4b43cd5e41696092d96b31114d", "source_kind": "new_review_module_composite"},
|
"contract_review_services": {"name": "contract_review_services", "version": "tp06-8-20260909-1", "content_sha256": "848d6b33eebf8b3a8bcee9330e26b8b8487fa0469ac2b9ad6790a3f1559711a3", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_transport_warehousing": {"name": "contract_review_transport_warehousing", "version": "tp06-8-20260909-1", "content_sha256": "950d6a02476bf37e6c2a733710bdfdfefe474b44743bd9d7448bbf95c01c6b14", "source_kind": "new_review_module_composite"},
|
"contract_review_transport_warehousing": {"name": "contract_review_transport_warehousing", "version": "tp06-8-20260909-1", "content_sha256": "950d6a02476bf37e6c2a733710bdfdfefe474b44743bd9d7448bbf95c01c6b14", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_manufacturing": {"name": "contract_review_manufacturing", "version": "tp06-8-20260909-1", "content_sha256": "97ba767d8d47166697d7c0790fa4e17cb7b7c72950e149d6bbf4bda9bfb3d5c4", "source_kind": "new_review_module_composite"},
|
"contract_review_manufacturing": {"name": "contract_review_manufacturing", "version": "tp06-8-20260909-1", "content_sha256": "97ba767d8d47166697d7c0790fa4e17cb7b7c72950e149d6bbf4bda9bfb3d5c4", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_construction": {"name": "contract_review_construction", "version": "tp06-2-20260831-1", "content_sha256": "444552139eeec9b210f652c132ffb1ea42697e28be0c8fe33e2b46a663238cbc", "source_kind": "new_review_module_composite"},
|
"contract_review_construction": {"name": "contract_review_construction", "version": "tp06-8-20260909-1", "content_sha256": "44f52d9d292232012e8c9712ac714ac8d00f65cff7e4ddb07f66b67076499d46", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_marketing": {"name": "contract_review_marketing", "version": "tp06-8-20260909-1", "content_sha256": "261fb010dcab3d20f6f0751df941b3be11a1db06be5e4f2e43122ea2e538fcf5", "source_kind": "new_review_module_composite"},
|
"contract_review_marketing": {"name": "contract_review_marketing", "version": "tp06-8-20260909-1", "content_sha256": "261fb010dcab3d20f6f0751df941b3be11a1db06be5e4f2e43122ea2e538fcf5", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_consumer_services": {"name": "contract_review_consumer_services", "version": "tp06-2-20260831-1", "content_sha256": "c42da047fae64cf91cf544f3c694ef3ff99226dd8af0d4465b71f272d3b3f603", "source_kind": "new_review_module_composite"},
|
"contract_review_consumer_services": {"name": "contract_review_consumer_services", "version": "tp06-8-20260909-1", "content_sha256": "1d15d83ccb6fd247e459974d64acedc860b02188f0053bd79b5d0e6edab443dd", "source_kind": "new_review_module_composite"},
|
||||||
"contract_review_notices_amendments": {"name": "contract_review_notices_amendments", "version": "tp06-8-20260909-1", "content_sha256": "19fbfc866d6a67c5da9aa18e55fcd239b3d8d9915a5fa1076e8befb64c64d6e3", "source_kind": "new_review_module_composite"},
|
"contract_review_notices_amendments": {"name": "contract_review_notices_amendments", "version": "tp06-8-20260909-1", "content_sha256": "19fbfc866d6a67c5da9aa18e55fcd239b3d8d9915a5fa1076e8befb64c64d6e3", "source_kind": "new_review_module_composite"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import stat
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import types
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import Any, Iterable, Mapping
|
from typing import Any, Iterable, Mapping
|
||||||
@@ -798,7 +797,6 @@ def _load_executable_prompt_dependencies(repo: Path) -> list[dict[str, Any]]:
|
|||||||
for name in previous:
|
for name in previous:
|
||||||
sys.modules.pop(name, None)
|
sys.modules.pop(name, None)
|
||||||
try:
|
try:
|
||||||
from yuqei_ai_platform_api import prompts as prompt_routes # type: ignore
|
|
||||||
from yuqei_ai_platform_api.contract_drafting import ( # type: ignore
|
from yuqei_ai_platform_api.contract_drafting import ( # type: ignore
|
||||||
DRAFT_PROMPT_CONTENT,
|
DRAFT_PROMPT_CONTENT,
|
||||||
DRAFT_PROMPT_NAMES,
|
DRAFT_PROMPT_NAMES,
|
||||||
@@ -813,7 +811,7 @@ def _load_executable_prompt_dependencies(repo: Path) -> list[dict[str, Any]]:
|
|||||||
)
|
)
|
||||||
from yuqei_ai_platform_api.review_scene_prompts import ( # type: ignore
|
from yuqei_ai_platform_api.review_scene_prompts import ( # type: ignore
|
||||||
review_module_evidence,
|
review_module_evidence,
|
||||||
scene_review_template,
|
scene_review_template_for_route,
|
||||||
)
|
)
|
||||||
|
|
||||||
drafting_evidence = drafting_module_evidence(DRAFT_PROMPT_CONTENT)
|
drafting_evidence = drafting_module_evidence(DRAFT_PROMPT_CONTENT)
|
||||||
@@ -833,35 +831,27 @@ def _load_executable_prompt_dependencies(repo: Path) -> list[dict[str, Any]]:
|
|||||||
"modules"
|
"modules"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
original_candidates = prompt_routes.contract_review_prompt_candidates
|
for route in review_routes:
|
||||||
try:
|
template = scene_review_template_for_route(
|
||||||
for route in review_routes:
|
route,
|
||||||
prompt_routes.contract_review_prompt_candidates = (
|
review_profile="scene_evaluation_v4",
|
||||||
lambda *_args, _route=route, **_kwargs: [_route]
|
scene_template_id="rc06-runtime-snapshot",
|
||||||
|
)
|
||||||
|
module_evidence = review_module_evidence(template)
|
||||||
|
if len(module_evidence) != 1 or module_evidence[0]["key"] != route:
|
||||||
|
raise PublicationBatchError(
|
||||||
|
f"runtime review Prompt does not contain its exact module: {route}"
|
||||||
)
|
)
|
||||||
request = types.SimpleNamespace(
|
results.append(
|
||||||
contract_type="rc06_runtime_snapshot",
|
{
|
||||||
scene_template_id="rc06-runtime-snapshot",
|
"kind": "new_review_module",
|
||||||
review_profile="scene_evaluation_v4",
|
"runtime_key": route,
|
||||||
)
|
"version": template.version,
|
||||||
template = scene_review_template(request)
|
"content_sha256": template.content_hash,
|
||||||
module_evidence = review_module_evidence(template)
|
"publication_dependency": "published_module",
|
||||||
if len(module_evidence) != 1 or module_evidence[0]["key"] != route:
|
"publication_module_dependencies": module_evidence,
|
||||||
raise PublicationBatchError(
|
}
|
||||||
f"runtime review Prompt does not contain its exact module: {route}"
|
)
|
||||||
)
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"kind": "new_review_module",
|
|
||||||
"runtime_key": route,
|
|
||||||
"version": template.version,
|
|
||||||
"content_sha256": template.content_hash,
|
|
||||||
"publication_dependency": "published_module",
|
|
||||||
"publication_module_dependencies": module_evidence,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
prompt_routes.contract_review_prompt_candidates = original_candidates
|
|
||||||
|
|
||||||
packaged, _, duplicates = load_packaged_prompt_assets(repo / DEFAULT_PROMPT_PATH)
|
packaged, _, duplicates = load_packaged_prompt_assets(repo / DEFAULT_PROMPT_PATH)
|
||||||
if duplicates:
|
if duplicates:
|
||||||
|
|||||||
Reference in New Issue
Block a user