AI3D-341 — Step 6 dashed/solid labels as source of truth, propagated into the XML

2026-07-15 · Miro / AI3D pipeline · Step 7 (modellinglines) + modellingexport

Summary

Step 6 already writes a per-cluster segment_type scalar ("solid" / "dashed") into each run6_cluster_*.npz. Step 7 today classifies dashed vs solid from cluster geometry (bounding-box heuristic is_middle_lane_bounding_box in filtering-clusters) unless the opt-in flag clustering.prefer_artifact_segment_type is set — and the XML never carries an explicit per-segment style. This change makes the Step 6 labels the default, authoritative classification (geometry only as fallback for missing/invalid labels) and propagates the label onto every exported line segment in the Step 7 XML as an explicit <Type> element on <Line> and <Spline>. Two repos are touched: modellinglines (this repo) and modellingexport (XML writer, backward-compatible extension). Implementation runs as a multi-agent workflow (Grok does most of the work), committed often under AI3D-341, delivered as draft PRs.

Current state (verified in code)

Key decisions

#DecisionRationale
1 Labels-first by default: flip clustering.prefer_artifact_segment_type default to true (config + both CLI entry points). Keep the flag as an escape hatch to restore geometry-only behavior. All the plumbing, logging, and stats events already exist and are tested behind the flag; flipping the default is the lowest-risk way to make labels authoritative.
2 Geometry stays as fallback only: per-cluster fallback when the label is missing/invalid, whole-run fallback when NPZ count ≠ cluster count (existing behavior, now on by default). No hard failure. Old Step 6 artifacts without segment_type must keep working; A1/A2 reruns shouldn't crash on stale caches.
3 Per-segment label in XML: each exported segment dict gets "type": "dashed" | "solid" (from LaneSegment.segment_type, which now originates from Step 6). write_xml.create_xml emits it as a <Type> child on <Line> and <Spline>only when the key is present (backward compatible). Export package bumped 0.7.1 → 0.8.0; this repo's pin updated. Explicit label survives even if a consumer doesn't know the Line-vs-Spline convention; additive schema change can't break existing consumers that ignore unknown elements.
4 Feature-level Type unchanged ("Center Lines" / "Axis of the Edge" derivation stays as is). It already consumes segment types, which now come from Step 6 labels — it inherits the fix for free; consumers depend on those strings.
5 Audit, don't rewrite, re-derivation sites: verify every place a SegmentType is assigned or inferred (lane_linking.py:248, refine/dedupe at cluster_stepper.py:3966, road_state.py) keeps the label rather than re-deriving from geometry. The label must survive chaining, merging, splitting, and snapshot round-trips to reach the XML intact.
6 Execution model: multi-agent Workflow — Grok 4.5 (cursor) for audit + implementation, Sonnet for mechanical work, GPT 5.6 Sol (codex) + Opus for adversarial review; Fable orchestrates. Frequent commits tagged AI3D-341; draft PRs, one per repo. Requested by Miro; Cursor tokens effectively free.

Phases

Phase 0 — Audit sweep small

Parallel Grok readers map every SegmentType assignment / re-derivation site, both export paths (Lane.info_dict and the stepper's _export at cluster_stepper.py:3973), snapshot round-trip, and existing tests around prefer_artifact_segment_type. Output: a checklist of touch points for phases 1–3, plus anything that silently re-derives type from geometry after cluster load.

Phase 1 — Labels-first default (modellinglines) small
  • Flip prefer_artifact_segment_type default to true in _config.py / config YAML and both CLI entry points (make the CLI flag a --no-…-capable pair or keep override semantics).
  • Ensure the misalignment / missing-label fallback logging stays accurate with the new default (message currently says "override skipped … falling back", which becomes the noteworthy case).
  • Tests: default-on behavior, per-file fallback, whole-run misalignment fallback, escape hatch off-switch. (No config-value-assertion tests — behavior tests only.)
Phase 2 — Label propagation to export dicts (modellinglines) small–medium
  • Add "type": segment.segment_type.name.lower() to every spline dict and line dict in Lane.info_dict (lane.py:348/366) and to the unassigned-lanes export path in cluster_stepper._export.
  • Fix any audit findings from Phase 0 where the label would be lost or re-derived geometrically between cluster load and export.
  • Tests: export_dict carries per-segment type; mixed solid/dashed lane keeps per-segment labels; snapshot-restored lanes still export labels.
Phase 3 — XML writer extension (modellingexport) cross-repo
  • In write_xml.create_xml: when a line/spline dict has "type", emit <Type>dashed|solid</Type> as a child of <Line> / <Spline>; omit otherwise.
  • Version bump 0.7.1 → 0.8.0, publish to Nexus (wrap-up flow), separate branch + draft PR under AI3D-341 in 3dai.iolabs.pointcloud.modellingexport.
  • Update modellinglines dependency pin to the new export version.
  • Tests in the export repo: element present when key given, absent otherwise (round-trip parse).
Phase 4 — Verification local
  • Full unit-test suites in both repos.
  • Local Step 7b run from existing snapshots (per the established local iteration workflow) on an abschnitt with Step 6 label-bearing NPZ caches; diff the produced XML before/after: per-segment <Type> present, count how many clusters changed classification label-vs-geometry, eyeball 7c overlays if classification shifts look suspicious.
Phase 5 — Adversarial review + ship small
  • Independent reviews: GPT 5.6 Sol (codex) and Opus lenses over both diffs (correctness of fallback paths, label survival through chaining/merge/dedupe, XML schema compatibility).
  • Fix confirmed findings, push branches, open draft PRs (both repos), report XML diff evidence.

Risks

Open questions — RESOLVED 2026-07-15

  1. XML shape: decided child element <Type> on Line/Spline confirmed by Miro.
  2. Escape hatch: decided not needed — the prefer_artifact_segment_type flag is removed entirely; labels are read directly by the loader (see scope extension below).
  3. Values: verified producer StrEnum writes lowercase solid/dashed (np.asarray(SegmentType.value) in mask-clustering cluster_io.py); real artifacts on battlebox (Abschnitt 4_5) checked as part of phase 6. The XML emits the same spellings.

Scope extension (Miro, 2026-07-15)

#ChangeDetail
E1 Drop the filtering-clusters dependency iolabs-point-cloud-filtering-clusters is removed from pyproject.toml. A local cluster_metadata module is vendored into modellinglines: ClusterInfo (with a new segment_type field) plus the NPZ cluster loader, which now reads segment_type directly per NPZ — this kills both the bbox geometry heuristic and the filename-order alignment step (_apply_artifact_segment_types).
E2 Reworked fit rule Label dashed and cluster length ≥ 0.70 m (new config value, clustering section) → straight line-segment fit. Label dashed but shorter → spline path (separator paint is ~24–25 cm wide; sub-0.7 m "dashes" are blobs). Label solid → spline path. Label missing/invalid → spline path + warning (old artifacts keep working).
E3 Escape hatch removed Config key, CLI flags (both entry points), guard, and the post-hoc label application are deleted; obsolete tests removed/rewritten against the vendored loader + fit rule.

Investigated by Grok readers; implemented directly by Fable after two cursor-delegate implementation attempts failed to land code; reviewed by Sol + Opus (2 findings, both fixed) and a fresh-context Fable pass (SHIP-WITH-NITS, all nits addressed). All committed under AI3D-341.

Verification results (2026-07-15) PASS