#!/usr/bin/env python3 """Induce and query generalized CAD experience without leaking case geometry.""" from __future__ import annotations import argparse import hashlib import importlib.util import itertools import json import math import re import subprocess import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path from statistics import median from typing import Any, Iterable SCHEMA_VERSION = "2.0" TOKEN_RE = re.compile(r"^[a-z][a-z0-9_.+-]*$") NUMERIC_LITERAL_RE = re.compile(r"(? str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def canonical_json(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def content_hash(payload: Any) -> str: return hashlib.sha256(canonical_json(payload).encode("utf-8")).hexdigest() def token(value: Any, default: str = "unknown") -> str: candidate = str(value or "").strip().lower().replace(" ", "_") return candidate if TOKEN_RE.fullmatch(candidate) else default def string_without_instance_numbers(value: Any) -> str | None: if not isinstance(value, str): return None rendered = " ".join(value.split()) if not rendered or NUMERIC_LITERAL_RE.search(rendered): return None return rendered def list_tokens(value: Any) -> list[str]: if not isinstance(value, list): return [] return sorted({item for raw in value if (item := token(raw)) != "unknown"}) def normalize_stage(raw: Any) -> dict[str, Any] | None: if not isinstance(raw, dict): return None stage_id = token(raw.get("id")) operation = token(raw.get("operation")) if stage_id == "unknown" or operation == "unknown": return None return { "id": stage_id, "operation": operation, "feature_roles": list_tokens(raw.get("feature_roles")), "reference_roles": list_tokens(raw.get("reference_roles")), } def normalize_reconstruction(raw: Any) -> dict[str, Any]: if not isinstance(raw, dict): return { "parameter_roles": [], "datum_roles": [], "feature_roles": [], "relation_roles": [], "canonical_stages": [], "cardinality_classes": [], "validation_roles": [], } private_cardinality = ( raw.get("private_cardinality_evidence") if isinstance(raw.get("private_cardinality_evidence"), dict) else {} ) class_values = private_cardinality.get("surface_type_classes", {}) cardinality_classes = list_tokens(raw.get("cardinality_classes")) if isinstance(class_values, dict): cardinality_classes.extend( token(f"{kind}_{value}") for kind, value in class_values.items() ) for role in ( "feature_role_count_class", "coaxial_radius_role_count_class", "repeated_member_count_class", ): value = private_cardinality.get(role) if value: cardinality_classes.append(token(f"{role}.{value}")) return { "parameter_roles": list_tokens(raw.get("parameter_roles")), "datum_roles": list_tokens(raw.get("datum_roles")), "feature_roles": list_tokens(raw.get("feature_roles")), "relation_roles": list_tokens(raw.get("relation_roles")), "canonical_stages": [ stage for item in raw.get("canonical_stages", []) if (stage := normalize_stage(item)) is not None ] if isinstance(raw.get("canonical_stages"), list) else [], "cardinality_classes": sorted( {item for item in cardinality_classes if item != "unknown"} ), "validation_roles": list_tokens(raw.get("validation_roles")), } def percentile(values: list[float], fraction: float) -> float: ordered = sorted(values) if len(ordered) == 1: return ordered[0] position = (len(ordered) - 1) * fraction lower = math.floor(position) upper = math.ceil(position) if lower == upper: return ordered[lower] weight = position - lower return ordered[lower] * (1.0 - weight) + ordered[upper] * weight def iter_json_files(root: Path) -> Iterable[Path]: if root.is_file(): yield root return yield from sorted(path for path in root.rglob("*.json") if path.is_file()) def normalize_rule(raw: Any) -> dict[str, Any] | None: if not isinstance(raw, dict): return None rule_id = token(raw.get("id")) statement = string_without_instance_numbers(raw.get("statement") or raw.get("guidance")) if rule_id == "unknown" or statement is None: return None result: dict[str, Any] = { "id": rule_id, "scope": list_tokens(raw.get("scope")), "statement": statement, } for key in ("failure_signature", "repair", "check"): clean = string_without_instance_numbers(raw.get(key)) if clean is not None: result[key] = clean return result def normalize_case(payload: dict[str, Any]) -> dict[str, Any]: design_ir = payload.get("design_ir") if isinstance(payload.get("design_ir"), dict) else {} 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)) family = token(design_ir.get("part_family") or payload.get("part_family")) if family.endswith("_candidate"): family = family.removesuffix("_candidate") features: set[str] = set() raw_features = design_ir.get("features", payload.get("features", [])) if isinstance(raw_features, list): for feature in raw_features: kind = feature.get("type") if isinstance(feature, dict) else feature normalized = token(kind) if normalized.endswith("_candidate"): normalized = normalized.removesuffix("_candidate") if normalized != "unknown": features.add(normalized) relations: set[str] = set() raw_relations = design_ir.get("constraints", payload.get("semantic_relations", [])) if isinstance(raw_relations, list): for relation in raw_relations: relation_id = ( relation.get("id") or relation.get("type") if isinstance(relation, dict) else relation ) normalized = token(relation_id) if normalized != "unknown": relations.add(normalized) rules = [ normalized for raw in experience.get("rules", payload.get("candidate_rules", [])) if (normalized := normalize_rule(raw)) is not None ] validations = [ normalized for raw in experience.get("validation_targets", payload.get("validation_targets", [])) if ( normalized := normalize_rule( { "id": raw.get("id") if isinstance(raw, dict) else None, "scope": raw.get("scope", []) if isinstance(raw, dict) else [], "statement": raw.get("check") if isinstance(raw, dict) else None, } ) ) is not None ] observations: list[dict[str, Any]] = [] raw_observations = design_ir.get( "normalized_observations", payload.get("normalized_observations", []) ) if isinstance(raw_observations, list): for observation in raw_observations: if not isinstance(observation, dict): continue name = token(observation.get("name")) numerator = token(observation.get("numerator_role")) denominator = token(observation.get("denominator_role")) value = observation.get("value") if ( name != "unknown" and numerator != "unknown" and denominator != "unknown" and isinstance(value, (int, float)) and math.isfinite(float(value)) and 0.0 <= float(value) <= 10.0 ): observations.append( { "name": name, "numerator_role": numerator, "denominator_role": denominator, "value": float(value), } ) return { "case_id": case_id, "family": family, "features": sorted(features), "relations": sorted(relations), "rules": rules, "validations": validations, "normalized_observations": observations, "reconstruction": normalize_reconstruction( design_ir.get("reconstruction_evidence") ), } def load_cases(root: Path) -> tuple[list[dict[str, Any]], dict[str, int]]: cases: dict[str, dict[str, Any]] = {} duplicates = 0 rejected = 0 for path in iter_json_files(root): try: payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError("root must be an object") case = normalize_case(payload) if not case["features"] and not case["rules"] and not case["relations"]: rejected += 1 continue if case["case_id"] in cases: duplicates += 1 continue cases[case["case_id"]] = case except (OSError, json.JSONDecodeError, ValueError, TypeError): rejected += 1 return list(cases.values()), { "duplicate_case_count": duplicates, "rejected_case_count": rejected, } def eligible( support: int, confidence: float, min_support: int, min_confidence: float ) -> bool: return support >= min_support and confidence >= min_confidence def induce( cases: list[dict[str, Any]], min_support: int, min_confidence: float, stats: dict[str, int], ) -> dict[str, Any]: total = len(cases) experiences: list[dict[str, Any]] = [] candidates: list[dict[str, Any]] = [] family_counts = Counter(case["family"] for case in cases) def record(item: dict[str, Any], support: int, confidence: float) -> None: item["support"] = support item["confidence"] = round(confidence, 6) if eligible(support, confidence, min_support, min_confidence): experiences.append(item) return candidate = { key: value for key, value in item.items() if key != "distribution" } candidate.update( { "promotion_state": "candidate", "required_support": min_support, "remaining_support": max(0, min_support - support), "required_confidence": min_confidence, "consumer_policy": "visible_for_review_but_not_available_to_cad_router", } ) candidates.append(candidate) family_cases: dict[str, list[dict[str, Any]]] = defaultdict(list) for case in cases: family_cases[case["family"]].append(case) for family, rows in sorted(family_cases.items()): pair_counts: Counter[tuple[str, str]] = Counter( pair for case in rows for pair in itertools.combinations(case["features"], 2) ) for pair, support in sorted(pair_counts.items()): confidence = support / len(rows) record( { "id": f"motif.{family}.{pair[0]}+{pair[1]}", "kind": "feature_motif", "scope": [family], "when": {"features": list(pair)}, "guidance": "Treat these feature roles as a reusable composition.", }, support, confidence, ) relation_counts = Counter( relation for case in rows for relation in set(case["relations"]) ) for relation_id, support in sorted(relation_counts.items()): confidence = support / len(rows) record( { "id": f"constraint.{family}.{relation_id}", "kind": "constraint", "scope": [family], "when": {"relation": relation_id}, "guidance": "Preserve this semantic relationship when its participating features are present.", }, support, confidence, ) rule_groups: dict[str, list[tuple[dict[str, Any], dict[str, Any]]]] = defaultdict(list) for case in cases: for rule in case["rules"]: rule_groups[rule["id"]].append((case, rule)) for rule_id, rows in sorted(rule_groups.items()): variants = Counter(canonical_json(rule) for _, rule in rows) rendered, support = variants.most_common(1)[0] rule = json.loads(rendered) relevant_total = sum( 1 for case in cases if not rule["scope"] or case["family"] in rule["scope"] ) denominator = relevant_total or total confidence = support / denominator if denominator else 0.0 item: dict[str, Any] = { "id": f"rule.{rule_id}", "kind": "failure_repair" if "repair" in rule else "design_rule", "scope": rule["scope"] or ["global"], "when": {"rule": rule_id}, "guidance": rule["statement"], } for key in ("failure_signature", "repair", "check"): if key in rule: item[key] = rule[key] record(item, support, confidence) validation_groups: dict[str, list[dict[str, Any]]] = defaultdict(list) for case in cases: for validation in case["validations"]: validation_groups[validation["id"]].append(validation) for validation_id, rows in sorted(validation_groups.items()): variants = Counter(canonical_json(row) for row in rows) rendered, support = variants.most_common(1)[0] validation = json.loads(rendered) confidence = support / total if total else 0.0 record( { "id": f"validation.{validation_id}", "kind": "validation_rule", "scope": validation["scope"] or ["global"], "when": {"validation": validation_id}, "check": validation["statement"], }, support, confidence, ) observation_groups: dict[tuple[str, str, str], list[float]] = defaultdict(list) for case in cases: seen_names: set[str] = set() for observation in case["normalized_observations"]: if observation["name"] in seen_names: continue seen_names.add(observation["name"]) key = ( observation["name"], observation["numerator_role"], observation["denominator_role"], ) observation_groups[key].append(observation["value"]) for (name, numerator, denominator), values in sorted(observation_groups.items()): support = len(values) confidence = support / total if total else 0.0 record( { "id": f"distribution.{name}", "kind": "dimensionless_distribution", "scope": ["global"], "when": { "ratio": { "numerator_role": numerator, "denominator_role": denominator, } }, "guidance": "Use this distribution only to fill an unspecified relationship; explicit user dimensions take priority.", "distribution": { "sample_count": support, "min": round(min(values), 6), "p10": round(percentile(values, 0.10), 6), "median": round(median(values), 6), "p90": round(percentile(values, 0.90), 6), "max": round(max(values), 6), }, }, support, confidence, ) sorted_experiences = sorted(experiences, key=lambda item: item["id"]) sorted_candidates = sorted(candidates, key=lambda item: item["id"]) kind_counts = Counter(item["kind"] for item in sorted_experiences) candidate_kind_counts = Counter(item["kind"] for item in sorted_candidates) scope_counts = Counter( scope for item in sorted_experiences for scope in item.get("scope", ["global"]) ) return { "schema_version": SCHEMA_VERSION, "library_kind": "generalized_cad_experience", "generated_at": utc_now(), "status": ( "ready" if experiences else "collecting_evidence" if candidates else "empty" ), "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "minimum_support": min_support, "minimum_confidence": min_confidence, }, "corpus_summary": { "accepted_case_count": total, "duplicate_case_count": stats["duplicate_case_count"], "rejected_case_count": stats["rejected_case_count"], "family_count": len(family_counts), }, "experience_summary": { "promoted_experience_count": len(sorted_experiences), "candidate_experience_count": len(sorted_candidates), "by_kind": [ {"kind": kind, "count": count} for kind, count in sorted(kind_counts.items()) ], "by_scope": [ {"scope": scope, "count": count} for scope, count in sorted(scope_counts.items()) ], "candidate_by_kind": [ {"kind": kind, "count": count} for kind, count in sorted(candidate_kind_counts.items()) ], }, "experiences": sorted_experiences, "candidate_experiences": sorted_candidates, } def audit_value(value: Any, path: tuple[str, ...], errors: list[str]) -> None: if isinstance(value, dict): for key, child in value.items(): if key in FORBIDDEN_KEYS: errors.append(f"{'.'.join(path + (key,))}: forbidden instance key") audit_value(child, path + (key,), errors) elif isinstance(value, list): for index, child in enumerate(value): 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: errors.append(f"{'.'.join(path)}: numeric value is not generalized metadata") def audit_library(payload: dict[str, Any]) -> list[str]: errors: list[str] = [] if payload.get("schema_version") != SCHEMA_VERSION: errors.append("schema_version: expected 2.0") if payload.get("library_kind") != "generalized_cad_experience": errors.append("library_kind: expected generalized_cad_experience") audit_value(payload, (), errors) return errors def query_library( payload: dict[str, Any], family: str | None, features: set[str] ) -> dict[str, Any]: selected: list[dict[str, Any]] = [] normalized_family = token(family) if family else None for item in payload.get("experiences", []): scope = set(item.get("scope", [])) if "global" not in scope: if not normalized_family or normalized_family not in scope: continue required = set(item.get("when", {}).get("features", [])) if required and features and not required.issubset(features): continue selected.append(item) return { "schema_version": SCHEMA_VERSION, "context_kind": "generalized_cad_experience_query", "family": normalized_family, "requested_features": sorted(features), "experiences": selected, "policy": { "contains_instance_parameters": False, "contains_absolute_coordinates": False, }, } def write_json(path: Path, payload: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text( json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" ) def extract_step_case(source: Path) -> dict[str, Any]: script = Path(__file__).with_name("step_to_case.py") spec = importlib.util.spec_from_file_location("cad_experience_step_to_case", script) if spec is None or spec.loader is None: raise RuntimeError(f"cannot load STEP extractor: {script}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.extract_step_case(source) def plugin_root() -> Path: return Path(__file__).resolve().parents[3] def default_library_path() -> Path: return plugin_root().parent / "cad-experience-library" / "library.json" def parser_root() -> Path: return plugin_root() / "parser" def step_files(root: Path) -> list[Path]: return sorted( path for path in root.rglob("*") if path.is_file() and path.suffix.lower() in {".step", ".stp"} ) def extract_folder(input_dir: Path, output_dir: Path) -> dict[str, Any]: output_dir.mkdir(parents=True, exist_ok=True) known_hashes: dict[str, Path] = {} for path in iter_json_files(output_dir): try: payload = json.loads(path.read_text(encoding="utf-8")) digest = payload.get("provenance", {}).get("source_sha256") if ( isinstance(digest, str) and payload.get("extractor_version") == "3.0" ): known_hashes[digest] = path except (OSError, json.JSONDecodeError, AttributeError): continue created: list[str] = [] skipped: list[str] = [] failures: list[dict[str, str]] = [] for source in step_files(input_dir): digest = hashlib.sha256(source.read_bytes()).hexdigest() if digest in known_hashes: skipped.append(str(source)) continue try: payload = extract_step_case(source) output = output_dir / f"{source.stem}.{digest[:12]}.case.json" write_json(output, payload) known_hashes[digest] = output created.append(str(output)) except Exception as exc: failures.append({"source": str(source), "error": str(exc)}) return { "schema_version": "1.0", "input_dir": str(input_dir.resolve()), "output_dir": str(output_dir.resolve()), "discovered_step_count": len(step_files(input_dir)), "created_case_count": len(created), "skipped_duplicate_count": len(skipped), "failed_count": len(failures), "created_cases": created, "failures": failures, } def default_review_dir() -> Path: return plugin_root() / "work" / "review" def prepare_semantic_batch( cases: list[dict[str, Any]], existing_library: dict[str, Any] | None ) -> dict[str, Any]: family_counts = Counter(case["family"] for case in cases) return { "schema_version": "1.0", "batch_kind": "sanitized_cad_semantic_review", "guardrails": { "contains_instance_dimensions": False, "contains_absolute_coordinates": False, "llm_must_propose_methods_not_copy_answers": True, "proposals_require_deterministic_evidence_verification": True, }, "corpus_summary": { "case_count": len(cases), "family_count": len(family_counts), }, "cases": [ { "anonymous_case": f"case_{index:06d}", "family": case["family"], "features": case["features"], "relations": case["relations"], "candidate_rules": case["rules"], "validation_targets": case["validations"], "reconstruction_evidence": case["reconstruction"], "dimensionless_observation_roles": [ { "name": item["name"], "numerator_role": item["numerator_role"], "denominator_role": item["denominator_role"], } for item in case["normalized_observations"] ], } for index, case in enumerate(cases, start=1) ], "existing_vocabulary": { "promoted_ids": [ item.get("id") for item in (existing_library or {}).get("experiences", []) if isinstance(item, dict) ], "candidate_ids": [ item.get("id") for item in (existing_library or {}).get( "candidate_experiences", [] ) if isinstance(item, dict) ], }, "draft_contract": { "draft_kind": "llm_generalized_experience_proposals", "proposal_required_fields": [ "id", "kind", "scope", "evidence_query", "guidance", "semantic_rationale", ], "allowed_kinds": [ "feature_motif", "constraint", "design_rule", "validation_rule", "failure_repair", "dimensionless_distribution", "reconstruction_grammar", ], }, } def normalize_proposals(payload: dict[str, Any]) -> list[dict[str, Any]]: if payload.get("draft_kind") != "llm_generalized_experience_proposals": raise ValueError("draft_kind must be llm_generalized_experience_proposals") proposals: list[dict[str, Any]] = [] allowed_kinds = { "feature_motif", "constraint", "design_rule", "validation_rule", "failure_repair", "dimensionless_distribution", "reconstruction_grammar", } for index, raw in enumerate(payload.get("proposals", [])): if not isinstance(raw, dict): raise ValueError(f"proposal {index} must be an object") proposal_id = token(raw.get("id")) kind = token(raw.get("kind")) scopes = list_tokens(raw.get("scope")) evidence = ( raw.get("evidence_query") if isinstance(raw.get("evidence_query"), dict) else {} ) required_features = list_tokens(evidence.get("required_features")) required_relations = list_tokens(evidence.get("required_relations")) context_features = list_tokens( evidence.get("context_features") or required_features ) observation_name = token(raw.get("observation_name")) guidance = string_without_instance_numbers(raw.get("guidance")) rationale = string_without_instance_numbers(raw.get("semantic_rationale")) if proposal_id == "unknown": raise ValueError(f"proposal {index} has an invalid id") if kind not in allowed_kinds: raise ValueError(f"proposal {proposal_id} has an unsupported kind") if not scopes: raise ValueError(f"proposal {proposal_id} has no scope") if ( not required_features and not required_relations and observation_name == "unknown" ): raise ValueError(f"proposal {proposal_id} has no evidence query") if guidance is None or rationale is None: raise ValueError( f"proposal {proposal_id} guidance/rationale contains instance numbers" ) proposal: dict[str, Any] = { "id": proposal_id, "kind": kind, "scope": scopes, "evidence_query": { "required_features": required_features, "required_relations": required_relations, "context_features": context_features, }, "guidance": guidance, "semantic_rationale": rationale, } if observation_name != "unknown": proposal["observation_name"] = observation_name if kind == "reconstruction_grammar": grammar = normalize_reconstruction(raw.get("reconstruction_grammar")) if ( not grammar["parameter_roles"] or not grammar["datum_roles"] or not grammar["canonical_stages"] or not grammar["validation_roles"] ): raise ValueError( f"proposal {proposal_id} has an incomplete reconstruction grammar" ) proposal["reconstruction_grammar"] = grammar for key in ("check", "repair", "failure_signature"): value = string_without_instance_numbers(raw.get(key)) if value is not None: proposal[key] = value proposals.append(proposal) if not proposals: raise ValueError("draft contains no proposals") return proposals def build_reviewed_library( cases: list[dict[str, Any]], proposals: list[dict[str, Any]], min_support: int, min_confidence: float, stats: dict[str, int], ) -> dict[str, Any]: promoted: list[dict[str, Any]] = [] candidates: list[dict[str, Any]] = [] family_counts = Counter(case["family"] for case in cases) def supports_reconstruction_grammar( case: dict[str, Any], grammar: dict[str, Any] ) -> bool: evidence = case.get("reconstruction", {}) for key in ( "parameter_roles", "datum_roles", "feature_roles", "relation_roles", "validation_roles", ): required = set(grammar.get(key, [])) available = set(evidence.get(key, [])) if required and not required.issubset(available): return False available_stages = { (item["id"], item["operation"]): item for item in evidence.get("canonical_stages", []) } for required_stage in grammar.get("canonical_stages", []): key = (required_stage["id"], required_stage["operation"]) available = available_stages.get(key) if available is None: return False if not set(required_stage.get("feature_roles", [])).issubset( available.get("feature_roles", []) ): return False if not set(required_stage.get("reference_roles", [])).issubset( available.get("reference_roles", []) ): return False return True for proposal in proposals: scope = set(proposal["scope"]) relevant = [ case for case in cases if "global" in scope or case["family"] in scope ] required_features = set( proposal["evidence_query"]["required_features"] ) required_relations = set( proposal["evidence_query"]["required_relations"] ) context_features = set( proposal["evidence_query"].get("context_features", required_features) ) if proposal["kind"] == "dimensionless_distribution": matched = [ observation for case in relevant for observation in case["normalized_observations"] if observation["name"] == proposal.get("observation_name") ] support = len(matched) item: dict[str, Any] = { "id": proposal["id"], "kind": proposal["kind"], "scope": proposal["scope"], "when": { "ratio": { "numerator_role": ( matched[0]["numerator_role"] if matched else "unknown" ), "denominator_role": ( matched[0]["denominator_role"] if matched else "unknown" ), } }, "guidance": proposal["guidance"], "semantic_rationale": proposal["semantic_rationale"], "support": support, "confidence": 1.0 if support else 0.0, } if matched: values = [observation["value"] for observation in matched] item["distribution"] = { "sample_count": support, "min": round(min(values), 6), "p10": round(percentile(values, 0.10), 6), "median": round(median(values), 6), "p90": round(percentile(values, 0.90), 6), "max": round(max(values), 6), } if eligible(support, item["confidence"], min_support, min_confidence): promoted.append(item) else: item.pop("distribution", None) item.update( { "promotion_state": "candidate", "required_support": min_support, "remaining_support": max(0, min_support - support), "required_confidence": min_confidence, "consumer_policy": "visible_for_review_but_not_available_to_cad_router", } ) candidates.append(item) continue contextual = [ case for case in relevant if context_features.issubset(case["features"]) ] supporting = [ case for case in contextual if required_features.issubset(case["features"]) and required_relations.issubset(case["relations"]) and ( proposal["kind"] != "reconstruction_grammar" or supports_reconstruction_grammar( case, proposal["reconstruction_grammar"] ) ) ] support = len(supporting) confidence = support / len(contextual) if contextual else 0.0 item: dict[str, Any] = { "id": proposal["id"], "kind": proposal["kind"], "scope": proposal["scope"], "when": { "features": sorted(required_features), "relations": sorted(required_relations), }, "guidance": proposal["guidance"], "semantic_rationale": proposal["semantic_rationale"], "support": support, "confidence": round(confidence, 6), } for key in ("check", "repair", "failure_signature"): if key in proposal: item[key] = proposal[key] if proposal["kind"] == "reconstruction_grammar": item["reconstruction_grammar"] = proposal["reconstruction_grammar"] if eligible(support, confidence, min_support, min_confidence): promoted.append(item) else: item.update( { "promotion_state": "candidate", "required_support": min_support, "remaining_support": max(0, min_support - support), "required_confidence": min_confidence, "consumer_policy": "visible_for_review_but_not_available_to_cad_router", } ) candidates.append(item) promoted.sort(key=lambda item: item["id"]) candidates.sort(key=lambda item: item["id"]) kind_counts = Counter(item["kind"] for item in promoted) candidate_kind_counts = Counter(item["kind"] for item in candidates) scope_counts = Counter( scope for item in promoted for scope in item.get("scope", ["global"]) ) return { "schema_version": SCHEMA_VERSION, "library_kind": "generalized_cad_experience", "generated_at": utc_now(), "induction_mode": "llm_proposals_with_deterministic_evidence_verification", "status": "ready" if promoted else "collecting_evidence", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, "minimum_support": min_support, "minimum_confidence": min_confidence, }, "corpus_summary": { "accepted_case_count": len(cases), "duplicate_case_count": stats["duplicate_case_count"], "rejected_case_count": stats["rejected_case_count"], "family_count": len(family_counts), }, "experience_summary": { "llm_proposal_count": len(proposals), "promoted_experience_count": len(promoted), "candidate_experience_count": len(candidates), "by_kind": [ {"kind": kind, "count": count} for kind, count in sorted(kind_counts.items()) ], "by_scope": [ {"scope": scope, "count": count} for scope, count in sorted(scope_counts.items()) ], "candidate_by_kind": [ {"kind": kind, "count": count} for kind, count in sorted(candidate_kind_counts.items()) ], }, "experiences": promoted, "candidate_experiences": candidates, } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) init_parser = subparsers.add_parser("init") init_parser.add_argument("output", type=Path) induce_parser = subparsers.add_parser("induce") induce_parser.add_argument("input", type=Path) induce_parser.add_argument("--output", type=Path, required=True) induce_parser.add_argument("--min-support", type=int, default=20) induce_parser.add_argument("--min-confidence", type=float, default=0.8) query_parser = subparsers.add_parser("query") query_parser.add_argument("library", type=Path) query_parser.add_argument("--family") query_parser.add_argument("--features", default="") query_parser.add_argument("--output", type=Path) audit_parser = subparsers.add_parser("audit") audit_parser.add_argument("library", type=Path) extract_parser = subparsers.add_parser("extract") extract_parser.add_argument("source", type=Path) extract_parser.add_argument("--output", type=Path, required=True) extract_folder_parser = subparsers.add_parser("extract-folder") extract_folder_parser.add_argument( "--input", type=Path, default=parser_root() / "input" ) extract_folder_parser.add_argument( "--output", type=Path, default=parser_root() / "output" ) prepare_parser = subparsers.add_parser("prepare") prepare_parser.add_argument( "--input", type=Path, default=parser_root() / "output" ) prepare_parser.add_argument( "--library", type=Path, default=default_library_path() ) prepare_parser.add_argument( "--output", type=Path, default=default_review_dir() / "semantic-batch.json", ) publish_parser = subparsers.add_parser("publish") publish_parser.add_argument("--draft", type=Path, required=True) publish_parser.add_argument( "--input", type=Path, default=parser_root() / "output" ) publish_parser.add_argument( "--library", type=Path, default=default_library_path() ) publish_parser.add_argument("--min-support", type=int, default=20) publish_parser.add_argument("--min-confidence", type=float, default=0.8) distill_parser = subparsers.add_parser("distill") distill_parser.add_argument( "--draft", type=Path, help="LLM-authored proposal draft. Without this, publishing is blocked.", ) distill_parser.add_argument( "--input", type=Path, default=parser_root() / "output" ) distill_parser.add_argument( "--library", type=Path, default=default_library_path() ) distill_parser.add_argument("--min-support", type=int, default=20) distill_parser.add_argument("--min-confidence", type=float, default=0.8) pipeline_parser = subparsers.add_parser("pipeline") pipeline_parser.add_argument("source", type=Path) pipeline_parser.add_argument( "--case-dir", type=Path, default=parser_root() / "output" ) pipeline_parser.add_argument( "--library", type=Path, default=default_library_path() ) pipeline_parser.add_argument( "--report-dir", type=Path, default=plugin_root() / "work" / "runs" ) pipeline_parser.add_argument("--min-support", type=int, default=20) pipeline_parser.add_argument("--min-confidence", type=float, default=0.8) pipeline_parser.add_argument( "--request", help="Optionally run cad-router on a new request after publishing the library.", ) pipeline_parser.add_argument( "--output-format", action="append", default=["step"] ) pipeline_parser.add_argument( "--manufacturing", choices=("unspecified", "machining", "printing", "laser-cutting", "concept"), default="unspecified", ) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) if args.command == "init": payload = induce( [], 20, 0.8, {"duplicate_case_count": 0, "rejected_case_count": 0}, ) write_json(args.output, payload) print(args.output) return 0 if args.command == "induce": if args.min_support < 2: raise SystemExit("--min-support must be at least 2") if not 0.0 < args.min_confidence <= 1.0: raise SystemExit("--min-confidence must be in (0, 1]") cases, stats = load_cases(args.input) payload = induce(cases, args.min_support, args.min_confidence, stats) errors = audit_library(payload) if errors: raise SystemExit("experience audit failed:\n- " + "\n- ".join(errors)) write_json(args.output, payload) print(args.output) return 0 if args.command == "extract": payload = extract_step_case(args.source) write_json(args.output, payload) print(args.output) return 0 if args.command == "extract-folder": result = extract_folder(args.input, args.output) print(json.dumps(result, indent=2, ensure_ascii=False)) return 1 if result["failed_count"] else 0 if args.command == "prepare": cases, _ = load_cases(args.input) existing_library: dict[str, Any] | None = None if args.library.is_file(): existing_library = json.loads(args.library.read_text(encoding="utf-8")) packet = prepare_semantic_batch(cases, existing_library) write_json(args.output, packet) print( json.dumps( { "semantic_batch": str(args.output.resolve()), "case_count": len(cases), "next_step": ( "Invoke $cad-experience-builder to author an LLM semantic " "proposal draft, then run publish." ), }, indent=2, ensure_ascii=False, ) ) return 0 if args.command in {"publish", "distill"}: if args.command == "distill" and args.draft is None: raise SystemExit( "direct statistical distillation is disabled: invoke " "$cad-experience-builder so the active model reviews the sanitized " "semantic batch, then pass its draft with --draft" ) if args.min_support < 2: raise SystemExit("--min-support must be at least 2") if not 0.0 < args.min_confidence <= 1.0: raise SystemExit("--min-confidence must be in (0, 1]") cases, stats = load_cases(args.input) draft_path = args.draft draft_payload = json.loads(draft_path.read_text(encoding="utf-8")) proposals = normalize_proposals(draft_payload) library = build_reviewed_library( cases, proposals, args.min_support, args.min_confidence, stats, ) errors = audit_library(library) if errors: raise SystemExit("experience audit failed:\n- " + "\n- ".join(errors)) write_json(args.library, library) print( json.dumps( { "input": str(args.input.resolve()), "library": str(args.library.resolve()), "llm_proposal_count": len(proposals), "accepted_case_count": library["corpus_summary"][ "accepted_case_count" ], "promoted_experience_count": len(library["experiences"]), "status": library["status"], }, indent=2, ensure_ascii=False, ) ) return 0 if args.command == "pipeline": raise SystemExit( "pipeline publishing is disabled because it bypasses LLM semantic " "review; use extract-folder, then invoke $cad-experience-builder" ) if args.min_support < 2: raise SystemExit("--min-support must be at least 2") if not 0.0 < args.min_confidence <= 1.0: raise SystemExit("--min-confidence must be in (0, 1]") case_payload = extract_step_case(args.source) case_output = args.case_dir / f"{args.source.stem}.case.json" write_json(case_output, case_payload) cases, stats = load_cases(args.case_dir) library = induce(cases, args.min_support, args.min_confidence, stats) errors = audit_library(library) if errors: raise SystemExit("experience audit failed:\n- " + "\n- ".join(errors)) write_json(args.library, library) route_output: Path | None = None route_summary: dict[str, Any] | None = None if args.request: route_script = ( plugin_root().parent / "text-to-cad" / "skills" / "cad-router" / "scripts" / "route.py" ) route_output = args.report_dir / f"{args.source.stem}.route.json" route_command = [ sys.executable, str(route_script), args.request, "--experience-library", str(args.library), "--manufacturing", args.manufacturing, "--manifest", str(route_output), ] for output_format in args.output_format: route_command.extend(["--output", output_format]) completed = subprocess.run( route_command, check=True, capture_output=True, text=True, ) route_summary = json.loads(completed.stdout) report = { "schema_version": "1.0", "pipeline": "step_to_private_case_to_generalized_library", "source": str(args.source.expanduser().resolve()), "private_case": str(case_output.resolve()), "published_library": str(args.library.resolve()), "accepted_case_count": library["corpus_summary"]["accepted_case_count"], "promoted_experience_count": len(library["experiences"]), "promotion_status": library["status"], "minimum_support": args.min_support, "case_geometry_published": False, "route_manifest": str(route_output.resolve()) if route_output else None, "route": ( { "selected_backend": route_summary["selected_backend"], "part_family": route_summary["design_plan"]["part_family"], "requested_feature_roles": route_summary["design_plan"][ "requested_feature_roles" ], "matched_generalized_method_count": len( route_summary["design_plan"]["generalized_methods"] ), "experience_status": route_summary["design_plan"][ "experience_status" ], } if route_summary else None ), "message": ( "The STEP was distilled into private evidence. No generalized " "experience is published until independent support reaches the threshold." ), } report_output = args.report_dir / f"{args.source.stem}.pipeline.json" write_json(report_output, report) print( json.dumps( { "private_case": str(case_output), "library": str(args.library), "report": str(report_output), "route": str(route_output) if route_output else None, "promoted_experience_count": len(library["experiences"]), }, ensure_ascii=False, ) ) return 0 payload = json.loads(args.library.read_text(encoding="utf-8")) errors = audit_library(payload) if errors: print("experience audit failed:\n- " + "\n- ".join(errors)) return 1 if args.command == "audit": print("experience audit passed") return 0 features = {token(item) for item in args.features.split(",") if item.strip()} result = query_library(payload, args.family, features) rendered = json.dumps(result, indent=2, ensure_ascii=False) + "\n" if args.output: write_json(args.output, result) print(args.output) else: print(rendered, end="") return 0 if __name__ == "__main__": raise SystemExit(main())