# VL Metadata Spec 3.2

> Status: canonical metadata schema for VLCode-Lite and stable Path `4`
>
> Scope:
> - Defines the canonical `ProjectMeta` JSON consumed by metadata diff, workflow regeneration, project context, and SystemDoc-backed tooling
> - Normalizes legacy extractor output into one stable shape
> - Aligns Theme metadata with VL 4.2.2 / THEME 7.0.3
> - Clarifies that SysDoc / workflow document bindings live in project config as `Doc ID`, not inside `ProjectMeta`
> - **3.2 additions** (additive, no schema break):
>   - §9 Layered authoring — `Process/MetaLayers/*.json` as streaming evidence; `ProjectMeta.json` remains the single canonical artifact
>   - §10 MetaDiff modes — `strict | reconcile | advisory` with authority classification
>   - §11 `Process/MetaConflict.json` — bookkeeping for unresolvable reconcile entries
>   - §12 Streaming generation discipline — entity-level fan-out and per-layer gates
>
> Publication note:
> - Stable Path `4` revision labels such as `V4` or `V5` track document publication history only
> - This markdown document describes Metadata Spec `3.2`
> - The canonical runtime payload version remains `$schema: "VL-ProjectMeta/3.0"` — root contract is unchanged in 3.2

## 1. Purpose

`ProjectMeta` is the machine-readable project model for a VL workspace.

It exists to support:

- deterministic workflow regeneration
- project-wide diff and impact analysis
- SystemDoc-backed prompt context
- IDE visualization and debug tooling
- stable references across `Apps/`, `Sections/`, `ExtComponents/`, `Services/`, `Database/`, and `Theme/`

This spec defines the canonical schema. Legacy field names may still be accepted by importers, but they are not canonical output.

## 1.1 Single Metadata Rule

VLCode-Lite must maintain exactly one canonical project metadata document at:

```text
.vl-code/ProjectMeta.json
```

Rules:

- producers may generate auxiliary reports, extraction logs, or spec snapshots, but they must not be written back into `ProjectMeta.json`
- `ProjectMeta.json` must only contain canonical project structure data defined by this spec
- run reports such as extracted spec dumps, benchmark snapshots, doc bindings, or workflow debug state must live in separate files
- consumers must normalize legacy input on read, then continue using the canonical in-memory shape downstream

## 1.2 Versioning Rule

Metadata schema versioning and document publication revisioning are intentionally separate:

- Document revision labels such as `V1` / `V2` / `V3` / `V4` / `V5` indicate published history for Path `4`
- `VL Metadata Spec 3.1` is the schema document version used by tooling and implementation notes
- `$schema: "VL-ProjectMeta/3.0"` is the canonical machine contract emitted into `.vl-code/ProjectMeta.json`

Tooling must not infer metadata shape from document publication labels alone.

## 2. Canonical Rules

### 2.1 Root schema

Canonical root object:

```json
{
  "$schema": "VL-ProjectMeta/3.0",
  "projectName": "SmartCampus",
  "projectDescription": "optional",
  "vlVersion": "4.3.1",
  "config": {
    "deviceTarget": "web",
    "screenResolution": "1440x900"
  },
  "fileManifest": [],
  "valueDomains": {},
  "apps": [],
  "sections": [],
  "components": [],
  "services": [],
  "dataSchema": {
    "tables": [],
    "relations": []
  },
  "theme": null,
  "dependencyGraph": null
}
```

### 2.2 Identifier policy

- `apps[*].id` is the canonical app identifier
- `sections[*].id` is the canonical section identifier
- `components[*].id` is the canonical component identifier
- `services[*].domainId` is the canonical service-domain identifier
- `services[*].methods[*].id` is the canonical method identifier
- `services[*].methods[*].serviceId` is the canonical fully-qualified service identifier, normally `domainId.methodId`
- `dataSchema.tables[*].id` is the canonical table identifier
- `theme.id` is the canonical theme identifier

Legacy aliases such as `appId`, `sectionId`, `componentId`, `tableName`, `serviceDomains`, `servicesUsed`, and `componentRefs` are compatibility inputs only.
`specVersion` is also a compatibility input only; canonical output uses `$schema`.

### 2.3 File path policy

Canonical default file locations:

