feat: unify CAD workflows on DesignIR 3.0
This commit is contained in:
@@ -65,7 +65,17 @@ ALLOWED_NUMERIC_KEYS = {
|
||||
"remaining_support",
|
||||
"required_confidence",
|
||||
"llm_proposal_count",
|
||||
"independent_case_count",
|
||||
"edit_contract_pass_rate",
|
||||
"baseline_score_delta",
|
||||
}
|
||||
ALLOWED_NUMERIC_SUFFIXES = (
|
||||
"_count",
|
||||
"_rate",
|
||||
"_ratio",
|
||||
"_delta",
|
||||
"_support",
|
||||
)
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
@@ -209,7 +219,12 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
experience = payload.get("experience") if isinstance(payload.get("experience"), dict) else {}
|
||||
provenance = payload.get("provenance") if isinstance(payload.get("provenance"), dict) else {}
|
||||
supplied_id = provenance.get("source_sha256") or payload.get("case_id")
|
||||
case_id = token(supplied_id, content_hash(payload))
|
||||
raw_id = str(supplied_id or "").strip().lower()
|
||||
case_id = (
|
||||
raw_id
|
||||
if re.fullmatch(r"[a-f0-9]{64}|[a-z][a-z0-9_.+-]*", raw_id)
|
||||
else content_hash(payload)
|
||||
)
|
||||
family = token(design_ir.get("part_family") or payload.get("part_family"))
|
||||
if family.endswith("_candidate"):
|
||||
family = family.removesuffix("_candidate")
|
||||
@@ -287,14 +302,67 @@ def normalize_case(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
|
||||
semantic_parameters: list[dict[str, Any]] = []
|
||||
raw_parameters = design_ir.get("semantic_parameter_candidates", [])
|
||||
if isinstance(raw_parameters, list):
|
||||
for raw in raw_parameters:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
name = token(raw.get("name"))
|
||||
if name == "unknown":
|
||||
continue
|
||||
edit_contract = (
|
||||
raw.get("edit_contract")
|
||||
if isinstance(raw.get("edit_contract"), dict)
|
||||
else {}
|
||||
)
|
||||
confidence = raw.get("confidence", 0.0)
|
||||
if not isinstance(confidence, (int, float)):
|
||||
confidence = 0.0
|
||||
semantic_parameters.append(
|
||||
{
|
||||
"name": name,
|
||||
"unit": token(raw.get("unit")),
|
||||
"semantic_aliases": list_tokens(raw.get("semantic_aliases")),
|
||||
"affects_feature_roles": list_tokens(
|
||||
raw.get("affects_feature_roles")
|
||||
),
|
||||
"expected_change": token(edit_contract.get("expected_change")),
|
||||
"confidence_class": (
|
||||
"high"
|
||||
if float(confidence) >= 0.8
|
||||
else "medium"
|
||||
if float(confidence) >= 0.6
|
||||
else "low"
|
||||
),
|
||||
"epistemic_status": token(raw.get("epistemic_status")),
|
||||
}
|
||||
)
|
||||
|
||||
geometry = (
|
||||
payload.get("geometry_evidence")
|
||||
if isinstance(payload.get("geometry_evidence"), dict)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"case_id": case_id,
|
||||
"extractor_version": str(payload.get("extractor_version") or "0"),
|
||||
"family": family,
|
||||
"features": sorted(features),
|
||||
"relations": sorted(relations),
|
||||
"rules": rules,
|
||||
"validations": validations,
|
||||
"normalized_observations": observations,
|
||||
"semantic_parameters": semantic_parameters,
|
||||
"reconstruction_eligible": bool(
|
||||
geometry.get(
|
||||
"reconstruction_eligible",
|
||||
geometry.get("valid", True)
|
||||
and int(geometry.get("solid_count", 1)) == 1
|
||||
and float(geometry.get("volume", 1.0)) > 0.0,
|
||||
)
|
||||
),
|
||||
"rejection_reasons": list_tokens(geometry.get("rejection_reasons")),
|
||||
"reconstruction": normalize_reconstruction(
|
||||
design_ir.get("reconstruction_evidence")
|
||||
),
|
||||
@@ -311,11 +379,18 @@ def load_cases(root: Path) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("root must be an object")
|
||||
case = normalize_case(payload)
|
||||
if not case["reconstruction_eligible"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if not case["features"] and not case["rules"] and not case["relations"]:
|
||||
rejected += 1
|
||||
continue
|
||||
if case["case_id"] in cases:
|
||||
duplicates += 1
|
||||
if case["extractor_version"] > cases[case["case_id"]][
|
||||
"extractor_version"
|
||||
]:
|
||||
cases[case["case_id"]] = case
|
||||
continue
|
||||
cases[case["case_id"]] = case
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
@@ -555,7 +630,9 @@ def audit_value(value: Any, path: tuple[str, ...], errors: list[str]) -> None:
|
||||
audit_value(child, path + (str(index),), errors)
|
||||
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
leaf = path[-1] if path else ""
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS:
|
||||
if leaf not in ALLOWED_NUMERIC_KEYS and not leaf.endswith(
|
||||
ALLOWED_NUMERIC_SUFFIXES
|
||||
):
|
||||
errors.append(f"{'.'.join(path)}: numeric value is not generalized metadata")
|
||||
|
||||
|
||||
@@ -646,7 +723,7 @@ def extract_folder(input_dir: Path, output_dir: Path) -> dict[str, Any]:
|
||||
digest = payload.get("provenance", {}).get("source_sha256")
|
||||
if (
|
||||
isinstance(digest, str)
|
||||
and payload.get("extractor_version") == "3.0"
|
||||
and payload.get("extractor_version") == "4.0"
|
||||
):
|
||||
known_hashes[digest] = path
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
@@ -711,6 +788,18 @@ def prepare_semantic_batch(
|
||||
"candidate_rules": case["rules"],
|
||||
"validation_targets": case["validations"],
|
||||
"reconstruction_evidence": case["reconstruction"],
|
||||
"semantic_parameter_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"unit": item["unit"],
|
||||
"semantic_aliases": item["semantic_aliases"],
|
||||
"affects_feature_roles": item["affects_feature_roles"],
|
||||
"expected_change": item["expected_change"],
|
||||
"confidence_class": item["confidence_class"],
|
||||
"epistemic_status": item["epistemic_status"],
|
||||
}
|
||||
for item in case["semantic_parameters"]
|
||||
],
|
||||
"dimensionless_observation_roles": [
|
||||
{
|
||||
"name": item["name"],
|
||||
@@ -753,6 +842,8 @@ def prepare_semantic_batch(
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
],
|
||||
},
|
||||
@@ -770,6 +861,8 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"validation_rule",
|
||||
"failure_repair",
|
||||
"dimensionless_distribution",
|
||||
"parameter_naming",
|
||||
"edit_strategy",
|
||||
"reconstruction_grammar",
|
||||
}
|
||||
for index, raw in enumerate(payload.get("proposals", [])):
|
||||
@@ -785,6 +878,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
)
|
||||
required_features = list_tokens(evidence.get("required_features"))
|
||||
required_relations = list_tokens(evidence.get("required_relations"))
|
||||
required_parameters = list_tokens(evidence.get("required_parameters"))
|
||||
context_features = list_tokens(
|
||||
evidence.get("context_features") or required_features
|
||||
)
|
||||
@@ -800,6 +894,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if (
|
||||
not required_features
|
||||
and not required_relations
|
||||
and not required_parameters
|
||||
and observation_name == "unknown"
|
||||
):
|
||||
raise ValueError(f"proposal {proposal_id} has no evidence query")
|
||||
@@ -814,6 +909,7 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"evidence_query": {
|
||||
"required_features": required_features,
|
||||
"required_relations": required_relations,
|
||||
"required_parameters": required_parameters,
|
||||
"context_features": context_features,
|
||||
},
|
||||
"guidance": guidance,
|
||||
@@ -833,6 +929,24 @@ def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
f"proposal {proposal_id} has an incomplete reconstruction grammar"
|
||||
)
|
||||
proposal["reconstruction_grammar"] = grammar
|
||||
if kind in {"parameter_naming", "edit_strategy"}:
|
||||
canonical_parameter = token(raw.get("canonical_parameter"))
|
||||
if canonical_parameter == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires canonical_parameter"
|
||||
)
|
||||
proposal["canonical_parameter"] = canonical_parameter
|
||||
proposal["semantic_aliases"] = list_tokens(
|
||||
raw.get("semantic_aliases")
|
||||
)
|
||||
if kind == "edit_strategy":
|
||||
expected_change = token(raw.get("expected_change"))
|
||||
if expected_change == "unknown":
|
||||
raise ValueError(
|
||||
f"proposal {proposal_id} requires expected_change"
|
||||
)
|
||||
proposal["expected_change"] = expected_change
|
||||
proposal["preserve_roles"] = list_tokens(raw.get("preserve_roles"))
|
||||
for key in ("check", "repair", "failure_signature"):
|
||||
value = string_without_instance_numbers(raw.get(key))
|
||||
if value is not None:
|
||||
@@ -849,10 +963,20 @@ def build_reviewed_library(
|
||||
min_support: int,
|
||||
min_confidence: float,
|
||||
stats: dict[str, int],
|
||||
validation_report: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
promoted: list[dict[str, Any]] = []
|
||||
candidates: list[dict[str, Any]] = []
|
||||
family_counts = Counter(case["family"] for case in cases)
|
||||
validated_methods = {
|
||||
token(item.get("experience_id")): item
|
||||
for item in (validation_report or {}).get("method_results", [])
|
||||
if isinstance(item, dict)
|
||||
and item.get("passed") is True
|
||||
and int(item.get("independent_case_count", 0)) >= 3
|
||||
and float(item.get("edit_contract_pass_rate", 0.0)) >= 1.0
|
||||
and float(item.get("baseline_score_delta", 0.0)) > 0.0
|
||||
}
|
||||
|
||||
def supports_reconstruction_grammar(
|
||||
case: dict[str, Any], grammar: dict[str, Any]
|
||||
@@ -901,6 +1025,9 @@ def build_reviewed_library(
|
||||
required_relations = set(
|
||||
proposal["evidence_query"]["required_relations"]
|
||||
)
|
||||
required_parameters = set(
|
||||
proposal["evidence_query"].get("required_parameters", [])
|
||||
)
|
||||
context_features = set(
|
||||
proposal["evidence_query"].get("context_features", required_features)
|
||||
)
|
||||
@@ -941,7 +1068,22 @@ def build_reviewed_library(
|
||||
"p90": round(percentile(values, 0.90), 6),
|
||||
"max": round(max(values), 6),
|
||||
}
|
||||
if eligible(support, item["confidence"], min_support, min_confidence):
|
||||
evidence_eligible = eligible(
|
||||
support, item["confidence"], min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(
|
||||
validation["independent_case_count"]
|
||||
),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(
|
||||
validation["baseline_score_delta"]
|
||||
),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.pop("distribution", None)
|
||||
@@ -951,6 +1093,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -966,6 +1113,9 @@ def build_reviewed_library(
|
||||
for case in contextual
|
||||
if required_features.issubset(case["features"])
|
||||
and required_relations.issubset(case["relations"])
|
||||
and required_parameters.issubset(
|
||||
{item["name"] for item in case.get("semantic_parameters", [])}
|
||||
)
|
||||
and (
|
||||
proposal["kind"] != "reconstruction_grammar"
|
||||
or supports_reconstruction_grammar(
|
||||
@@ -982,6 +1132,7 @@ def build_reviewed_library(
|
||||
"when": {
|
||||
"features": sorted(required_features),
|
||||
"relations": sorted(required_relations),
|
||||
"semantic_parameters": sorted(required_parameters),
|
||||
},
|
||||
"guidance": proposal["guidance"],
|
||||
"semantic_rationale": proposal["semantic_rationale"],
|
||||
@@ -993,7 +1144,24 @@ def build_reviewed_library(
|
||||
item[key] = proposal[key]
|
||||
if proposal["kind"] == "reconstruction_grammar":
|
||||
item["reconstruction_grammar"] = proposal["reconstruction_grammar"]
|
||||
if eligible(support, confidence, min_support, min_confidence):
|
||||
if proposal["kind"] in {"parameter_naming", "edit_strategy"}:
|
||||
item["canonical_parameter"] = proposal["canonical_parameter"]
|
||||
item["semantic_aliases"] = proposal["semantic_aliases"]
|
||||
if proposal["kind"] == "edit_strategy":
|
||||
item["expected_change"] = proposal["expected_change"]
|
||||
item["preserve_roles"] = proposal["preserve_roles"]
|
||||
evidence_eligible = eligible(
|
||||
support, confidence, min_support, min_confidence
|
||||
)
|
||||
validation = validated_methods.get(item["id"])
|
||||
if evidence_eligible and validation is not None:
|
||||
item["validation"] = {
|
||||
"independent_case_count": int(validation["independent_case_count"]),
|
||||
"edit_contract_pass_rate": float(
|
||||
validation["edit_contract_pass_rate"]
|
||||
),
|
||||
"baseline_score_delta": float(validation["baseline_score_delta"]),
|
||||
}
|
||||
promoted.append(item)
|
||||
else:
|
||||
item.update(
|
||||
@@ -1002,6 +1170,11 @@ def build_reviewed_library(
|
||||
"required_support": min_support,
|
||||
"remaining_support": max(0, min_support - support),
|
||||
"required_confidence": min_confidence,
|
||||
"validation_state": (
|
||||
"validated_positive"
|
||||
if validation is not None
|
||||
else "awaiting_positive_heldout_ab"
|
||||
),
|
||||
"consumer_policy": "visible_for_review_but_not_available_to_cad_router",
|
||||
}
|
||||
)
|
||||
@@ -1026,7 +1199,8 @@ def build_reviewed_library(
|
||||
"single_case_promotion_allowed": False,
|
||||
"llm_semantic_review_required": True,
|
||||
"draft_evidence_verified": True,
|
||||
"router_consumable": True,
|
||||
"positive_heldout_ab_required": True,
|
||||
"router_consumable": bool(promoted),
|
||||
"minimum_support": min_support,
|
||||
"minimum_confidence": min_confidence,
|
||||
},
|
||||
@@ -1115,6 +1289,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
publish_parser.add_argument("--min-support", type=int, default=20)
|
||||
publish_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
publish_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
distill_parser = subparsers.add_parser("distill")
|
||||
distill_parser.add_argument(
|
||||
@@ -1130,6 +1309,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
distill_parser.add_argument("--min-support", type=int, default=20)
|
||||
distill_parser.add_argument("--min-confidence", type=float, default=0.8)
|
||||
distill_parser.add_argument(
|
||||
"--validation-report",
|
||||
type=Path,
|
||||
help="Held-out per-method A/B and edit-contract validation report.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@@ -1205,12 +1389,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
draft_path = args.draft
|
||||
draft_payload = json.loads(draft_path.read_text(encoding="utf-8"))
|
||||
proposals = normalize_proposals(draft_payload)
|
||||
validation_report = None
|
||||
if args.validation_report is not None:
|
||||
validation_report = json.loads(
|
||||
args.validation_report.read_text(encoding="utf-8")
|
||||
)
|
||||
library = build_reviewed_library(
|
||||
cases,
|
||||
proposals,
|
||||
args.min_support,
|
||||
args.min_confidence,
|
||||
stats,
|
||||
validation_report,
|
||||
)
|
||||
errors = audit_library(library)
|
||||
if errors:
|
||||
|
||||
Reference in New Issue
Block a user