Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbc18429bb | |||
| 273b4b5c7b |
@@ -157,6 +157,7 @@ Feature level — attach `intent` to every feature:
|
||||
"label": "shaft_passage",
|
||||
"summary": "Ø45 central through bore, concentric with the outer contour",
|
||||
"why": "The fitting face of the sleeve; diameter follows the mating shaft",
|
||||
"ties_to_requirement": "R2",
|
||||
"provenance": "authored"
|
||||
}
|
||||
}
|
||||
@@ -178,10 +179,18 @@ Feature level — attach `intent` to every feature:
|
||||
Use parameterized wording (`M8`, `Ø75`, `R8`), never a restatement of the
|
||||
request prose.
|
||||
- `why` (optional, ≤400 characters): the functional reason this feature exists.
|
||||
- `ties_to_requirement` (optional): the requirement id from the task's
|
||||
requirements contract that this feature implements. Include it whenever the
|
||||
task carries requirement ids and the feature plainly implements one of them;
|
||||
omit it when the task has no requirement ids. Never invent an id.
|
||||
- `provenance`: always `"authored"` when the model writes it.
|
||||
- Do not invent fields inside `intent`, and never treat a mismatch between an
|
||||
annotation and geometry as acceptable — the label must match the feature
|
||||
actually constructed.
|
||||
- Geometry metrics (`meta.derived_metrics`: volumes, hole counts, per-feature
|
||||
deltas) are computed and backfilled by the server after rebuild. Never
|
||||
author or estimate them; if a document already carries `derived_metrics`,
|
||||
leave it untouched.
|
||||
|
||||
## Sketches And Coordinates
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.cad_agent.domain.operation_contract import (
|
||||
validate_operation_contract,
|
||||
)
|
||||
from app.cad_agent.ports import AdapterUnavailable
|
||||
from app.services.derived_metrics import derive_metrics, make_prefix_runner
|
||||
from app.services.engine_service import load_engine, topology_snapshot, validate_cdsl, validate_cdsl_shape
|
||||
from app.services.render_bundle import RenderBundleError, render_checkpoint
|
||||
from app.settings import Settings
|
||||
@@ -102,7 +103,6 @@ class ProfileCadRuntime:
|
||||
cdsl_copy = deepcopy(cdsl)
|
||||
cdsl_copy["part_id"] = task_id
|
||||
step_path = root / "model.step"
|
||||
self._write_json(root / "model.cdsl.json", cdsl_copy)
|
||||
try:
|
||||
self._validate_finite_tree(cdsl_copy)
|
||||
validate_cdsl(cdsl_copy, self.engine)
|
||||
@@ -112,6 +112,15 @@ class ProfileCadRuntime:
|
||||
health = self._health(engine_result, step_path)
|
||||
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id)
|
||||
self._write_json(root / "model.topology.json", topology)
|
||||
# Derived metrics are backfilled after validation, so they can
|
||||
# never influence acceptance; the persisted document carries them
|
||||
# for training while rebuild behavior stays untouched.
|
||||
derived = derive_metrics(
|
||||
cdsl_copy, engine_result, topology,
|
||||
prefix_runner=make_prefix_runner(self.engine.run_cdsl_only),
|
||||
)
|
||||
cdsl_copy.setdefault("meta", {})["derived_metrics"] = derived
|
||||
self._write_json(root / "model.cdsl.json", cdsl_copy)
|
||||
report = {"engine_result": engine_result, "preview": {}, "health": health, "render_manifest": {}}
|
||||
self._write_json(root / "rebuild-report.json", report)
|
||||
return {"health": health, "topology": topology, "report": report, "render_manifest": {}, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "topology": "model.topology.json", "report": "rebuild-report.json"}}
|
||||
|
||||
@@ -49,7 +49,8 @@ def validate_operation_contract(contract: dict[str, Any]) -> None:
|
||||
"server_injected_paths", "reference_policy", "semantic_preflight", "candidate_verifiers",
|
||||
"runtime_capability",
|
||||
}
|
||||
if not required.issubset(contract) or set(contract) - required - {"contract_hash", "registry_revision"}:
|
||||
optional = {"contract_hash", "registry_revision", "nested_selector_policies"}
|
||||
if not required.issubset(contract) or set(contract) - required - optional:
|
||||
raise OperationContractError("Operation contract has unknown or missing fields")
|
||||
if not isinstance(contract["atomic_id"], str) or not contract["atomic_id"]:
|
||||
raise OperationContractError("Operation contract atomic_id is invalid")
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Derived-metrics backfill for CDSL documents (training-grade, server-owned).
|
||||
|
||||
The metrics describe the *reconstructed geometry* of a document and are
|
||||
written exclusively by the server after a successful rebuild. Models never
|
||||
author them; the fields are excluded from validation ordering (backfilled
|
||||
after validation) so they can never influence acceptance.
|
||||
|
||||
Counting conventions:
|
||||
- ``hole_count`` : inner cylindrical walls on the final body (cylinder faces
|
||||
whose ``cylinder_role`` is ``inner``). Countersink/counterbore bores add
|
||||
their own walls; no coaxial clustering is applied in v1.
|
||||
- ``fillet_count``: torus faces (rolling-ball blends) on the final body.
|
||||
- ``face_type_distribution``: final-body faces by ``surface_type``.
|
||||
- ``per_feature`` : prefix re-execution sampling. Prefix ``i`` re-runs the
|
||||
first ``i`` features; sampling stops at the first non-executable prefix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.services.engine_service import topology_snapshot
|
||||
|
||||
_METRIC_SNAPSHOT_KEYS = (
|
||||
"volume_mm3", "surface_area_mm2", "bbox_mm", "solid_count",
|
||||
"face_count", "edge_count", "vertex_count", "hole_count",
|
||||
"fillet_count", "face_type_distribution",
|
||||
)
|
||||
_DELTA_KEYS = ("volume_mm3", "surface_area_mm2", "face_count", "hole_count")
|
||||
|
||||
#: Prefix sampling above this feature count degrades to final-only metrics,
|
||||
#: keeping the O(n^2) re-execution cost bounded on very large documents.
|
||||
MAX_PER_FEATURE_SAMPLES = 64
|
||||
|
||||
|
||||
def _face_stats(topology: dict[str, Any]) -> dict[str, Any]:
|
||||
faces = [
|
||||
record for record in (topology.get("records") or [])
|
||||
if isinstance(record, dict) and record.get("kind") == "face"
|
||||
]
|
||||
distribution: Counter[str] = Counter()
|
||||
hole_walls = 0
|
||||
torus = 0
|
||||
for face in faces:
|
||||
geometry = face.get("geometry") or {}
|
||||
surface_type = str(geometry.get("surface_type") or "unknown")
|
||||
distribution[surface_type] += 1
|
||||
if surface_type == "cylinder" and geometry.get("cylinder_role") == "inner":
|
||||
hole_walls += 1
|
||||
if surface_type == "torus":
|
||||
torus += 1
|
||||
return {
|
||||
"face_type_distribution": dict(sorted(distribution.items())),
|
||||
"hole_count": hole_walls,
|
||||
"fillet_count": torus,
|
||||
}
|
||||
|
||||
|
||||
def _snapshot_from_engine_result(engine_result: dict[str, Any], topology: dict[str, Any]) -> dict[str, Any]:
|
||||
records = [r for r in (topology.get("records") or []) if isinstance(r, dict)]
|
||||
faces = [r for r in records if r.get("kind") == "face"]
|
||||
area = sum(float((f.get("geometry") or {}).get("area_mm2") or 0.0) for f in faces)
|
||||
snapshot = {
|
||||
"volume_mm3": float(engine_result.get("volume_mm3") or 0.0),
|
||||
"surface_area_mm2": area or float(engine_result.get("surface_area_mm2") or 0.0),
|
||||
"bbox_mm": [float(value) for value in (engine_result.get("bbox_mm") or {}).get("min", [])]
|
||||
+ [float(value) for value in (engine_result.get("bbox_mm") or {}).get("max", [])],
|
||||
"solid_count": int(engine_result.get("solid_count") or 0),
|
||||
"face_count": len(faces),
|
||||
"edge_count": sum(1 for r in records if r.get("kind") == "edge"),
|
||||
"vertex_count": sum(1 for r in records if r.get("kind") == "vertex"),
|
||||
}
|
||||
snapshot.update(_face_stats(topology))
|
||||
return {key: snapshot[key] for key in _METRIC_SNAPSHOT_KEYS}
|
||||
|
||||
|
||||
def _delta(after: dict[str, Any], before: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"volume_mm3": float(after["volume_mm3"]) - float(before["volume_mm3"]),
|
||||
"surface_area_mm2": float(after["surface_area_mm2"]) - float(before["surface_area_mm2"]),
|
||||
"face_count": int(after["face_count"]) - int(before["face_count"]),
|
||||
"hole_count": int(after["hole_count"]) - int(before["hole_count"]),
|
||||
}
|
||||
|
||||
|
||||
def derive_metrics(
|
||||
cdsl: dict[str, Any],
|
||||
engine_result: dict[str, Any],
|
||||
topology: dict[str, Any],
|
||||
*,
|
||||
engine_build: str = "",
|
||||
prefix_runner: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the ``meta.derived_metrics`` payload for a successfully rebuilt document.
|
||||
|
||||
``prefix_runner`` re-executes a prefix of the document's features and
|
||||
returns the same shape as ``engine_result``; when omitted, ``per_feature``
|
||||
degrades to status entries without geometric snapshots.
|
||||
"""
|
||||
features = cdsl.get("features") or []
|
||||
final = _snapshot_from_engine_result(engine_result, topology)
|
||||
zero = {
|
||||
"volume_mm3": 0.0, "surface_area_mm2": 0.0,
|
||||
"face_count": 0, "hole_count": 0,
|
||||
}
|
||||
|
||||
per_feature: list[dict[str, Any]] = []
|
||||
if prefix_runner is not None and 1 <= len(features) <= MAX_PER_FEATURE_SAMPLES:
|
||||
previous = zero
|
||||
for index in range(1, len(features) + 1):
|
||||
prefix_doc = deepcopy(cdsl)
|
||||
prefix_doc["features"] = deepcopy(features[:index])
|
||||
try:
|
||||
prefix_result = prefix_runner(prefix_doc)
|
||||
topology_prefix = topology_snapshot(prefix_result)
|
||||
after = _snapshot_from_engine_result(prefix_result, topology_prefix)
|
||||
status = str(next(
|
||||
(fr.get("status") for fr in prefix_result.get("feature_results") or []
|
||||
if fr.get("feature_id") == features[index - 1].get("id")),
|
||||
"executed",
|
||||
))
|
||||
except Exception:
|
||||
break # prefix chain broken; keep samples collected so far
|
||||
per_feature.append({
|
||||
"feature_id": str(features[index - 1].get("id") or ""),
|
||||
"atomic_id": str(features[index - 1].get("atomic_id") or ""),
|
||||
"status": status,
|
||||
"after": after,
|
||||
"delta": _delta(after, previous),
|
||||
})
|
||||
previous = after
|
||||
|
||||
histogram = Counter(str(feature.get("atomic_id") or "") for feature in features)
|
||||
metrics: dict[str, Any] = {
|
||||
"engine": str(engine_result.get("engine") or ""),
|
||||
"final": final,
|
||||
"per_feature": per_feature,
|
||||
"atomic_histogram": dict(sorted(histogram.items())),
|
||||
}
|
||||
if engine_build:
|
||||
metrics["engine_build"] = engine_build
|
||||
return metrics
|
||||
|
||||
|
||||
def make_prefix_runner(run_cdsl_only: Callable[[dict[str, Any], Path], dict[str, Any]]):
|
||||
"""Wrap an engine ``run_cdsl_only`` entry point for prefix sampling."""
|
||||
def runner(prefix_doc: dict[str, Any]) -> dict[str, Any]:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
return run_cdsl_only(prefix_doc, Path(tmp) / "prefix.step")
|
||||
return runner
|
||||
@@ -9,7 +9,13 @@
|
||||
"schema_version": {"type": "string"},
|
||||
"kind": {"type": "string", "minLength": 1},
|
||||
"part_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{3,80}$"},
|
||||
"meta": {"type": "object"},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"derived_metrics": {"$ref": "#/$defs/derivedMetrics"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"bodies": {"type": "array", "items": {"$ref": "#/$defs/runtimeBody"}},
|
||||
"geometry": {
|
||||
"type": "object",
|
||||
@@ -24,6 +30,61 @@
|
||||
"required": ["schema", "geometry", "features"],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"metricSnapshot": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"volume_mm3": {"type": "number"},
|
||||
"surface_area_mm2": {"type": "number"},
|
||||
"bbox_mm": {"type": "array", "items": {"type": "number"}, "minItems": 6, "maxItems": 6},
|
||||
"solid_count": {"type": "integer", "minimum": 0},
|
||||
"face_count": {"type": "integer", "minimum": 0},
|
||||
"edge_count": {"type": "integer", "minimum": 0},
|
||||
"vertex_count": {"type": "integer", "minimum": 0},
|
||||
"hole_count": {"type": "integer", "minimum": 0},
|
||||
"fillet_count": {"type": "integer", "minimum": 0},
|
||||
"face_type_distribution": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}}
|
||||
},
|
||||
"required": ["volume_mm3", "surface_area_mm2", "bbox_mm", "solid_count", "face_count", "edge_count", "vertex_count", "hole_count", "fillet_count", "face_type_distribution"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"metricDelta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"volume_mm3": {"type": "number"},
|
||||
"surface_area_mm2": {"type": "number"},
|
||||
"face_count": {"type": "integer"},
|
||||
"hole_count": {"type": "integer"}
|
||||
},
|
||||
"required": ["volume_mm3", "surface_area_mm2", "face_count", "hole_count"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"derivedMetrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"engine": {"type": "string", "minLength": 1},
|
||||
"engine_build": {"type": "string", "minLength": 1},
|
||||
"generated_at": {"type": "string", "minLength": 1},
|
||||
"final": {"$ref": "#/$defs/metricSnapshot"},
|
||||
"per_feature": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"feature_id": {"type": "string", "minLength": 1},
|
||||
"atomic_id": {"$ref": "#/$defs/feature_atomic_ids"},
|
||||
"status": {"type": "string", "minLength": 1},
|
||||
"after": {"$ref": "#/$defs/metricSnapshot"},
|
||||
"delta": {"$ref": "#/$defs/metricDelta"}
|
||||
},
|
||||
"required": ["feature_id", "atomic_id", "status", "after", "delta"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"atomic_histogram": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}}
|
||||
},
|
||||
"required": ["engine", "final", "per_feature", "atomic_histogram"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"featureIntent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user