- app: `Apps/<id>.vx`
- section: `Sections/<id>.sc`
- component: `ExtComponents/<id>.cp`
- service: `Services/<domainId>.vs`
- database: `Database/<projectName>.vdb` or explicit per-table/file-level mapping when available
- theme: `Theme/Theme.vth`

If `filePath` is omitted during normalization, tooling may synthesize the conventional path above.

### 2.4 Doc binding exclusion

`ProjectMeta` must not inline doc binding configuration for core specs or workflow prompts.

The following values belong to IDE / project profile configuration, not metadata schema:

- `coreDocIds`
- `docIdOverrides`
- any UI-level `Doc Ref` or viewer URL such as `vl://doc/16` or `/sysdoc.html?docId=16`

Reason:

- `ProjectMeta` models the project itself
- doc bindings model external tooling configuration
- keeping them separate prevents schema drift when official docs are republished or remapped

## 3. Entity Schemas

### 3.1 App

```json
{
  "id": "AdminApp",
  "filePath": "Apps/AdminApp.vx",
  "vlVersion": "4.3.1",
  "device": "web",
  "resolution": "1440x900",
  "description": "",
  "globalVars": [],
  "pages": [
    {
      "id": "Dashboard",
      "route": "/dashboard",
      "sections": ["DashboardMain"],
      "sectionRefs": [
        {
          "sectionId": "DashboardMain",
          "instanceId": "mainSection",
          "layoutProps": {}
        }
      ],
      "componentRefs": [],
      "layout": []
    }
  ],
  "routeMap": {
    "/dashboard": "DashboardMain"
  },
  "homeRoute": "/dashboard",
  "wiring": [],
  "navSectionInstanceId": null,
  "navEventName": null
}
```

Rules:

- `pages[*].id` must be stable inside the app
- `pages[*].sections` contains section IDs only
- `pages[*].sectionRefs[*].sectionId` must resolve to an existing section
- `routeMap` is a convenience index, not a second source of truth

### 3.2 Section

```json
{
  "id": "DashboardMain",
  "filePath": "Sections/DashboardMain.sc",
  "vlVersion": "4.3.1",
  "previewSize": null,
  "publicProps": [],
  "publicEvents": [],
  "publicMethods": [],
  "globalVars": [],
  "derivedVars": [],
  "consumesServices": [
    "CampusService.getOverview",
    "CampusService.listAlerts"
  ],
  "usesComponents": [
    "StatCard",
    "AlertTable"
  ],
  "interactiveElements": [],
  "internalMethods": [],
  "pipeFuncs": [],
  "isNavSection": false,
  "navMenuItems": [],
  "navItemInstanceId": null,
  "keyStates": {},
  "description": ""
}
```

Rules:

- `consumesServices` is an array of service ID strings, not nested service objects
- `usesComponents` is an array of component ID strings, not component ref objects
- `consumesServices[*]` must resolve to an existing `services[*].methods[*].serviceId`
- `usesComponents[*]` must resolve to an existing `components[*].id`

### 3.3 Component

```json
{
  "id": "StatCard",
  "filePath": "ExtComponents/StatCard.cp",
  "vlVersion": "4.3.1",
  "previewSize": null,
  "publicProps": [],
  "publicEvents": [],
  "derivedVars": [],
  "interactiveElements": [],
  "internalMethods": [],
  "pipeFuncs": [],
  "description": ""
}
```

### 3.4 Service domain

```json
{
  "domainId": "CampusService",
  "filePath": "Services/CampusService.vs",
  "vlVersion": "4.3.1",
  "envVars": [],
  "methods": [
    {
      "id": "getOverview",
      "serviceId": "CampusService.getOverview",
      "type": null,
      "params": [],
      "returns": {},
      "expose": null,
      "sig": null
    }
  ],
  "virtualTables": [
    {
      "id": "OverviewView",
      "source": "campus_overview",
      "fields": ["id", "title"],
      "extraSpecs": {}
    }
  ],
  "transactions": [],
  "backendComponents": []
}
```

Rules:

- `domainId` is canonical; `id`/`name` are compatibility inputs only
- `methods[*].id` is canonical; `methods[*].name` is compatibility input only
- `methods[*].serviceId` should be emitted even when it can be derived
- `virtualTables[*].source` must reference an existing table ID when it points to a physical table

### 3.5 Database schema

