添加cadfs_to_cdsl代码,优化cad生成
This commit is contained in:
@@ -7,6 +7,7 @@ import json
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
from app.cad_agent.domain.feature_plan import FeaturePlan, FeatureScheduler, node_hash
|
||||
from app.cad_agent.application.llm_contracts import (
|
||||
CandidateReview,
|
||||
FinalReview,
|
||||
@@ -55,6 +56,236 @@ class ActionCommandHandler:
|
||||
available.append(atomic_id)
|
||||
return tuple(available)
|
||||
|
||||
def schedule_next_feature(self, task_id: str) -> Accepted | Rejected:
|
||||
"""Select the server-owned next ready node by fixed plan priority."""
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is None or state.phase != TaskPhase.SCHEDULING_FEATURE:
|
||||
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Feature scheduling is not expected in the current workflow state."))
|
||||
plan = self._feature_plan(task_id, state)
|
||||
if plan is None:
|
||||
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "The active feature plan is unavailable.", retryable=True))
|
||||
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
|
||||
if scheduler.all_done():
|
||||
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
|
||||
deterministic_failures = [
|
||||
item for item in claim_results
|
||||
if item.get("deterministic") and item.get("status") != "pass"
|
||||
]
|
||||
if deterministic_failures:
|
||||
next_state = transition(
|
||||
state,
|
||||
"feature_replan",
|
||||
error=ErrorCode.CLAIM_VERIFICATION_FAILED,
|
||||
)
|
||||
result = {
|
||||
"status": "replan_required",
|
||||
"phase": next_state.phase.value,
|
||||
"claim_results": claim_results,
|
||||
}
|
||||
if not self.repository.compare_and_swap(next_state, events=[{
|
||||
"event": "feature_plan_completion_failed",
|
||||
"plan_hash": state.feature_plan_hash,
|
||||
"revision_id": state.active_revision,
|
||||
"claim_results": claim_results,
|
||||
"failed_claim_ids": [str(item.get("claim_id") or "") for item in deterministic_failures],
|
||||
"message": "All feature nodes completed, but final deterministic validation failed.",
|
||||
}]):
|
||||
return Rejected(self._stale())
|
||||
return Accepted(result)
|
||||
next_state = transition(state, "final_requested")
|
||||
result = {"status": "final_validation", "phase": next_state.phase.value, "claim_results": claim_results}
|
||||
if not self.repository.compare_and_swap(next_state, events=[{
|
||||
"event": "feature_plan_complete",
|
||||
"plan_hash": state.feature_plan_hash,
|
||||
"revision_id": state.active_revision,
|
||||
"claim_results": claim_results,
|
||||
}], invocation_id=None, invocation_result=None):
|
||||
return Rejected(self._stale())
|
||||
return Accepted(result)
|
||||
node = scheduler.next_ready()
|
||||
if node is None:
|
||||
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "Feature plan has no runnable node; revise its unresolved subgraph."))
|
||||
contract = self.runtime.operation_contract(node.atomic_id)
|
||||
requirement_ids = tuple(sorted(self._requirement_ids_for_claims(task_id, tuple(node.claim_ids))))
|
||||
action_id = "feature_" + sha256(f"{state.feature_plan_hash}|{node.node_id}|{state.active_revision}".encode("utf-8")).hexdigest()[:16]
|
||||
pending = PendingAction(
|
||||
action_id=action_id,
|
||||
working_head=state.working_head,
|
||||
intent=node.intent,
|
||||
requirement_ids=requirement_ids,
|
||||
atomic_id=node.atomic_id,
|
||||
expected_change=node.expected_change,
|
||||
contract_hash=contract["contract_hash"],
|
||||
idempotency_key=sha256(f"{task_id}|{action_id}|{state.working_head}".encode("utf-8")).hexdigest(),
|
||||
node_id=node.node_id,
|
||||
plan_hash=state.feature_plan_hash,
|
||||
claim_ids=tuple(node.claim_ids),
|
||||
depends_on_node_ids=tuple(node.depends_on),
|
||||
)
|
||||
next_state = transition(state, "feature_scheduled", pending_action=pending)
|
||||
event = {
|
||||
"event": "feature_node_scheduled", "node_id": node.node_id, "node_hash": node_hash(node),
|
||||
"plan_hash": state.feature_plan_hash, "action_id": action_id, "atomic_id": node.atomic_id,
|
||||
"depends_on": node.depends_on, "claim_ids": node.claim_ids, "priority": node.priority,
|
||||
}
|
||||
if not self.repository.compare_and_swap(next_state, events=[event]):
|
||||
return Rejected(self._stale())
|
||||
return Accepted({"status": "scheduled", "node_id": node.node_id, "atomic_id": node.atomic_id, "phase": next_state.phase.value})
|
||||
|
||||
def submit_feature_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected:
|
||||
state = self.repository.get_state(task_id)
|
||||
action = state.pending_feature if state else None
|
||||
if state is None or state.phase != TaskPhase.FEATURE_PENDING or action is None:
|
||||
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A feature fragment requires one scheduled feature node."))
|
||||
plan = self._feature_plan(task_id, state)
|
||||
if plan is None or action.plan_hash != state.feature_plan_hash:
|
||||
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The scheduled feature belongs to an obsolete plan."))
|
||||
node = next((item for item in plan.nodes if item.node_id == action.node_id), None)
|
||||
if node is None or node.atomic_id != action.atomic_id or tuple(node.claim_ids) != action.claim_ids:
|
||||
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The scheduled feature no longer matches the active plan."))
|
||||
contract = self.runtime.operation_contract(action.atomic_id)
|
||||
if contract["contract_hash"] != action.contract_hash:
|
||||
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The operation contract changed while the feature was scheduled."))
|
||||
base = self.artifacts.read_active_cdsl(task_id, state.active_revision)
|
||||
selectors = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
|
||||
references = self.runtime.reference_tokens(base)
|
||||
selector_policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {}
|
||||
selector_kind = str(selector_policy.get("token_kind") or "")
|
||||
allowed_selectors = [
|
||||
token for token, value in selectors.items()
|
||||
if str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden") == "required"
|
||||
and isinstance(value, dict) and value.get("kind") == selector_kind
|
||||
]
|
||||
errors = validate_fragment(contract, fragment, selector_tokens=allowed_selectors, reference_tokens=list(references), root_xy_datum=not bool(state.active_revision))
|
||||
if errors:
|
||||
return self._feature_failure(task_id, state, action, ErrorCode.AUTHOR_FORMAT_INVALID, "fragment_preflight", "CDSL fragment violates the scheduled operation schema.", invocation_id=invocation_id, field_errors=tuple(errors))
|
||||
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
|
||||
feature_ids = scheduler.feature_ids()
|
||||
direct_feature_ids = tuple(feature_ids.get(dependency, "") for dependency in action.depends_on_node_ids)
|
||||
if any(not value for value in direct_feature_ids):
|
||||
return self._feature_failure(task_id, state, action, ErrorCode.RUNTIME_PRECONDITION_FAILED, "dependency", "A direct dependency has no verified runtime feature.", invocation_id=invocation_id)
|
||||
fragment_hash = canonical_hash(fragment)
|
||||
key = self._key(task_id, "feature_fragment", state.working_head, {"node_id": action.node_id, "fragment": fragment})
|
||||
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
||||
if invocation.status == "finished" and invocation.result is not None:
|
||||
return Accepted(invocation.result)
|
||||
try:
|
||||
cdsl, audit = self.runtime.materialize_fragment(
|
||||
base, fragment, contract, selectors, references,
|
||||
require_through=self._claims_require_through(task_id, action.claim_ids),
|
||||
depends_on_feature_ids=direct_feature_ids,
|
||||
)
|
||||
except Exception as error:
|
||||
return self._feature_failure(task_id, state, action, self._runtime_error(error).code, "fragment_preflight", str(error), invocation_id=invocation_id, invocation=invocation)
|
||||
stage_id = "feature_stage_" + sha256(key.encode("utf-8")).hexdigest()[:16]
|
||||
try:
|
||||
stage = self.artifacts.start_candidate_stage(task_id, key, {
|
||||
"schema_version": "cad.v3.2.feature-input.v1", "stage_id": stage_id,
|
||||
"node_id": action.node_id, "node_hash": node_hash(node), "plan_hash": state.feature_plan_hash,
|
||||
"action_id": action.action_id, "fragment": fragment, "fragment_audit": audit,
|
||||
})
|
||||
except OSError as error:
|
||||
return self._park_retry(state, ErrorCode.STORAGE_FAILURE, event="feature_stage_storage_failure", message=str(error), details={"node_id": action.node_id})
|
||||
building = transition(state, "feature_started", feature_stage_id=stage.stage_id)
|
||||
if not self.repository.compare_and_swap(building, events=[{
|
||||
"event": "feature_node_building", "node_id": action.node_id, "node_hash": node_hash(node),
|
||||
"plan_hash": state.feature_plan_hash, "stage_id": stage.stage_id, "fragment_hash": fragment_hash,
|
||||
}]):
|
||||
return Rejected(self._stale())
|
||||
try:
|
||||
rebuilt = self.runtime.build_checkpoint(cdsl, stage.output_dir, task_id, stage_id)
|
||||
try:
|
||||
preview = self.runtime.create_preview(stage.output_dir)
|
||||
rebuilt["paths"]["glb"] = preview["path"]
|
||||
except Exception as preview_error:
|
||||
preview = {"preview_unavailable": str(preview_error)[:500]}
|
||||
claim_results = self._evaluate_claims(task_id, rebuilt, claim_ids=set(action.claim_ids))
|
||||
operation_results = self._operation_candidate_results(
|
||||
action, contract, cdsl, rebuilt, parent_facts=self._facts(task_id, state.active_revision),
|
||||
require_through=self._claims_require_through(task_id, action.claim_ids),
|
||||
)
|
||||
global_failures = [item for item in self._evaluate_claims(task_id, rebuilt) if item.get("claim_kind") in {"solid_count_equals", "single_connected_body"} and item.get("status") != "pass"]
|
||||
blockers = [
|
||||
# A claim is assigned to exactly one runtime-atomic node in
|
||||
# the Feature Plan. Unlike unassigned future-work claims,
|
||||
# an assigned claim may not remain pending when that node is
|
||||
# published: doing so would turn a missing feature into a
|
||||
# permanent, apparently successful checkpoint.
|
||||
*[item for item in claim_results if item.get("status") != "pass"],
|
||||
*[item for item in operation_results if item.get("status") != "pass"],
|
||||
*global_failures,
|
||||
]
|
||||
if blockers:
|
||||
self.artifacts.write_stage_json(task_id, stage.stage_id, "node-verification.json", {
|
||||
"schema_version": "cad.v3.2.node-verification.v1", "node_id": action.node_id,
|
||||
"claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers,
|
||||
})
|
||||
return self._feature_failure(task_id, building, action, ErrorCode.CLAIM_VERIFICATION_FAILED, "node_validation", "The feature did not satisfy its local deterministic acceptance.", invocation_id=invocation_id, invocation=invocation, details={"stage_id": stage.stage_id, "blockers": blockers})
|
||||
verification = {
|
||||
"schema_version": "cad.v3.2.node-verification.v1", "node_id": action.node_id,
|
||||
"node_hash": node_hash(node), "plan_hash": state.feature_plan_hash,
|
||||
"feature_id": audit["assigned_feature_ids"][0], "claim_results": claim_results,
|
||||
"operation_verifier_results": operation_results, "health": rebuilt["health"], "preview": preview,
|
||||
}
|
||||
self.artifacts.write_stage_json(task_id, stage.stage_id, "node-verification.json", verification)
|
||||
revision_id = self._next_revision(task_id)
|
||||
paths = self.artifacts.publish_candidate(task_id, stage.stage_id, revision_id)
|
||||
next_state = transition(building, "feature_verified", active_revision=revision_id)
|
||||
result = {"status": "verified", "node_id": action.node_id, "revision_id": revision_id, "paths": paths, "claim_results": claim_results}
|
||||
event = {
|
||||
"event": "feature_node_verified", "node_id": action.node_id, "node_hash": node_hash(node),
|
||||
"plan_hash": state.feature_plan_hash, "feature_id": audit["assigned_feature_ids"][0],
|
||||
"atomic_id": action.atomic_id,
|
||||
"revision_id": revision_id, "parent_revision": state.active_revision, "stage_id": stage.stage_id,
|
||||
"claim_results": claim_results, "preview_unavailable": preview.get("preview_unavailable", ""),
|
||||
}
|
||||
if not self._commit_invocation(next_state, [event], invocation, result):
|
||||
return Rejected(self._stale())
|
||||
return Accepted(result)
|
||||
except OSError as error:
|
||||
return self._park_retry(building, ErrorCode.STORAGE_FAILURE, event="feature_build_storage_failure", message=str(error), details={"node_id": action.node_id, "stage_id": stage.stage_id})
|
||||
except Exception as error:
|
||||
return self._feature_failure(task_id, building, action, self._runtime_error(error).code, "engine_build", str(error), invocation_id=invocation_id, invocation=invocation, details={"stage_id": stage.stage_id})
|
||||
|
||||
def recover_feature_build(self, task_id: str) -> Accepted | Rejected:
|
||||
"""Make a crashed build retryable without losing its scheduled node."""
|
||||
state = self.repository.get_state(task_id)
|
||||
action = state.pending_feature if state else None
|
||||
if state is None or state.phase != TaskPhase.FEATURE_BUILDING or action is None:
|
||||
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "There is no recoverable feature build."))
|
||||
next_state = transition(state, "feature_retry")
|
||||
if not self.repository.compare_and_swap(next_state, events=[{
|
||||
"event": "feature_build_recovered", "node_id": action.node_id, "plan_hash": action.plan_hash,
|
||||
"stage_id": state.feature_stage_id, "message": "Interrupted node build returned to the same scheduled feature.",
|
||||
}]):
|
||||
return Rejected(self._stale())
|
||||
return Accepted({"status": "retry", "node_id": action.node_id, "phase": next_state.phase.value})
|
||||
|
||||
def _feature_failure(self, task_id: str, state: TaskState, action: PendingAction, code: ErrorCode, failure_class: str, message: str, *, invocation_id: str, field_errors: tuple[dict[str, Any], ...] = (), invocation: Any = None, details: dict[str, Any] | None = None) -> Rejected:
|
||||
plan = self._feature_plan(task_id, state)
|
||||
node = next((item for item in (plan.nodes if plan else []) if item.node_id == action.node_id), None)
|
||||
if node is None:
|
||||
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "The active feature node is unavailable.", retryable=True))
|
||||
scheduler = FeatureScheduler(plan, self.repository.ledger_events(task_id))
|
||||
attempt = scheduler.failure_count(action.node_id, failure_class) + 1
|
||||
terminal = attempt >= 2
|
||||
event = {
|
||||
"event": "feature_node_failed", "node_id": action.node_id, "node_hash": node_hash(node),
|
||||
"plan_hash": state.feature_plan_hash, "failure_class": failure_class, "attempt": attempt,
|
||||
"terminal": terminal, "code": code.value, "normalized_error_code": code.value,
|
||||
"atomic_id": action.atomic_id, "checkpoint_revision": state.active_revision,
|
||||
"message": message[:1000], **(details or {}),
|
||||
}
|
||||
next_state = transition(state, "feature_replan" if terminal else "feature_retry", error=code)
|
||||
result = {"status": "replan_required" if terminal else "retry", "node_id": action.node_id, "attempt": attempt, "failure_class": failure_class}
|
||||
if invocation is not None:
|
||||
committed = self._commit_invocation(next_state, [event], invocation, result)
|
||||
else:
|
||||
committed = self.repository.compare_and_swap(next_state, events=[event])
|
||||
if not committed:
|
||||
return Rejected(self._stale())
|
||||
return Rejected(WorkflowError(code, message, field_errors=field_errors, details={**(details or {}), "node_id": action.node_id, "attempt": attempt, "replan_required": terminal}))
|
||||
|
||||
def propose_next_action(self, task_id: str, proposal: NextAction, *, invocation_id: str) -> Accepted | Rejected:
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
|
||||
@@ -388,6 +619,8 @@ class ActionCommandHandler:
|
||||
|
||||
def submit_cdsl_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected:
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is not None and state.phase == TaskPhase.FEATURE_PENDING:
|
||||
return self.submit_feature_fragment(task_id, fragment, invocation_id=invocation_id)
|
||||
action = state.pending_action if state else None
|
||||
if state is None or state.phase != TaskPhase.ACTION_PENDING or action is None:
|
||||
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A CDSL fragment requires one pending action."))
|
||||
@@ -922,11 +1155,18 @@ class ActionCommandHandler:
|
||||
"atomic_id": atomic_id,
|
||||
"fragment_hash": accepted_fragment,
|
||||
}) if accepted_fragment and atomic_id else ""
|
||||
next_state = transition(state, "final_repair", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED)
|
||||
is_dag = bool(state.feature_plan_hash)
|
||||
next_state = transition(
|
||||
state,
|
||||
"feature_replan" if is_dag else "final_repair",
|
||||
repair_required=not is_dag,
|
||||
error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED,
|
||||
)
|
||||
result = {"status": "repair", "revision_id": state.active_revision}
|
||||
if not self._commit_invocation(next_state, [{
|
||||
"event": "final_review_repair",
|
||||
"event": "final_visual_reviewed" if is_dag else "final_review_repair",
|
||||
"revision_id": state.active_revision,
|
||||
**({"plan_hash": state.feature_plan_hash} if is_dag else {}),
|
||||
"claim_results": claim_results,
|
||||
"review_claim_coverage": [item.model_dump(mode="json") for item in review.claim_coverage],
|
||||
"visual_not_passed": [str(item.get("claim_id") or "") for item in visual_not_passed],
|
||||
@@ -947,15 +1187,48 @@ class ActionCommandHandler:
|
||||
return Accepted(result)
|
||||
next_state = transition(state, "final_accepted")
|
||||
result = {"status": "completed", "revision_id": state.active_revision}
|
||||
if not self._commit_invocation(next_state, [{"event": "completed", "revision_id": state.active_revision, "final_review_path": final_review_path, "completion_result_path": "completion-result.md", "claim_results": claim_results}], invocation, result):
|
||||
if not self._commit_invocation(next_state, [{"event": "final_visual_reviewed", "revision_id": state.active_revision, "final_review_path": final_review_path, "completion_result_path": "completion-result.md", "claim_results": claim_results}], invocation, result):
|
||||
return Rejected(self._stale())
|
||||
return Accepted(result)
|
||||
|
||||
def _evaluate_claims(self, task_id: str, facts: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
def _evaluate_claims(self, task_id: str, facts: dict[str, Any], *, claim_ids: set[str] | None = None) -> list[dict[str, Any]]:
|
||||
contract = self._requirements_contract(task_id) or {}
|
||||
claims = [claim for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)]
|
||||
claims = [
|
||||
claim for requirement in contract.get("requirements") or () if isinstance(requirement, dict)
|
||||
for claim in requirement.get("acceptance_claims") or ()
|
||||
if isinstance(claim, dict) and (claim_ids is None or str(claim.get("claim_id") or "") in claim_ids)
|
||||
]
|
||||
return self.verifiers.evaluate(claims, facts)
|
||||
|
||||
def _feature_plan(self, task_id: str, state: TaskState | None = None) -> FeaturePlan | None:
|
||||
state = state or self.repository.get_state(task_id)
|
||||
if state is None or not state.feature_plan_path:
|
||||
return None
|
||||
raw = self.artifacts.read_json(task_id, state.feature_plan_path)
|
||||
try:
|
||||
return FeaturePlan.model_validate(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _requirement_ids_for_claims(self, task_id: str, claim_ids: tuple[str, ...]) -> set[str]:
|
||||
wanted = set(claim_ids)
|
||||
contract = self._requirements_contract(task_id) or {}
|
||||
return {
|
||||
str(requirement.get("requirement_id") or "")
|
||||
for requirement in contract.get("requirements") or ()
|
||||
if isinstance(requirement, dict)
|
||||
and any(str(claim.get("claim_id") or "") in wanted for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict))
|
||||
}
|
||||
|
||||
def _claims_require_through(self, task_id: str, claim_ids: tuple[str, ...]) -> bool:
|
||||
wanted = set(claim_ids)
|
||||
contract = self._requirements_contract(task_id) or {}
|
||||
return any(
|
||||
str(claim.get("claim_id") or "") in wanted and claim.get("claim_kind") == "through_cylindrical_bore"
|
||||
for requirement in contract.get("requirements") or () if isinstance(requirement, dict)
|
||||
for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)
|
||||
)
|
||||
|
||||
def claim_summary(self, task_id: str, state: TaskState) -> list[dict[str, Any]]:
|
||||
"""Return bounded current claim facts for author context assembly."""
|
||||
return [
|
||||
@@ -1059,7 +1332,16 @@ class ActionCommandHandler:
|
||||
continue
|
||||
expected: dict[str, Any]
|
||||
if claim_kind in {"cylindrical_bore", "through_cylindrical_bore"}:
|
||||
diameter = params.get("diameter_mm")
|
||||
# A counterbore can reuse an already-existing pilot bore. In
|
||||
# that case the material change is the larger cylindrical
|
||||
# recess, not an additional instance of the pilot diameter.
|
||||
# Measuring the pilot would count the parent bore as a new
|
||||
# feature and make a valid counterbore checkpoint fail.
|
||||
diameter = (
|
||||
params.get("counterbore_diameter_mm")
|
||||
if action.atomic_id == "hole_counterbore" and claim_kind == "cylindrical_bore"
|
||||
else params.get("diameter_mm")
|
||||
)
|
||||
positions = params.get("positions")
|
||||
if not isinstance(diameter, (int, float)):
|
||||
return [{"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": claim_kind, "deterministic": True, "status": "unavailable", "evidence": {"reason": "operation has no measurable bore diameter"}}]
|
||||
@@ -1152,7 +1434,7 @@ class ActionCommandHandler:
|
||||
values = [
|
||||
int(str(event.get("revision_id") or "").removeprefix("rev_"))
|
||||
for event in self.repository.ledger_events(task_id)
|
||||
if event.get("event") == "accepted"
|
||||
if event.get("event") in {"accepted", "feature_node_verified"}
|
||||
and str(event.get("revision_id") or "").startswith("rev_")
|
||||
and str(event.get("revision_id") or "").removeprefix("rev_").isdigit()
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user