.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 |
|
Payload |
When written |
|---|---|---|---|
|
|
full |
File › Save Project |
|
|
job-neutral |
File › Save Template |
|
|
single |
File › Save Panel |
|
|
|
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 |
|---|---|---|---|
|
|
yes |
Bump on breaking changes only. Additive changes do NOT bump. Consumers that see an unknown version SHOULD warn and attempt to parse anyway. |
|
enum |
yes |
Discriminates payload. Any other value is a hard reject. |
|
ISO-8601 UTC |
yes |
Wall-clock timestamp at write time. Not used for ordering — purely audit. |
|
object |
yes (per |
Exactly one of these keys must be present, matching |
What the legacy .pnl taught us, which carried forward:
Version stamp at the root.
.pnlopens withmpv#="V3.60"; v1.0 usescvVersion: "1.0". A mismatched stamp is a parse-time signal, not a silent data corruption.Type discriminator at the root.
.pnlfiles are implicitly typed by extension only; v1.0 makes the type explicit so a.cvprenamed to.cvpanelstill parses correctly (or rejects loudly).Predictable, grouped field names.
.pnlgroup 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 |
|---|---|---|---|
|
|
1 (singleton) |
Main panel — overall dims, mark, project header. |
|
|
4 slots |
Recesses / openings without doors/windows. |
|
|
6 slots |
Windows. |
|
|
6 slots |
Doors. |
|
|
3 slots |
Dock-level openings. |
|
|
6 slots |
Slab block-outs. |
|
|
6 slots |
Roof block-outs. |
|
|
19 slots |
Horizontal features (reveals, scores). |
|
|
19 slots |
Vertical features. |
|
|
2 slots |
Pilasters. |
|
|
2 slots |
Ledger lines. |
|
|
1 (singleton) |
Top-of-panel detail. |
|
|
1 (singleton) |
Ledger beam. |
|
|
1 (singleton) |
Chamfers (per-edge). |
|
|
2 slots |
Tube steel. |
|
|
2 slots |
Form sequencing. |
|
|
2 slots |
Steel sections. |
|
|
4 slots |
Slab dowels. |
|
|
8 slots |
Brace points. |
|
|
8 slots |
Pick points (lifting). |
|
|
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
mpvarjob-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".parseCVFilewill accept both"1.0"and"2.0"once v2.0 ships, with a migration hook at the// Version migration hookcomment incvpFormat.ts.A consumer that sees a
cvVersionit 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.