1379 lines
51 KiB
Python
Executable File
1379 lines
51 KiB
Python
Executable File
#!/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"(?<![a-zA-Z])[-+]?\d+(?:\.\d+)?")
|
|
FORBIDDEN_KEYS = {
|
|
"parameters",
|
|
"parameter_examples",
|
|
"coordinates",
|
|
"coordinate",
|
|
"center",
|
|
"location",
|
|
"axis_origin",
|
|
"source_sha256",
|
|
"source_path",
|
|
"manifest_path",
|
|
"face_refs",
|
|
"surface_ids",
|
|
"evidence",
|
|
"diameter",
|
|
"radius",
|
|
"height",
|
|
"length",
|
|
"depth",
|
|
"spacing",
|
|
"pcd",
|
|
}
|
|
ALLOWED_NUMERIC_KEYS = {
|
|
"support",
|
|
"confidence",
|
|
"frequency",
|
|
"sample_count",
|
|
"accepted_case_count",
|
|
"duplicate_case_count",
|
|
"rejected_case_count",
|
|
"family_count",
|
|
"minimum_support",
|
|
"minimum_confidence",
|
|
"p10",
|
|
"median",
|
|
"p90",
|
|
"min",
|
|
"max",
|
|
"count",
|
|
"promoted_experience_count",
|
|
"candidate_experience_count",
|
|
"required_support",
|
|
"remaining_support",
|
|
"required_confidence",
|
|
"llm_proposal_count",
|
|
}
|
|
|
|
|
|
def utc_now() -> 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())
|