Post-Mortem — Elevation Marker / Dimension Pipeline Refactor

Session date: 2026-05-05 Branch: main Outcome: Elevation-marker stack on TB11 unified through dd-elev-flush; snake-tongue, duplicate TOC, WC/RL overlap, tiny right-side text, and \X literal-rendering all fixed.

Context

We started this session debugging a single visible defect on the right-hand elevation stack (duplicate TOC at panel top, WC competing with RL) and ended up rewriting the marker engine. By the end:

  • TOC, RL, FS, WC, DL, PL, PP, BP are unified through one panel-scoped accumulator (dd-r-acc / dd-l-acc) and one staggering pass (dd-elev-flush).

  • dim1 is no longer used to draw individual elevation markers — dd-marker-l / dd-marker-r entmake the SOLID arrow + horizontal + diagonal + through-line + INSERT ELEV + MTEXT directly. This eliminated the “snake-tongue” artifact (dim1 extends an arrow tail past the ELEV block when defpoints are coincident) and the “\X showing literally” artifact (\\X is a dim-text-only code; plain MTEXT needs \\P).

  • TOC uses the same dd-sfx-formatted MTEXT as every other marker — no bespoke entmake TEXT for the “T.O.C.” label any more.

The reason the work felt like weeks of churn (with dimensions appearing to be drawn 6×) is documented below, along with a punch list for the next round.

Why it took so long — root causes

These are not excuses, they are the things that should be fixed so the next change is fast.

  1. Three different load paths with inconsistent semantics. csv.lsp (production) loads modules by short name via SFSP. cv-auto-draw.lsp (headless test) loads by absolute path from cv-base. cv-gui-draw.lsp (GUI test) was loading by short name — same as production — which meant the GUI silently picked up an older drawdim.lsp from the AutoCAD support path instead of the file I was editing. Fix landed: changed cv-gui-draw.lsp to absolute-path loading, matching cv-auto-draw.lsp.

  2. Stale acad-console.log masquerading as fresh output. I burned ~30 min thinking my (princ) diagnostics weren’t firing because the file I was reading was from yesterday. The actual current log is per-session at reports/auto-test/CSB001_<hash>.log. The legacy acad-console.log looks alive (right name, right location) but isn’t updated by the current Run-ParityTest.ps1.

  3. Hidden cross-pass state. elevmrkr was called once per multi-item drawdim invocation. Each call reset dd-cy-l / dd-cy-r to -99999 at function entry, which meant items from different passes (FS/RL on the feature pass, WC on the connection pass, RL fallback on the perimeter pass) couldn’t see each other’s positions — they overlapped. The visual symptom was “RL on perimeter_dim landing within 1″ of WC on connections_dim”. This was invisible until you trace it; the comment “reset each draw” suggested it was correct.

  4. Nested defun shadowing. drawdim.lsp declares (defun basedim …), (defun drwbas …), (defun elevmrkr …) inside its own body. Standalone basedim.lsp, drwbas.lsp, elevmrkr.lsp exist in the same directory, are loaded by csv.prj, but their definitions are silently overwritten by drawdim’s nested versions on every (drawdim …) call. New behavior added to standalones is dead code; new behavior added to nested versions is invisible to anyone reading the standalone files. This is bug #1 cause of “I can’t find where this is drawn”.

  5. dim1 does more than you think. With DIMASSOC=0 + dimblk1=elev + arg1/arg2 0.001 apart, dim1 emits a dim line, an arrowhead, a backstop, and an extension that overshoots leftward when the marker is staggered. The “snake-tongue” through the circle was that overshoot. None of it shows up unless the stagger is large enough to push the overshoot past the diagonal leader endpoint — which is why FS (no stagger) looked fine and the cluster around y=274 did not.

  6. The “drawn 6 times” feeling is real but mislabeled. finpan.lsp calls drawdim six times — once per layer group (mpvar, fhvar/fvvar, sdvar/wcvar, bpvar/ppvar, tpvar/lbvar, then a full perimeter sweep). Each call clears no global state; the global accumulator xNlst lists chain across calls. Combined with the elevmrkr reset bug, the same elevation could be staggered into existence multiple times on different layers.

  7. x32x64 auto-sync. Run-ParityTest.ps1 copies x64 → x32 when x64 is newer. This works, but I edited x32 first and burned a cycle when the test still loaded the (older) x64 copy. The canonical source IS src/x64/TB11-01x64/; x32 is a derived artifact for the production VM.