```json
{
  "dataSchema": {
    "tables": [
      {
        "id": "campus_overview",
        "name": "campus_overview",
        "filePath": "",
        "fields": [
          {
            "name": "id",
            "type": "STRING",
            "notNull": true,
            "default": null,
            "enumRef": null,
            "sourceField": null
          }
        ],
        "indexes": [],
        "seedData": null
      }
    ],
    "relations": [
      {
        "id": "overview_to_building",
        "from": "campus_overview",
        "to": "building",
        "cardinality": "N:1"
      }
    ]
  }
}
```

Rules:

- canonical table key is `id`, not `tableName`
- canonical column collection is `fields`; `columns` is compatibility input only
- `enumRef` must resolve into `valueDomains.enums[*].name` when used

### 3.6 Theme

```json
{
  "theme": {
    "id": "Theme",
    "name": "Theme",
    "filePath": "Theme/Theme.vth",
    "vlVersion": "4.3.1",
    "rootTag": "Theme-Enterprise-7.0",
    "meta": {
      "mode": "light",
      "version": "7.0.3",
      "styleSpaceVersion": "1.7",
      "base_theme": "Platform/Theme-Default-Light@1",
      "profile": "enterprise"
    },
    "slots": {
      "intent.primary.intentBg": "#2563EB",
      "size.md.sizeMinHeight": "40px",
      "state.focus.stateShadow": "@intent.intentFocusRing"
    },
    "designTokens": [],
    "componentVariants": [],
    "bindingRules": [],
    "overrides": []
  }
}
```

Rules:

- for VL 4.2.2 / THEME 7.0.3, the canonical theme model is `# Meta` plus `# Point Slot Values`
- `theme.slots` is the canonical compiled metadata view of `.vth` point-slot assignments
- `designTokens`, `componentVariants`, `bindingRules`, and `overrides` are compatibility carry-through fields only; new tooling should not require them

## 4. Consistency Constraints

Validation must reject or flag at least the following:

- section consumes a non-existent service ID
- section uses a non-existent component ID
- service virtual table points at a non-existent table
- app wiring references an instance ID not present in layout refs
- field `enumRef` points at a non-existent enum

## 5. Legacy Compatibility Mapping

Normalizers may accept the following legacy inputs:

| Legacy input | Canonical output |
| --- | --- |
| `project.name` / `project.projectName` | `projectName` |
| `database` | `dataSchema` |
| `serviceDomains` | `services` |
| `appId` | `apps[*].id` |
| `sectionId` | `sections[*].id` |
| `componentId` | `components[*].id` |
| `servicesUsed` / `services` / `serviceDomains` on section | `consumesServices` |
| `componentRefs` / `components` on section | `usesComponents` |
| `tableName` / `entityId` | `dataSchema.tables[*].id` |
| `columns` | `fields` |
| `methods[*].name` | `methods[*].id` |
| `theme.pointSlotValues` / `theme.pointSlots` | `theme.slots` |

Compatibility input does not change canonical output names.

## 6. Producer Requirements

Any producer that claims compliance with Metadata Spec 3.1 must emit:

- `$schema: "VL-ProjectMeta/3.0"`
- canonical IDs and field names
- `sections[*].consumesServices` as string IDs
- `sections[*].usesComponents` as string IDs
- `services[*].methods[*].id`
- `dataSchema.tables[*].id`
- `theme.slots` when theme slot data is available

## 7. Consumer Requirements

Consumers should:

- read canonical fields first
- optionally normalize known legacy aliases on import
- never write new metadata using legacy key names
- treat stable path references and workflow prompts as external configuration, not as metadata schema fields

## 8. Canonical Schema String

Use exactly:

```text
VL-ProjectMeta/3.0
```

## 9. Layered Authoring (Spec 3.2 addition)

### 9.1 Authoring vs Canonical

`ProjectMeta.json` remains the single canonical artifact (§1.1 unchanged). The
*authoring path* that produces it MAY stream through layers — each layer commits
a subset of `ProjectMeta` fields, gated by per-layer invariants, allowing
downstream VL generation to start as soon as its inputs are stable.

Layer files are *evidence of the authoring process*, not metadata duplicates.
They live alongside `ProjectMeta.json` but never replace it.

### 9.2 File locations

```text
.vl-code/
  ProjectMeta.json              # canonical (unchanged from 3.1)
Process/
  Specs/
    RequirementSpec.json        # PRD captured before metadata layers
  MetaLayers/
    IntentMeta.json             # L1
    DomainMeta.json             # L2
    ServiceContractMeta.json    # L3
    UIContractMeta.json         # L4
    InteractionMeta.json        # L5
    QualityTestMeta.json        # L6
    WorkspaceMeta.json          # L7 (rebuilt from files post-generation)
  MetaConflict.json             # §11 — only present when reconcile cannot resolve
```

`RequirementSpec.json` deliberately sits under `Process/Specs/`, NOT inside
ProjectMeta — PRD content is input to authoring, not project structure.

### 9.3 Layer roster

| L | Layer | Drives | Unlocks (downstream VL) |
|---|---|---|---|
| 1 | `IntentMeta` | `projectName`, `apps[].id`, `apps[].pages[].route`, `sections[].id`, `components[].id`, `services[].domainId`, `dataSchema.tables[].id` (skeleton only) | App route skeletons, empty Section/Component stubs, Map initial render |
| 2 | `DomainMeta` | `dataSchema.tables[].fields`, `dataSchema.relations`, `valueDomains.enums` | `Database/*.vdb`, Service skeleton |
| 3 | `ServiceContractMeta` | `services[].methods[].id`, `services[].methods[].params`, `services[].methods[].returns`, `services[].virtualTables` | `Services/*.vs` (entity-level fan-out per method) |
| 4 | `UIContractMeta` | `sections[].publicProps/Events/Methods`, `components[].publicProps/Events`, `apps[].pages[].sectionRefs` (boundaries only) | `ExtComponents/*.cp` skeleton, `Sections/*.sc` skeleton (entity-level fan-out) |
| 5 | `InteractionMeta` | `sections[].consumesServices`, `sections[].usesComponents`, `apps[].wiring`, `apps[].navSectionInstanceId`, `apps[].navEventName` | Section behaviour body, App wiring completion |
| 6 | `QualityTestMeta` | `sections[].interactiveElements` (with planned `vlid` / `selector`), per-page smoke paths, CRUD assertions | AutoTest plan generation; UI generation **respects planned `vlid` instead of inventing them** |
| 7 | `WorkspaceMeta` | rebuilt from on-disk `.vx/.sc/.cp/.vs/.vdb/.vth` after generation completes | Source-of-fact channel for `MetaDiff` against ProjectMeta |

L1–L6 are *forward authoring layers*. L7 is *reverse extraction* — it answers
"what do the files actually say". A reconcile pass (§10) joins L7 with the
authoritative composition of L1–L6.

### 9.4 Per-layer file shape

Each layer file MUST be a JSON object with the shape:

```json
{
  "$schema": "VL-MetaLayer/1.0",
  "layer": "IntentMeta",
  "layerVersion": 1,
  "committedAt": "2026-04-28T13:30:00Z",
  "producedBy": "agent_layer_intent",
  "ready": true,
  "fields": { },
  "invariantsChecked": ["intent.idsArePascal", "intent.routeUnique"],
  "notes": ""
}
```

The `fields` subtree carries only the canonical ProjectMeta fields this layer is
responsible for, using the same schema and identifier rules as §3. No legacy
aliases (§5) are emitted by layer files; producers MUST normalise on commit.

### 9.5 Per-layer invariants

A layer is `ready: true` only if its invariants pass. Each invariant is an ID
producers and gate steps reference by string.

| Layer | Invariant ID | Check |
|---|---|---|
| IntentMeta | `intent.idsArePascal` | every `apps[].id`, `sections[].id`, `components[].id`, `services[].domainId` matches `^[A-Z][A-Za-z0-9]*$`. `dataSchema.tables[].id` is excluded — table identifiers follow database conventions (typically snake_case, see §3.5) |
| IntentMeta | `intent.routeUnique` | every `apps[].pages[].route` is unique within its app |
| DomainMeta | `domain.fieldsHaveType` | every `dataSchema.tables[].fields[].type` is non-empty |
| DomainMeta | `domain.relationsResolve` | every `relations[].from` and `relations[].to` resolves to a `tables[].id` |
| DomainMeta | `domain.enumRefResolves` | every field `enumRef` resolves to `valueDomains.enums[].name` |
| ServiceContractMeta | `service.serviceIdCanonical` | every `methods[].serviceId === domainId + "." + methods[].id` |
| ServiceContractMeta | `service.virtualTableSourceResolves` | every `virtualTables[].source` (when pointing at a physical table) resolves to a `dataSchema.tables[].id` |
| UIContractMeta | `ui.sectionRefsResolve` | every `apps[].pages[].sectionRefs[].sectionId` resolves to a `sections[].id` |
| UIContractMeta | `ui.componentBoundariesDefined` | every `components[].publicProps` is an array (may be empty) |
| InteractionMeta | `interaction.consumesServicesResolve` | every `sections[].consumesServices[]` resolves to an existing `services[].methods[].serviceId` |
| InteractionMeta | `interaction.usesComponentsResolve` | every `sections[].usesComponents[]` resolves to a `components[].id` |
| InteractionMeta | `interaction.wiringInstancesResolve` | every `apps[].wiring[].instanceId` is present in some page's layout/`sectionRefs` |
| QualityTestMeta | `quality.vlidUnique` | per section, every `interactiveElements[].vlid` is unique |
| QualityTestMeta | `quality.smokePathSectionsResolve` | every smoke path step references an existing `sections[].id` |
| WorkspaceMeta | `workspace.rebuildSucceeded` | rebuild from files completed without parse errors |

Producers MAY add more checks; consumers MUST trust the producer's
`invariantsChecked` list as a minimum guarantee.

### 9.6 Composition rules

A consumer that wants the current "best-effort ProjectMeta" composes layers in
order L1 → L6, with later layers shallow-merging into earlier ones for shared
field paths. Layers MUST NOT contradict each other for fields they both touch;
a contradiction is a hard validation failure (caught by the next-layer gate).

The composition is materialised as `.vl-code/ProjectMeta.json` only at:

1. End of a layered codegen run (after L1–L6 commit), OR
2. Explicit `WriteProjectMeta` step in heal/reconcile workflows (§10).

Tools MUST NOT write a partial ProjectMeta to `.vl-code/ProjectMeta.json`.
Partial state lives in `Process/MetaLayers/` only.

## 10. MetaDiff Modes (Spec 3.2 addition)

The host-runtime `MetaDiff` step accepts `oldMeta`, `newMeta`, and an optional
`mode`. Mode replaces the older binary `failOnChanges` flag.

### 10.1 Modes

| Value | Behaviour |
|---|---|
| `strict` | non-empty diff → step fails the workflow |
| `advisory` | always succeeds; populates `out.diff` for downstream inspection |
| `reconcile` | succeeds; emits `out.diff` AND `out.reconcileDelta` (`VL_META_DELTA/1.0`) — the operations that, when applied to `oldMeta`, yield a meta consistent with `newMeta` for VL-authoritative fields only (§10.2) |

Backwards compatibility:

- `mode` absent + `failOnChanges: true` → `strict`
- `mode` absent + `failOnChanges: false` → `advisory`
- `mode` absent + `failOnChanges` absent → `advisory` (existing default behaviour: `_executeMetaDiff` defaults `failOnChanges` to `false`, so historical workflows without an explicit flag never threw — preserving that)

No existing workflow needs to change.

### 10.2 Authority classification

`reconcile` mode classifies each diff entry by dotted field path. **Meta-authoritative**
entries are dropped from the delta (the meta value wins; file-level lanes must
realign the file). **VL-authoritative** entries are folded into the delta as
`set` / `array-upsert` / `array-remove` operations.

| Field family | Authority | Rationale |
|---|---|---|
| `apps[].id`, `sections[].id`, `components[].id`, `services[].domainId`, `services[].methods[].id`, `services[].methods[].serviceId` | meta | IDs are identity; renaming via file is a regression |
| `apps[].pages[].sectionRefs[].sectionId`, `sections[].consumesServices`, `sections[].usesComponents`, `apps[].routeMap`, `apps[].wiring` | meta | Wiring graph is planner intent |
| `services[].methods[].params`, `services[].methods[].returns` | meta | Contract is upstream of implementation |
| `dataSchema.tables[].id`, `dataSchema.tables[].fields[].name`, `dataSchema.tables[].fields[].type`, `dataSchema.relations` | meta | Schema identity |
| `components[].publicProps[].default`, `components[].state[].default` | VL | Implementation detail |
| `sections[].layoutTree`, `sections[].interactiveElements[].layout` | VL | Layout is implementation |
| `services[].methods[].body`, anything ending `.bodyHint` | VL | Implementation |
| `theme.slots[*]` token values | VL | Override values are runtime decisions |
| `fileManifest.*` (entire subtree), legacy top-level `database.*` (normalize-emitted compat field), anything starting with `_` or `runtime.` | VL | Bookkeeping / normalize-derived; safe to fold |

Unclassified paths default to **meta-authoritative** (safer to drop the entry
than to fold an unknown field). Producers SHOULD extend this table as new
fields are introduced.

### 10.3 Output envelope

```json
{
  "protocol": "VL_META_DELTA/1.0",
  "summary": "3 prop defaults, 1 layout tree folded; 2 ID drifts dropped",
  "operations": [
    { "op": "set",          "path": "components.StatCard.publicProps.value.default", "value": "0" },
    { "op": "array-upsert", "path": "sections.Dashboard.interactiveElements", "key": "vlid", "value": { "vlid": "btn-refresh", "type": "Button" } }
  ],
  "droppedMetaEntries": [
    { "path": "apps.AdminApp.id", "old": "AdminApp", "new": "Admin", "reason": "meta-authoritative" }
  ]
}
```

`droppedMetaEntries[]` is informational — it tells the file-level heal lane
which files diverged from canonical IDs and need realignment.

## 11. Process/MetaConflict.json (Spec 3.2 addition)

When `reconcile` mode encounters entries it cannot resolve automatically (e.g.
two layer commits disagree on the same field path, or a custom invariant fails
post-fold), the runtime appends them to `Process/MetaConflict.json`:

```json
{
  "$schema": "VL-MetaConflict/1.0",
  "generatedAt": "2026-04-28T14:00:00Z",
  "entries": [
    {
      "path": "sections.Dashboard.consumesServices",
      "sources": [
        { "from": "InteractionMeta", "value": ["CampusService.getOverview"] },
        { "from": "WorkspaceMeta",   "value": ["CampusService.getOverview", "CampusService.listAlerts"] }
      ],
      "classification": "meta",
      "suggestedResolution": "rebuild Sections/Dashboard.sc from InteractionMeta",
      "userDecision": null
    }
  ]
}
```

The file is consumer-facing — UI may surface entries for user review, then
write `userDecision` back. The runtime never auto-resolves entries with a
non-null `userDecision`.

## 12. Streaming Generation Discipline (Spec 3.2 addition)

Workflows that generate VL files from layered metadata MUST follow these rules
to keep `ProjectMeta.json` and `Process/MetaLayers/` consistent:

1. **Layer commit is atomic per file.** A layer is either `ready: true` and on
   disk, or it does not exist. Half-written layer files are a producer bug.
2. **Downstream reads layers, not partial ProjectMeta.** VL-generation steps
   read `Process/MetaLayers/<Layer>.json` (composed up to the highest ready
   layer they depend on), not `.vl-code/ProjectMeta.json`. ProjectMeta is
   written once at run end.
3. **Entity-level fan-out is permitted.** A `Loop` step MAY iterate
   `IntentMeta.fields.services[]` and spawn a per-service `Services/*.vs`
   generation as soon as that service's `ServiceContractMeta` row is ready,
   without waiting for the rest of `services[]`. The engine's `spawnSibling`
   primitive is the supported mechanism.
4. **MetaGate failure stops only that lane.** A `MetaGate` step that fails an
   invariant fails its branch; sibling lanes that do not depend on the failed
   layer continue. The run's final outcome is determined by the heal/reconcile
   pass at the end (§10).
5. **No source-mutation during compile or read.** Layer files and ProjectMeta
   are write-only by their authoring producers; compile, lint, autotest, and
   read-paths MUST NOT modify them. (Reinforces §1.1 single-canonical rule for
   the streaming case.)
6. **WorkspaceMeta is the only retroactive source.** L7 is the one layer
   produced after files exist. Any "what did the files actually become" query
   reads `WorkspaceMeta`, not direct file scans, to keep the audit trail
   complete.

A workflow that breaks any of these rules MUST NOT claim Spec 3.2 conformance.
