.cvp / .cvt / .cvpanel / .cvsite Format v1.0 — Native CV Wire Contract

Status: v1.0 (CV-172, Sprint 22). Implementation: webpage/Simplestruct/cv-web/src/utils/cvpFormat.ts. Round-trip test: webpage/Simplestruct/cv-web/src/utils/cvpFormat.test.ts. Consumers: cv-web (read + write), cv-cad (CV-150 — will write the same envelopes when the Pascal editor lands).

Why this exists

The native CV file family is the format that ships projects between cv-web, cv-cad, and customers. It must survive renames, reorganizations, and a parallel writer in cv-cad without consumers silently mis-parsing. Documenting v1.0 here pins the contract before cv-cad’s writer lands so divergence is a compile-time / test-time signal, not a customer support ticket.

This format is the .pnl of 2026: a versioned, integrity-checked envelope around a fully-engineered panel set. Unlike the legacy .pnl, it is JSON, lossless across versions of cv-web, and round-trippable.

File extensions and what each carries

All four extensions share one envelope shape distinguished by the type discriminator. A consumer that sees cvVersion + type knows immediately which payload to expect.

Extension

type

Payload

When written

.cvp

"project"

full CVProject (panels + site + plans + elevations + settings)

File › Save Project

.cvt

"template"

job-neutral CVTemplatePayload — panels with id/createdAt/updatedAt stripped + project info cleared

File › Save Template

.cvpanel

"panel"

single PanelData with full 21-group structure

File › Save Panel

.cvsite

"site"

SiteData only (grid + scale + placements)

File › Save Site Plan

Envelope shape

{
  "cvVersion":  "1.0",
  "type":       "project" | "template" | "panel" | "site",
  "exportedAt": "2026-05-27T21:00:00Z",
  "<payloadKey>": { ... }
}

Field

Type

Required

Notes

cvVersion

"1.0"

yes

Bump on breaking changes only. Additive changes do NOT bump. Consumers that see an unknown version SHOULD warn and attempt to parse anyway.

type

enum

yes

Discriminates payload. Any other value is a hard reject.

exportedAt

ISO-8601 UTC

yes

Wall-clock timestamp at write time. Not used for ordering — purely audit.

project / template / panel / site

object

yes (per type)

Exactly one of these keys must be present, matching type.

What the legacy .pnl taught us, which carried forward:

  • Version stamp at the root. .pnl opens with mpv# = "V3.60"; v1.0 uses cvVersion: "1.0". A mismatched stamp is a parse-time signal, not a silent data corruption.

  • Type discriminator at the root. .pnl files are implicitly typed by extension only; v1.0 makes the type explicit so a .cvp renamed to .cvpanel still parses correctly (or rejects loudly).

  • Predictable, grouped field names. .pnl group keys were terse (mpv0, wdv4, etc.); v1.0 uses the full panelvar group names (mpvar, wdvar) — but the grouping survives.

Panel payload — the 21 panelvar groups

A PanelData is the JSON mirror of ConstructiVision’s 21 LSP var-groups. The interfaces below live in webpage/Simplestruct/cv-web/src/types/panelvar.ts — that file is the authoritative source of types. This document is the human-readable index.

Group

TS interface

Slot count

Purpose

mpvar

MpVar

1 (singleton)

Main panel — overall dims, mark, project header.

rovar

RoVar

4 slots

Recesses / openings without doors/windows.

wdvar

WdVar

6 slots

Windows.

drvar

DrVar

6 slots

Doors.

dlvar

DlVar

3 slots

Dock-level openings.

sbvar

SbVar

6 slots

Slab block-outs.

rbvar

RbVar

6 slots

Roof block-outs.

fhvar

FhVar

19 slots

Horizontal features (reveals, scores).

fvvar

FvVar

19 slots

Vertical features.

plvar

PlVar

2 slots

Pilasters.

llvar

LlVar

2 slots

Ledger lines.

tpvar

TpVar

1 (singleton)

Top-of-panel detail.

lbvar

LbVar

1 (singleton)

Ledger beam.

chvar

ChVar

1 (singleton)

Chamfers (per-edge).

tsvar

TsVar

2 slots

Tube steel.

fsvar

FsVar

2 slots

Form sequencing.

ssvar

SsVar

2 slots

Steel sections.

sdvar

SdVar

4 slots

Slab dowels.

bpvar

BpVar

8 slots

Brace points.

ppvar

PpVar

8 slots

Pick points (lifting).

wcvar

WcVar

15 slots

Weld connections (corner ties, embeds).

