2 Commits

Author SHA1 Message Date
ganjihong dbc18429bb ties_to_requirement 这个指标忘记加到intent里面 2026-09-16 14:29:11 +08:00
ganjihong 273b4b5c7b cdsl新增一些笼统的指标集 2026-09-16 14:06:44 +08:00
5 changed files with 237 additions and 3 deletions
@@ -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
+10 -1
View File
@@ -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")
+154
View File
@@ -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
+62 -1
View File
@@ -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": {