Add model-reviewed CAD experience pipeline

This commit is contained in:
Jerry
2026-07-23 14:08:17 +08:00
parent 32ee79e0d1
commit 304911a69f
274 changed files with 6546 additions and 36578 deletions
@@ -0,0 +1,26 @@
{
"name": "cad-experience-plugin",
"version": "0.1.0",
"description": "Parse STEP inboxes and use the active model plus deterministic evidence checks to distill backend-neutral CAD experience.",
"author": {
"name": "Local developer"
},
"skills": "./skills/",
"interface": {
"displayName": "CAD Experience",
"shortDescription": "Use model review to distill reusable CAD methods.",
"longDescription": "CAD Experience maintains a visible STEP input/output staging area, gives the active large model a sanitized semantic review packet, verifies every proposed method against the accumulated corpus, and promotes only repeatedly supported backend-neutral experience.",
"developerName": "Local developer",
"category": "Productivity",
"capabilities": [
"STEP inbox batch extraction",
"Visible private case JSON",
"Batch CAD experience induction",
"Large-model semantic review",
"Deterministic proposal evidence verification",
"Generalized experience queries",
"Instance-data leakage audits"
],
"defaultPrompt": "Parse the STEP input folder, distill the output JSON, and report promoted generalized CAD experience without exposing source-part geometry."
}
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORKSPACE_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
PYTHON_BIN="${CAD_EXPERIENCE_PYTHON:-}"
if [[ -z "$PYTHON_BIN" && -x "$WORKSPACE_ROOT/text-to-cad/.venv/bin/python" ]]; then
PYTHON_BIN="$WORKSPACE_ROOT/text-to-cad/.venv/bin/python"
fi
if [[ -z "$PYTHON_BIN" ]]; then
PYTHON_BIN="python3"
fi
exec "$PYTHON_BIN" "$PLUGIN_ROOT/skills/cad-experience-builder/scripts/cad_experience.py" "$@"
@@ -0,0 +1,119 @@
---
name: cad-experience-builder
description: Batch-extract STEP files into private JSON, use the active large model to semantically distill transferable CAD methods from a sanitized batch, and deterministically verify and publish a backend-neutral experience library. Use when distilling STEP/STP corpora, reviewing candidate methods, inducing feature motifs and constraints, auditing instance-data leakage, or preparing promoted experience for cad-router and CAD backends.
---
# CAD Experience Builder
Induce methods, not copied answers. Keep source-part evidence outside the
published library and require repeated support before promoting any experience.
## Required workflow
1. Put source `.step` or `.stp` files in `parser/input/`.
2. Parse every new, content-unique STEP into visible private JSON under
`parser/output/`:
```bash
../../scripts/cad-experience extract-folder
```
3. Prepare a sanitized semantic batch:
```bash
../../scripts/cad-experience prepare
```
4. Read `work/review/semantic-batch.json` completely. Act as the semantic
distiller: compare families, features, relations, prior vocabulary, failure
rules, and validation targets; then author
`work/review/experience-draft.json` using
`references/llm-review-contract.md`.
Propose methods and invariants, not raw co-occurrence pairs. Do not infer
operation order, manufacturing intent, or failure causes that final B-Rep
evidence cannot support.
5. Publish only after the model draft exists:
```bash
../../scripts/cad-experience publish \
--draft ../../work/review/experience-draft.json
```
The publisher recomputes support from the private corpus, rejects numeric
instance answers, keeps weak proposals as candidates, and marks the library
router-consumable only after this verification.
6. Audit the public library. Treat any failure as a hard publishing failure:
```bash
python scripts/cad_experience.py audit \
/path/to/cad-experience-library/library.json
```
7. Query only the published library for creation, modification, or rebuilding:
```bash
python scripts/cad_experience.py query \
/path/to/cad-experience-library/library.json \
--family flanged_hub_adapter \
--features base_flange,hollow_sleeve,counterbore
```
8. Pass the query result, never `parser/input`, `parser/output`, or a case JSON,
to `$cad-router` or a backend.
Direct `distill` without an LLM-authored draft is intentionally blocked. The
active Codex model supplies semantic judgment; no separate web API key is
required.
## STEP extraction boundary
- The extractor reads analytic B-Rep surfaces, bounding boxes, topology counts,
axes, radii, and locations into a private evidence record.
- It derives only semantic candidates such as rotational stacks, axis
relationships, transitions, and repeated-hole motifs.
- Exact source paths, hashes, dimensions, coordinates, and face references are
permitted in private case JSON and forbidden in the published library.
- Keep input/output as a human-visible staging area and induction corpus. Never
use it directly during creation, modification, or reconstruction.
- A semantic candidate is not a trusted design method until repeated,
independently sourced cases satisfy the promotion policy.
## Promotion rules
- One case is evidence, not experience.
- Publish generalized, dimension-free observations below threshold as visible
`candidate_experiences`, but never expose them to generation consumers.
- Require `min_support` independent cases and `min_confidence` before promotion.
- Promote feature compositions, semantic constraints, dimensionless
distributions, validation checks, and failure-repair methods.
- Reject absolute dimensions, coordinates, face references, source paths,
source hashes, and full parameter dictionaries from published experience.
- Keep generated examples out of the trusted corpus until independently
validated.
- Evaluate generalization on held-out cases that were not used for induction.
## Backend contract
The library is backend-neutral. A consumer receives:
- `feature_motif`: reusable compositions of feature types;
- `constraint`: invariant semantic relationships;
- `design_rule`: conditional modeling guidance;
- `dimensionless_distribution`: multi-case ratio ranges;
- `validation_rule`: checks independent of one part's coordinates;
- `failure_repair`: reusable detection and repair methods.
build123d, SimpleCADAPI, or another backend translates the same queried
experience into its own operations. Experience must never contain executable
teacher geometry.
## References
- Read `references/case-input-contract.md` before producing batch input.
- Read `references/experience-library-schema.md` before extending the output
schema or adding a consumer.
- Read `references/llm-review-contract.md` before authoring or revising a
semantic proposal draft.
@@ -0,0 +1,4 @@
interface:
display_name: "CAD Experience Builder"
short_description: "用大模型语义复核与证据门禁蒸馏可迁移的 CAD 通用经验"
default_prompt: "Use $cad-experience-builder to prepare sanitized STEP evidence, perform model-led semantic induction, publish only evidence-verified generalized CAD experience, and report candidate versus promoted results."
@@ -0,0 +1,54 @@
# Case JSON input contract
Case JSON is private evidence used only during induction. It may contain exact
parameters, but the inducer never copies those fields into published experience.
Store source STEP files in `cad-experience-plugin/parser/input` and generated
case JSON in `cad-experience-plugin/parser/output`.
Do not pass either folder or a case JSON to cad-router or a CAD backend.
Creation, modification, and reconstruction may consume only the promoted public
experience library.
Supported fields:
```json
{
"provenance": {"source_sha256": "..."},
"design_ir": {
"part_family": "flanged_hub_adapter",
"features": [{"type": "base_flange"}, {"type": "hollow_sleeve"}],
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
"normalized_observations": [
{
"name": "sleeve_od_to_base_od",
"value": 0.35,
"numerator_role": "sleeve_outer_diameter",
"denominator_role": "base_outer_diameter"
}
]
},
"experience": {
"rules": [
{
"id": "bound_counterbore_to_host",
"scope": ["counterbore"],
"statement": "Terminate a counterbore at its host boundary.",
"repair": "Bind the cutter extent to the host feature."
}
],
"validation_targets": [
{
"id": "counterbore_host_boundary",
"check": "A counterbore does not cross an unrelated adjacent feature."
}
]
}
}
```
Rules and checks containing numeric literals are rejected because they are
likely instance answers. Ratios are accepted only through
`normalized_observations`, with an explicit numerator and denominator role.
Legacy distilled JSON can be used when it contains the fields above. Exact
`parameters`, `evidence`, centers, axes, and surface references are ignored.
@@ -0,0 +1,64 @@
# Published experience library
The published library uses schema version `2.0` and contains only promoted,
generalized experience:
```json
{
"schema_version": "2.0",
"library_kind": "generalized_cad_experience",
"induction_mode": "llm_proposals_with_deterministic_evidence_verification",
"policy": {
"instance_parameters_allowed": false,
"absolute_coordinates_allowed": false,
"llm_semantic_review_required": true,
"draft_evidence_verified": true,
"router_consumable": true,
"minimum_support": 20
},
"corpus_summary": {
"accepted_case_count": 1000,
"duplicate_case_count": 12
},
"experience_summary": {
"llm_proposal_count": 24,
"promoted_experience_count": 18,
"candidate_experience_count": 6,
"by_kind": [{"kind": "feature_motif", "count": 7}],
"by_scope": [{"scope": "flanged_hub_adapter", "count": 5}]
},
"experiences": [
{
"id": "motif.base_flange+hollow_sleeve",
"kind": "feature_motif",
"scope": ["flanged_hub_adapter"],
"when": {"features": ["base_flange", "hollow_sleeve"]},
"guidance": "Treat these feature roles as a reusable composition.",
"support": 243,
"confidence": 0.91
}
],
"candidate_experiences": [
{
"id": "motif.flanged_hub_adapter.base_flange+hollow_sleeve",
"kind": "feature_motif",
"scope": ["flanged_hub_adapter"],
"when": {"features": ["base_flange", "hollow_sleeve"]},
"support": 7,
"required_support": 20,
"remaining_support": 13,
"promotion_state": "candidate",
"consumer_policy": "visible_for_review_but_not_available_to_cad_router"
}
]
}
```
Allowed numeric values describe evidence strength or dimensionless
distributions: support counts, confidence, frequency, percentiles, and sample
counts. Absolute lengths, diameters, heights, radii, angles tied to a single
case, positions, and coordinates are forbidden.
Consumers query this library by requested family and feature vocabulary. They
must not receive the induction corpus, private case JSON, or
`candidate_experiences`. Only `experiences` is executable guidance.
@@ -0,0 +1,45 @@
# LLM semantic review contract
Read the sanitized `work/review/semantic-batch.json`, not raw STEP geometry.
Maintain stable proposal identifiers when an existing candidate expresses the
same method.
Write `work/review/experience-draft.json` with this shape:
```json
{
"schema_version": "1.0",
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [
{
"id": "constraint.part_family.shared_axis",
"kind": "constraint",
"scope": ["part_family"],
"evidence_query": {
"required_features": ["rotational_body", "central_passage"],
"required_relations": ["dominant_axis_alignment"]
},
"guidance": "Preserve a shared axis across the participating feature roles.",
"semantic_rationale": "The relationship remains meaningful when individual dimensions change.",
"check": "Confirm that participating features retain their intended common axis."
}
]
}
```
Allowed kinds are `feature_motif`, `constraint`, `design_rule`,
`validation_rule`, and `failure_repair`.
Follow these rules:
- Require an evidence query composed only of semantic feature and relation
tokens present in the sanitized batch.
- Use scopes to prevent unrelated part families from supporting a proposal.
- Explain why a proposal transfers across dimensions and variants.
- Avoid dimensions, coordinates, source identifiers, face references, and
numeric literals in prose.
- Do not infer build order or causal failure mechanisms from final geometry.
- Prefer a small set of meaningful methods over every possible feature pair.
- Preserve earlier proposal IDs when evidence still supports the same meaning.
- Let the publisher calculate support and confidence; never claim them in the
draft.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""Extract a private, backend-neutral case record from an analytic STEP model."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
def _rounded(value: float) -> float:
return round(float(value), 6)
def _component(value: Any, name: str) -> float:
component = getattr(value, name)
return float(component() if callable(component) else component)
def _point(value: Any) -> list[float]:
return [_rounded(_component(value, name)) for name in ("X", "Y", "Z")]
def _direction(value: Any) -> list[float]:
return _point(value)
def _canonical_axis(direction: list[float]) -> str:
index = max(range(3), key=lambda item: abs(direction[item]))
return ("x", "y", "z")[index]
def _radial_offset(location: list[float], center: list[float], axis: str) -> float:
axes = {"x": (1, 2), "y": (0, 2), "z": (0, 1)}
first, second = axes[axis]
return math.hypot(
location[first] - center[first],
location[second] - center[second],
)
def _surface_record(face: Any, index: int) -> dict[str, Any]:
from OCP.BRepAdaptor import BRepAdaptor_Surface
from OCP.GeomAbs import (
GeomAbs_Cone,
GeomAbs_Cylinder,
GeomAbs_Plane,
GeomAbs_Sphere,
GeomAbs_Torus,
)
adaptor = BRepAdaptor_Surface(face.wrapped)
kind = adaptor.GetType()
record: dict[str, Any] = {
"surface_id": f"face_{index}",
"area": _rounded(face.area),
"center": _point(face.center()),
}
if kind == GeomAbs_Plane:
plane = adaptor.Plane()
record.update(
{
"type": "plane",
"location": _point(plane.Location()),
"axis": _direction(plane.Axis().Direction()),
}
)
elif kind == GeomAbs_Cylinder:
cylinder = adaptor.Cylinder()
record.update(
{
"type": "cylinder",
"location": _point(cylinder.Location()),
"axis": _direction(cylinder.Axis().Direction()),
"radius": _rounded(cylinder.Radius()),
}
)
elif kind == GeomAbs_Cone:
cone = adaptor.Cone()
record.update(
{
"type": "cone",
"location": _point(cone.Location()),
"axis": _direction(cone.Axis().Direction()),
"reference_radius": _rounded(cone.RefRadius()),
"semi_angle_radians": _rounded(cone.SemiAngle()),
}
)
elif kind == GeomAbs_Sphere:
sphere = adaptor.Sphere()
record.update(
{
"type": "sphere",
"location": _point(sphere.Location()),
"radius": _rounded(sphere.Radius()),
}
)
elif kind == GeomAbs_Torus:
torus = adaptor.Torus()
record.update(
{
"type": "torus",
"location": _point(torus.Location()),
"axis": _direction(torus.Axis().Direction()),
"major_radius": _rounded(torus.MajorRadius()),
"minor_radius": _rounded(torus.MinorRadius()),
}
)
else:
record["type"] = "other"
return record
def _semantic_summary(
surfaces: list[dict[str, Any]], bbox_center: list[float], bbox_size: list[float]
) -> tuple[str, list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
cylinders = [item for item in surfaces if item["type"] == "cylinder"]
cones = [item for item in surfaces if item["type"] == "cone"]
axis_votes: Counter[str] = Counter()
for surface in cylinders + cones:
axis_votes[_canonical_axis(surface["axis"])] += max(surface["area"], 1.0)
dominant_axis = axis_votes.most_common(1)[0][0] if axis_votes else "z"
scale = max(bbox_size) if bbox_size else 1.0
coaxial_tolerance = max(scale * 0.005, 1e-4)
parallel_cylinders: list[dict[str, Any]] = []
coaxial_cylinders: list[dict[str, Any]] = []
off_axis_cylinders: list[dict[str, Any]] = []
for surface in cylinders:
if _canonical_axis(surface["axis"]) != dominant_axis:
continue
enriched = dict(surface)
enriched["radial_offset"] = _rounded(
_radial_offset(surface["location"], bbox_center, dominant_axis)
)
parallel_cylinders.append(enriched)
if enriched["radial_offset"] <= coaxial_tolerance:
coaxial_cylinders.append(enriched)
else:
off_axis_cylinders.append(enriched)
distinct_coaxial_radii = {
round(surface["radius"], 4) for surface in coaxial_cylinders
}
repeated_groups: dict[float, list[dict[str, Any]]] = defaultdict(list)
for surface in off_axis_cylinders:
repeated_groups[round(surface["radius"], 4)].append(surface)
repeated_group_sizes = sorted(
(len(rows) for rows in repeated_groups.values()), reverse=True
)
parallel_cones = [
item for item in cones if _canonical_axis(item["axis"]) == dominant_axis
]
has_rotational_stack = len(distinct_coaxial_radii) >= 2
has_repeated_axial_holes = bool(repeated_group_sizes and repeated_group_sizes[0] >= 3)
has_multi_axis_passages = any(
_canonical_axis(item["axis"]) != dominant_axis for item in cylinders
)
features: list[dict[str, Any]] = []
if has_rotational_stack:
features.extend(
[
{"type": "rotational_body"},
{"type": "coaxial_cylindrical_stack"},
]
)
if len(distinct_coaxial_radii) >= 3:
features.append({"type": "central_passage_candidate"})
if parallel_cones:
features.append({"type": "conical_transition"})
if has_repeated_axial_holes:
features.append({"type": "repeated_axial_hole_pattern"})
if has_multi_axis_passages:
features.append({"type": "multi_axis_passage"})
constraints: list[dict[str, Any]] = []
if has_rotational_stack:
constraints.extend(
[
{"id": "dominant_axis_alignment", "type": "axis_alignment"},
{"id": "coaxial_stack", "type": "coaxial"},
]
)
if has_repeated_axial_holes:
constraints.append(
{"id": "repeated_radius_group", "type": "repeated_feature"}
)
if parallel_cones:
constraints.append(
{"id": "transition_axis_continuity", "type": "axis_alignment"}
)
if has_rotational_stack and parallel_cones and has_repeated_axial_holes:
family = "flanged_hub_adapter_candidate"
elif has_rotational_stack:
family = "rotational_part_candidate"
elif has_repeated_axial_holes:
family = "patterned_prismatic_part_candidate"
else:
family = "unclassified_part"
summary = {
"dominant_axis": dominant_axis,
"coaxial_cylinder_surface_count": len(coaxial_cylinders),
"distinct_coaxial_radius_count": len(distinct_coaxial_radii),
"off_axis_parallel_cylinder_surface_count": len(off_axis_cylinders),
"largest_repeated_radius_group": repeated_group_sizes[0]
if repeated_group_sizes
else 0,
"parallel_cone_surface_count": len(parallel_cones),
"has_multi_axis_passages": has_multi_axis_passages,
}
return family, features, constraints, summary
def extract_step_case(source: Path) -> dict[str, Any]:
from build123d import import_step
resolved = source.expanduser().resolve()
source_bytes = resolved.read_bytes()
shape = import_step(str(resolved))
bbox = shape.bounding_box()
bbox_min = _point(bbox.min)
bbox_max = _point(bbox.max)
bbox_size = _point(bbox.size)
bbox_center = [
_rounded((lower + upper) / 2.0)
for lower, upper in zip(bbox_min, bbox_max)
]
surfaces = [
_surface_record(face, index) for index, face in enumerate(shape.faces())
]
surface_counts = Counter(surface["type"] for surface in surfaces)
family, features, constraints, semantic_summary = _semantic_summary(
surfaces, bbox_center, bbox_size
)
digest = hashlib.sha256(source_bytes).hexdigest()
return {
"schema_version": "1.0",
"case_kind": "private_step_evidence",
"case_id": digest,
"provenance": {
"source_path": str(resolved),
"source_sha256": digest,
"source_format": "step",
},
"geometry_evidence": {
"valid": bool(shape.is_valid),
"solid_count": len(shape.solids()),
"face_count": len(shape.faces()),
"volume": _rounded(shape.volume),
"bounding_box": {
"min": bbox_min,
"max": bbox_max,
"size": bbox_size,
"center": bbox_center,
},
"analytic_surface_counts": dict(sorted(surface_counts.items())),
"analytic_surfaces": surfaces,
},
"design_ir": {
"part_family": family,
"features": features,
"constraints": constraints,
"normalized_observations": [],
"semantic_summary": semantic_summary,
},
"experience": {
"rules": [],
"validation_targets": [],
"promotion_state": "evidence_only",
},
}
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 main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Extract a private case JSON from a STEP file."
)
parser.add_argument("source", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
write_json(args.output, extract_step_case(args.source))
print(args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,256 @@
from __future__ import annotations
import importlib.util
import json
import unittest
from pathlib import Path
SCRIPT = (
Path(__file__).parents[1]
/ "skills"
/ "cad-experience-builder"
/ "scripts"
/ "cad_experience.py"
)
SPEC = importlib.util.spec_from_file_location("cad_experience", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(MODULE)
STEP_SCRIPT = (
Path(__file__).parents[1]
/ "skills"
/ "cad-experience-builder"
/ "scripts"
/ "step_to_case.py"
)
STEP_SPEC = importlib.util.spec_from_file_location("step_to_case", STEP_SCRIPT)
STEP_MODULE = importlib.util.module_from_spec(STEP_SPEC)
assert STEP_SPEC.loader
STEP_SPEC.loader.exec_module(STEP_MODULE)
def make_case(index: int) -> dict:
return {
"provenance": {"source_sha256": f"case_{index}"},
"design_ir": {
"part_family": "flanged_hub_adapter",
"features": [
{"type": "base_flange", "center": [91.0, 52.0]},
{"type": "hollow_sleeve", "surface_ids": ["face_1"]},
{"type": "counterbore"},
],
"constraints": [{"id": "coaxial_stack", "type": "coaxial"}],
"parameters": {
"base_outer_diameter": 91.0,
"counterbore_spacing": 52.0,
},
"normalized_observations": [
{
"name": "sleeve_od_to_base_od",
"value": 0.3 + index * 0.001,
"numerator_role": "sleeve_outer_diameter",
"denominator_role": "base_outer_diameter",
}
],
},
"experience": {
"rules": [
{
"id": "bound_counterbore_to_host",
"scope": ["flanged_hub_adapter"],
"statement": "Terminate a counterbore at its host boundary.",
"repair": "Bind the cutter extent to the host feature.",
}
],
"validation_targets": [
{
"id": "host_boundary",
"check": "A counterbore does not cross an unrelated adjacent feature.",
}
],
},
}
class CadExperienceTest(unittest.TestCase):
def reviewed_proposal(self) -> dict:
return {
"id": "constraint.flanged_hub_adapter.shared_axis",
"kind": "constraint",
"scope": ["flanged_hub_adapter"],
"evidence_query": {
"required_features": ["base_flange", "hollow_sleeve"],
"required_relations": ["coaxial_stack"],
},
"guidance": "Preserve a shared axis across participating feature roles.",
"semantic_rationale": "The relationship transfers across dimensional variants.",
}
def test_semantic_summary_derives_roles_without_copying_dimensions(self):
surfaces = [
{
"type": "cylinder",
"axis": [0.0, 1.0, 0.0],
"location": [0.0, 0.0, 0.0],
"radius": radius,
"area": 100.0,
}
for radius in (5.0, 10.0, 20.0)
]
surfaces.extend(
{
"type": "cylinder",
"axis": [0.0, 1.0, 0.0],
"location": [offset, 0.0, 0.0],
"radius": 2.0,
"area": 20.0,
}
for offset in (-15.0, 15.0, 16.0)
)
surfaces.append(
{
"type": "cone",
"axis": [0.0, 1.0, 0.0],
"location": [0.0, 0.0, 0.0],
"reference_radius": 10.0,
"semi_angle_radians": 0.5,
"area": 50.0,
}
)
family, features, constraints, summary = STEP_MODULE._semantic_summary(
surfaces, [0.0, 0.0, 0.0], [40.0, 20.0, 40.0]
)
self.assertEqual("flanged_hub_adapter_candidate", family)
self.assertIn(
"coaxial_cylindrical_stack", {item["type"] for item in features}
)
self.assertIn("coaxial_stack", {item["id"] for item in constraints})
self.assertEqual("y", summary["dominant_axis"])
def test_single_case_never_promotes(self):
case = MODULE.normalize_case(make_case(1))
library = MODULE.induce(
[case],
2,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(library["status"], "collecting_evidence")
self.assertEqual(library["experiences"], [])
self.assertTrue(library["candidate_experiences"])
self.assertTrue(
all(
item["promotion_state"] == "candidate"
for item in library["candidate_experiences"]
)
)
self.assertTrue(
all(
item["consumer_policy"]
== "visible_for_review_but_not_available_to_cad_router"
for item in library["candidate_experiences"]
)
)
def test_batch_promotes_methods_without_instance_values(self):
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
library = MODULE.induce(
cases,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(MODULE.audit_library(library), [])
rendered = json.dumps(library)
self.assertNotIn('"parameters"', rendered)
self.assertNotIn("91.0", rendered)
self.assertNotIn("52.0", rendered)
self.assertNotIn('"center"', rendered)
ids = {item["id"] for item in library["experiences"]}
self.assertIn("rule.bound_counterbore_to_host", ids)
self.assertIn("constraint.flanged_hub_adapter.coaxial_stack", ids)
self.assertIn("distribution.sleeve_od_to_base_od", ids)
def test_family_support_is_not_diluted_by_unrelated_families(self):
target = [MODULE.normalize_case(make_case(index)) for index in range(20)]
unrelated = []
for index in range(100):
payload = make_case(index + 1000)
payload["design_ir"]["part_family"] = "gear"
payload["design_ir"]["features"] = [
{"type": "gear_blank"},
{"type": "gear_teeth"},
]
unrelated.append(MODULE.normalize_case(payload))
library = MODULE.induce(
target + unrelated,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
ids = {item["id"] for item in library["experiences"]}
self.assertIn(
"motif.flanged_hub_adapter.base_flange+hollow_sleeve", ids
)
def test_query_returns_backend_neutral_context(self):
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
library = MODULE.induce(
cases,
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
result = MODULE.query_library(
library,
"flanged_hub_adapter",
{"base_flange", "hollow_sleeve", "counterbore"},
)
self.assertTrue(result["experiences"])
self.assertFalse(result["policy"]["contains_instance_parameters"])
def test_llm_proposal_is_candidate_until_evidence_threshold(self):
proposal = self.reviewed_proposal()
one_case = MODULE.normalize_case(make_case(1))
candidate_library = MODULE.build_reviewed_library(
[one_case],
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual([], candidate_library["experiences"])
self.assertEqual(1, len(candidate_library["candidate_experiences"]))
self.assertTrue(candidate_library["policy"]["router_consumable"])
cases = [MODULE.normalize_case(make_case(index)) for index in range(20)]
promoted_library = MODULE.build_reviewed_library(
cases,
[proposal],
20,
0.8,
{"duplicate_case_count": 0, "rejected_case_count": 0},
)
self.assertEqual(1, len(promoted_library["experiences"]))
self.assertEqual([], promoted_library["candidate_experiences"])
def test_numeric_instance_answer_is_rejected_from_llm_draft(self):
proposal = self.reviewed_proposal()
proposal["guidance"] = "Always use a diameter of 91 millimeters."
with self.assertRaisesRegex(ValueError, "instance numbers"):
MODULE.normalize_proposals(
{
"draft_kind": "llm_generalized_experience_proposals",
"proposals": [proposal],
}
)
def test_direct_distill_without_model_draft_is_blocked(self):
with self.assertRaisesRegex(SystemExit, "statistical distillation is disabled"):
MODULE.main(["distill"])
if __name__ == "__main__":
unittest.main()