Slot arrays use a uniform Slot<T> shape: { enabled: boolean; data: T }. Disabled slots survive serialization so position-sensitive code (group indices used in .pnl-style lookups, cv-cad’s positional mapping) doesn’t shift when a slot is toggled off.

Project payload (.cvp)

CVProject (see panelvar.ts:708) carries:

  • Header fields (name, buildingName, jobNumber, client info).

  • panels: PanelData[] — every panel in full detail.

  • site: SiteData | null — site plan with grid + scale + panel placements.

  • plans: PlansData | null — overall/floor/foundation/roof plans + section schedules + door/window types.

  • elevations — auto-grouped panel collections per direction (B/F/R/L).

  • settings: ProjectSettings — units, paper size, concrete strength, rebar defaults, joint width.

Everything is structural — there are no UI-only fields in the envelope. Camera positions, selected panel, and dialog state are scoped to the in-memory store and are deliberately not serialized.

Template payload (.cvt)

CVTemplatePayload is a job-neutral subset of CVProject. The export path in cvpFormat.ts:exportTemplateCVT strips:

  • All panel id, createdAt, updatedAt (fresh IDs are generated on import).

  • All mpvar job-identifying fields: projectName, drawingNumber, jobNumber, date, drawBy, checkBy, notes.

What survives is the shape — settings + panels stripped of identity — so a customer can save “our standard 8” panel” once and load it into any new project.

Site payload (.cvsite)

SiteData (see panelvar.ts:526):

  • scale, gridSpacing, northArrow.

  • panels: SitePanelPlacement[]{ panelId, x, y, rotation, angle }.

.cvsite references panel IDs by string — a panel referenced by site must already exist in the loading project, or the consumer must drop it with a warning.

Parse contract

parseCVFile(json: string): ParseResult (in cvpFormat.ts:149) returns a tagged union:

  • { ok: true; envelope: CVFileEnvelope; warnings?: string[] } — native v1.0 envelope.

  • { ok: true; cadImport: true; panels: PanelData[]; warnings: string[] } — cv-cad named-export with positional keys (legacy-shape, adapter-decoded).

  • { ok: true; cadProjectImport: true; project: CVProject; ... } — cv-cad full project export (cvxpproj.lsp).

  • { ok: true; cadSiteImport: true; site: SiteData; ... } — cv-cad site export (cvxpsite.lsp).

  • { ok: false; error: string } — hard reject. Producer is non-CV or malformed.

The three cadImport: true branches handle cv-cad LSP-generated JSON whose shape is positional (flat mainPanel_1_1, standardOpenings_2_3 keys) rather than nested. The adapters in cadImportAdapter.ts, cadProjectAdapter.ts, cadSiteAdapter.ts reshape into native PanelData. These adapter paths predate cv-cad shipping its own native writer; once CV-150 lands cv-cad’s writer they fade to “legacy compatibility only”.

Versioning policy

  • New optional fields are additive within v1.0. Consumers MUST ignore unknown fields.

  • Renames, type changes, or required-field additions bump to cvVersion: "2.0".

  • parseCVFile will accept both "1.0" and "2.0" once v2.0 ships, with a migration hook at the // Version migration hook comment in cvpFormat.ts.

  • A consumer that sees a cvVersion it doesn’t understand MUST surface a warning to the user and SHOULD attempt best-effort parse rather than hard reject.

Validation contract

import { parseCVFile } from '../utils/cvpFormat';

const result = parseCVFile(jsonString);
if (!result.ok) {
  // surface result.error to the user; do not silently drop
} else if ('envelope' in result) {
  // native v1.0 envelope — switch on result.envelope.type
} else if ('cadImport' in result) {
  // cv-cad positional shape — already converted to PanelData[]
}

Round-trip is lossless for native envelopes — a panel exported from cv-web and re-imported is byte-identical. The vitest in cvpFormat.test.ts pins this guarantee, including a slot-count check across all 21 panelvar groups (so a future refactor that drops or renames a group breaks the test, not the customer’s file).

What v1.0 does NOT carry

  • Image / blob payloads. Site plan background PDFs, panel photographs, drawing-stamp images — all referenced by URL or omitted entirely. v1.0 is text-only, ≤ a few MB per project.

  • Engineering calculations. Wind loads, rebar designs, lifting-point loads — these are derived from the panel data, not stored. The engineer’s review pass downstream recomputes them.

  • Undo / redo history. In-memory store concern, not a wire format concern.

  • PWA cache state, user-prefs, billing entitlement. Persisted via IndexedDB and Supabase respectively, never via .cvp.