Skip to content

feat: delta visibility before archive — materialized view, preview, and conditional sync #21

@lsmonki

Description

@lsmonki

Problem

When working on an active change that modifies existing specs via deltas, the canonical spec remains unchanged until archive. This means:

  1. Agents and humans reading specs during implementation see stale content — the spec doesn't reflect the changes being implemented. CompileContext provides delta instructions and outlines of the current spec, but never the merged result.

  2. There's no way to see "what the spec will look like" after delta application, except mentally reconstructing it or waiting until archive.

  3. Other changes targeting the same spec don't see the pending modifications from an in-progress change, leading to potential conflicts discovered only at archive time.

Current Behavior

Operation Reads base spec Merges deltas Mutates canonical spec
CompileContext Yes (outlines only) No No
ValidateArtifacts Yes (preview for validation) Yes (internal, discarded) No
ArchiveChange Yes Yes Yes (only here)

The delta merge algorithm already exists and works (apply-delta.ts, ~350 lines). ValidateArtifacts already does a merge internally to validate the result. The infrastructure is there — it's just not exposed.

Prerequisites

Note: Once #22 is implemented, Level 1 and 2 can be enhanced with drift detection (warn if the base spec changed since the delta was authored). This is a nice-to-have safety guard, not a blocker.

Proposal

Three levels of delta visibility, ordered by value and risk:

Level 1 — Materialized view in CompileContext (zero risk)

When compiling context for an active change with delta artifacts, include the merged spec content instead of (or alongside) the raw outline.

How it works:

  • For each spec in change.specIds with a delta artifact:
    • Load base spec from SpecRepository
    • Load delta from change directory
    • Apply via ArtifactParser.apply(baseAst, deltaEntries)
    • Serialize merged result
    • Include in the instruction block as "Spec (with pending changes applied)"
  • The canonical spec is never mutated — this is a read-only view
  • If delta application fails, fall back to current behavior (outline only) with a warning
  • Future enhancement: when feat: capture delta baselines (content hash + git ref) on validation #22 is available, compare current spec hash against deltaBaselines — if they differ, skip merge and warn about drift

Impact: Agents always see the "true" spec state while working on a change. No governance concerns because nothing is mutated.

Level 2 — Explicit preview command (zero risk)

A CLI command to inspect the merged result without mutating anything:

specd spec preview <spec-path> --change <change-name>

Output: The spec as it would look after delta application. Useful for human review.

Could also support --diff mode to show what changed.

Level 3 — Sync deltas to canonical specs

Allow syncing validated deltas to canonical specs before archive.

specd change spec-sync <name>

Requires: #22 (delta baselines) for drift detection.

Preconditions

Sync flow

  1. Apply changes to canonical specs:
    • New spec artifacts (specs/) → copy to canonical spec location
    • Delta artifacts (deltas/) → merge into existing canonical specs (same merge logic as archive)
  2. Archive synced artifacts within the change directory:
    .specd/changes/<change-name>/
    ├── history/
    │   └── 2026-03-26T14-30-00/        # timestamp of sync
    │       ├── originals/              # copy of canonical specs BEFORE sync
    │       ├── specs/                   # new spec artifacts that were created
    │       ├── deltas/                  # delta artifacts that were applied
    │       └── sync-manifest.yaml       # rollback metadata
    ├── deltas/                          # recreated: no-op deltas for all specs
    └── (no specs/ folder)              # all specs are now canonical — future changes are deltas
    
    • Copy each canonical spec (before mutation) into history/<timestamp>/originals/ — these are the pre-sync snapshots needed for rollback and triple diff
    • Move the current specs/ and deltas/ folders into history/<timestamp>/
    • Generate a sync-manifest.yaml in the history entry with rollback metadata:
      syncedAt: "2026-03-26T14:30:00Z"
      vcsRef: "a1b2c3d"                    # commit hash at sync time (if VCS available)
      specs:
        "default:auth/login":
          type: delta                       # was a delta merge
          originalContentHash: "sha256:..."  # canonical spec hash before merge
        "default:auth/register":
          type: new                          # was a new spec creation
    • Recreate all artifacts as no-op deltas — both previously-new specs and previously-delta specs are now canonical, so all become no-op deltas
  3. The change remains active in its current lifecycle state. It is still a valid change with valid (no-op) artifacts.