Where dimensions are made — current map

Dimension-emitting code, by file (canonical source: src/x64/TB11-01x64/):

File

Lines

Role

What it emits

drawpan.lsp

~1100

Panel geometry

Polylines, 3D solids, RL DASHED line, viewport. No dim primitives.

drawdim.lsp

2783

The dim engine

All dimension and elevation primitives. 58 command "dim1" invocations + entmake for markers/labels.

finpan.lsp

825

Orchestrator

Calls drawdim six times (one per layer group), then (dd-elev-flush).

csvutil.lsp

Wrapper

csv_dim1 — thin wrapper over (command "dim1" …) for hor/ver/ali/leader subtypes.

basedim.lsp (173 L)

Dead

Loaded but the nested (defun basedim …) inside drawdim overwrites it before any caller hits it.

drwbas.lsp (113 L)

Dead

Same — overwritten by nested copy.

elevmrkr.lsp (173 L)

Dead

Same. The interesting recent fixes (RL L/R split, FS/RL/DL/PL routing, the snake-tongue bypass) live in drawdim.lsp’s nested copy.

drawdimlst.lsp

Duplicate routing

Mirrors drawdim’s multi-item branch. Called only by nbblock.lsp. Candidate for deletion or reuse.

panatt.lsp

Data reader

Decodes the NOD XRecord (compact / source-mode formats). Sets panelvar, mpe1, mpl1, csv_rl_elev_l/r, etc. No drawing.

convert.lsp

Defaults

Fills panelvar defaults when XRecord version != V2.25. No drawing.

tiltxrec.lsp, pjdll.lsp

Decoders

Compact-format and legacy-VLX decoders called from panatt. No drawing.

Feature files (green.lsp, dowels.lsp, weldconn.lsp, chamfer.lsp, miter.lsp, centgrav.lsp, feature.lsp, rndblock.lsp, mkblk.lsp, revision.lsp)

Geometry

Confirmed by grep: none of them emit dim primitives. They draw 3D solids / hatches / block geometry only; their dimensioning is handled by the drawdim per-section cond branches.

Inside drawdim.lsp, the elevation-marker call graph is now:

(elevmrkr)  ; called from drawdim's multi-item branch
  ├── PP/BP from ypplst/ybplst → cons onto dd-l-acc
  ├── PP-conflict-WC y1lst → cons onto dd-r-acc (connections_dim)
  └── y2lst with PP/BP-proximity check → cons onto dd-r-acc or dd-l-acc

(dd-elev-flush)  ; called once from finpan after the 6 drawdim passes
  ├── append FS/RL_R/DL/PL → dd-r-acc
  ├── append RL_L → dd-l-acc
  ├── append T.O.C. (cadr p3) → dd-r-acc
  ├── filter dd-r-acc: drop entries equal to panel-bottom
  ├── (dd-elev-multi dd-l-acc 'dd-cy-l "L")  → dd-marker-l per item
  ├── (dd-elev-multi dd-r-acc 'dd-cy-r "R")  → dd-marker-r per item
  ├── panel-bottom-L  via dd-marker-l
  ├── DL/SD/FF (left)  via dd-draw-elev → dd-marker-l
  └── panel-bottom-R   via direct csv_dim1  ← still bespoke; see punch list

Refactoring inventory (what we factored out)

drawdim.lsp already had ~10 dd-* helpers; this session added or modified:

  • dd-elev-flush (new) — single drain function, called from finpan after the six drawdim passes. Encapsulates panel-top/bottom filter, the unified left+right dd-elev-multi calls, and the structural-marker draws.

  • dd-marker-l / dd-marker-r (rewritten) — replaced csv_dim1 + dd-fixmtext-x with explicit entmake of SOLID + 3 LINEs + INSERT ELEV + MTEXT. Eliminates the snake-tongue artifact entirely.

  • dd-sfx (modified) — switched the line separator from \X (dim-only) to \P (universal MTEXT) so the entmake’d labels render correctly. The \H0.7x; height directive still applies to the suffix.

  • y1lst panel-top/bottom filter — added drop step before accumulating to dd-r-acc so a PP at panel-top can no longer duplicate the TOC.

  • y2lst ascending sort — added before the conflict-check loop so a smaller WC can’t be staggered above a larger one drawn earlier in the list.

  • dd-elev-flush saves and restores DIMASSOC, dimblk1/2, dimsd1/2, dimse1/2, dimtad, dimsah so it works in either csv_headless=T (DIMASSOC=0) or GUI (DIMASSOC=1) mode.

  • cv-gui-draw.lsp — load loop changed from (load m) (short name, SFSP-resolved) to (load (strcat cv-base m ".lsp")) (absolute path) — matches cv-auto-draw.lsp.

  • finpan.lsp — resets dd-l-acc / dd-r-acc alongside the existing dd-fs-elevs / dd-rl-elevs-* resets, and calls (dd-elev-flush) after the last drawdim pass.

Existing helpers that this work depends on (no change needed):

  • dd-stagger, dd-elev-multi, dd-elev-dim, dd-build-elevs, dd-accum, dd-dedupe-tier, dd-plate-dim, dd-fixmtext, dd-fixmtext-x, dd-draw-elev.

What’s still messy — punch list

These are the things that would make the next change fast.

  1. Delete or merge the standalone shadow modules. basedim.lsp, drwbas.lsp, elevmrkr.lsp are dead. Either delete them and remove from csv.prj, or extract the nested versions back to standalone and remove the nesting. The current state actively misleads code readers.

  2. Issue #151 (DRY) and #152 (global pollution) cover the two next moves. From gh issue list:

    • #151 [Refactor] drawdim.lsp: ~275 lines of saveable duplication (DRY violations)

    • #152 [Refactor] drawdim.lsp: global variable pollution (*dd-* constants + accumulator lists + dd-el-* state) These are the right framing — link them from the punch list when the next branch starts.

  3. Single-item cond branches in drawdim.lsp. ~20 sections (mpvar, rovar, wdvar, drvar, dlvar, fsvar, plvar, llvar, tsvar, ssvar, sbvar, rbvar, tpvar, lbvar, mivar, ch1-ch4, wcvar, sdvar, bpvar, ppvar, fhvar, fvvar) each get their own ~50-150 line block with inline csv_dim1/entmake calls. This is the bulk of the 2783-line file. Candidate: per-feature handler files dispatched by table lookup.

  4. drawdimlst.lsp is a near-duplicate of drawdim’s multi-item branch. Only nbblock.lsp calls it. Either fold its sole caller into drawdim or delete it.

  5. Six drawdim calls in finpan.lsp could be one. The layer routing is (setvar "clayer" …) before each call; the work could be one combined pass that routes per-section to the correct layer. Worth doing only after #3 above.

  6. Panel-bottom-R (in dd-elev-flush) still uses bespoke csv_dim1 + dd-fixmtext. Everything else routes through the accumulator and dd-marker-r. This is a one-off because panel-bottom-R lives at cadr p1 rather than at a feature elevation, but it could be added to dd-r-acc with a “PB” suffix and let dd-elev-multi draw it.

Diagnostic-print audit

The source is noisier than it needs to be for a shipped build. Counts (Grep):

Tag

Count

Where

Status

[DD-DIAG]

~15

drawdim.lsp

Should be removed — these were debug prints from this session that were partially reverted but the entries in the _dd-fc=true accumulation aren’t fully cleaned.

[DD-FEAT]

6

drawdim.lsp

Useful — feature routing

[DD-EXIT], [DD-Y1-VALS], [DD-Y2-VALS], [DD-X1-VALS], [DD-X2-VALS], [DD-VAR], [DD-TIER]

7

drawdim.lsp

Useful as gated tracing — would prefer behind (if csv_trace …)

[DP-FEAT]

82

drawpan.lsp

Excessive. Every feature entry/exit. Behind a csv_trace gate or remove.

[FP-2-DIAG]

9

finpan.lsp

Useful — entity counts per layer

[GREEN-DIAG]

many

green.lsp

Looks debug-only; fires per-overlap-hit

[COMPACT-DUMP], [FS-DECODE], [COMPACT-CVT]

many

panatt.lsp

Useful for parsing-bug diagnosis but very verbose. Gate behind csv_trace.

[CHADW-…], [TEMP-…]

0

Good — none left from this session.

Recommended cleanup commit: introduce a csv_trace global (default nil) and gate the [DP-FEAT], [GREEN-DIAG], [COMPACT-DUMP], [FS-DECODE] blocks behind it. Keep [FP-2-DIAG] and [CV-AUTO]/[GUI] entry/exit lines — those are checkpoints, not traces.

Documentation status

What’s documented in source headers (good):

  • drawdim.lsp — 143-line header explaining nlst routing, x/y accumulator tiers, single-vs-multi-item handling.

  • finpan.lsp — 76-line header listing the seven phases.

  • drawpan.lsp — UPDATE block read condition, Bug 49 root cause.

  • dd-elev-flush has a comment block explaining “why a separate flush” — good.

What is not documented in headers (only in commit messages):

  • The panel-scoped accumulator architecture and why dd-l-acc / dd-r-acc survive across drawdim calls.

  • The snake-tongue / dim1-bypass rationale in dd-marker-l/r.

  • The \X\P switch in dd-sfx.

  • The RL left/right split (dd-rl-elevs-l vs dd-rl-elevs-r).

  • The cv-gui-draw absolute-path load fix.

  • The standalone-module-is-dead policy.

Recommended doc commit: add a “Pipeline overview — Apr 2026 refactor” section near the top of drawdim.lsp cross-referencing finpan.lsp and cv-gui-draw.lsp. One paragraph each.

Deprecated code & risk artefacts (where they live)

  • docs/source/modernization-2026/05-risk-register.md — present.

  • docs/source/modernization-2026/32-tb11-bug-tracker.md — present.

  • docs-developer/grg-system.md — untracked; the GRG (grid reference guide) overlay system the user generated this session.

  • GitHub issues: #150 (weldconn lesson learned), #151 (DRY), #152 (globals). All open before this post-mortem.

Verification

Both gates that matter:

  1. Headless DXF parity (post-refactor): powershell -ExecutionPolicy Bypass -File scripts/Run-ParityTest.ps1 — current state is Test=391, Golden=350 with the residual delta on window-cutout layer drift (feature_dim vs perimeter_dim) — pre-existing and out of scope of this refactor.

  2. GUI visual (per the standing memory rule): user runs (c:cv-gui-draw) and visually confirms TOC + cluster + leaders. Current state per user’s own confirmation: ✅.

Going forward, the GUI test now loads from the same absolute path as headless, so DXF parity and GUI parity should agree.

Cross-references

  • DFMEA new failure modes added (doc 31, §9): NEW-DFMEA-A through NEW-DFMEA-F (see below).

  • Risk Register entries added (doc 05): R-LOAD-DIVERGE, R-STALE-LOG, R-NESTED-SHADOW, R-DIM1-OVERSHOOT, R-CROSS-PASS-STATE.

  • Validation Plan additions (doc 06): V-LOAD-PATH-PARITY, V-LOG-FRESHNESS, V-NESTED-DEFUN-AUDIT.

  • GH issues closed: any session-specific defect issues for snake-tongue / TOC duplicate / WC-RL overlap / \X literal.

  • GH issues opened: load-path divergence, stale log capture, nested-defun shadowing audit, diagnostic-print gating, dead-module deletion.