Why all no-op deltas after sync?

After sync, all specs targeted by the change exist as canonical specs — including those that were originally new. The artifacts must reflect this:

  • Previously new specs are now canonical → they become no-op deltas (the spec already exists as synced)
  • Previously delta specs were already merged → they become no-op deltas (the canonical spec already matches)

This means:

  • The change remains valid and can proceed through its lifecycle normally
  • CompileContext won't try to re-merge already-applied changes
  • ArchiveChange becomes a clean close (no mutations needed, just lifecycle transition)
  • If new modifications are needed, new deltas are authored against the now-updated canonical specs — all future artifacts are deltas, since no spec is "new" anymore

Governance interaction

Sync is allowed regardless of governance gates — spec approval gates govern the design phase (authoring deltas), not the act of applying validated work. If deltas passed validation, the spec changes have already been reviewed to the extent governance requires at that stage.

However, governance gates still apply to the archive step. A synced change with no-op deltas still needs to complete its full lifecycle (including signoff if configured) before being archived.

Lifecycle restrictions after sync

Once a change has been synced, the canonical specs have been mutated. To prevent orphaned mutations, the following lifecycle transitions are blocked while a sync is active:

  • Discard — blocked. The canonical specs contain synced changes; discarding would leave them orphaned.
  • Draft — blocked. Moving back to draft implies the design isn't final, but the specs already reflect it.

To unblock these transitions, the user must first rollback the sync:

specd change spec-rollback <name>

Rollback uses the originals/ snapshots and sync-manifest.yaml from the most recent history/ entry:

Clean rollback (no drift):
For each synced spec, compare originalContentHash from the manifest against hash(current canonical - synced changes). If no other changes occurred:

  • Restore canonical specs from history/<timestamp>/originals/
  • Move the original artifacts back from history/<timestamp>/specs/ and history/<timestamp>/deltas/
  • Remove the history entry

Rollback with drift (someone else changed the spec):
If the current canonical spec differs from what the sync produced (i.e., other changes landed on top), rollback is blocked and a two-panel diff is shown:

── Your sync (original → synced) ─────────────────
Shows what your sync changed in the spec.
Source: diff(originals/<spec>, apply(originals/<spec>, deltas/<spec>))

── Changes since sync (synced → current) ─────────
Shows what others changed after your sync.
Source: diff(apply(originals/<spec>, deltas/<spec>), canonical/<spec>)

This gives the user full context to decide how to proceed manually. All three versions are available:

  • Originalhistory/<timestamp>/originals/<spec>
  • Synced → reconstructed by applying history/<timestamp>/deltas/ to the original
  • Current → the canonical spec as it is now

After a clean rollback, the change is back to its pre-sync state and all lifecycle transitions are available again.

Multiple syncs

A change can be synced multiple times if new deltas are added after a previous sync. Each sync creates a new history/<timestamp>/ entry, moves current artifacts there, and recreates no-ops. Rollback always operates on the most recent sync entry — each rollback peels back one layer in reverse chronological order.

Design Tensions

Approval invalidation

If governance is enabled and we allow Level 1 (materialized view), there's no issue — the canonical spec hasn't changed. Level 3 applies only validated artifacts, so governance over the design phase is respected.

Multiple changes targeting the same spec

If change A syncs its deltas and change B also targets the same spec, change B's delta was authored against the pre-sync base. This is detectable via baselines (#22) — change B's validation would flag the drift before it could sync stale deltas.

Recommended Implementation Order

  1. Level 1 — highest value, zero risk, builds on existing ValidateArtifacts pattern
  2. Level 2 — simple CLI wrapper around the same merge logic
  3. Level 3 — requires feat: capture delta baselines (content hash + git ref) on validation #22, careful lifecycle integration, and the history mechanism described above

Related Specs

  • specs/core/delta-format/spec.md — delta structure and apply algorithm
  • specs/core/archive-change/spec.md — current merge-on-archive flow
  • specs/core/compile-context/spec.md — context assembly (delta context block)
  • specs/core/validate-artifacts/spec.md — already does internal merge for validation
  • specs/core/config/spec.md — governance gates (approvals.spec, approvals.signoff)
  • specs/core/change/spec.md — change lifecycle and discard semantics

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions