front-depiction

the-vm-standard

11
5
# Install this skill:
npx skills add front-depiction/claude-setup --skill "the-vm-standard"

Install specific skill from multi-skill repository

# Description

The VM Standard - inviolable covenants governing View Model architecture in this codebase. These covenants SHALL NOT be violated under any circumstance.

# SKILL.md


name: the-vm-standard
description: The VM Standard - inviolable covenants governing View Model architecture in this codebase. These covenants SHALL NOT be violated under any circumstance.


TITLE 1: VIEW MODEL ARCHITECTURE CODE

PREAMBLE

This Code establishes the governing requirements for View Model architecture within this codebase. The View Model pattern serves as the bridge between domain services and user interface. These provisions ensure consistency, maintainability, and proper separation of concerns across all View Model implementations.


CHAPTER 1: STRUCTURAL REQUIREMENTS

§ 1.1 Colocation

A View Model SHALL be defined in a single file bearing the name format {ComponentName}.vm.ts. No View Model definition SHALL span multiple files.

The interface, the tag, and the layer SHALL coexist within this single source file.

§ 1.2 Unity of Type and Tag

The interface type and the Context.Tag for any View Model SHALL bear identical names.

export interface ChatVM {
  readonly history$: Atom.Atom<Prompt.Prompt>;
  readonly inputValue$: Atom.Atom<string>;
  readonly setInputValue: (value: string) => void;
  readonly sendMessageAtom: Atom.AtomResultFn<void, void, unknown>;
}

export const ChatVM = Context.GenericTag<ChatVM>("ChatVM");

When imported as a namespace, this unity SHALL enable both type and runtime tag access through the same identifier.


CHAPTER 2: EXPORT REQUIREMENTS

§ 2.1 Live Layer Export

Every View Model SHALL export a live layer providing full production dependencies.

const FullLayer = pipe(
  FullSessionLayer,
  Layer.provide(DependentVMKey.variants.live)
);

const layerLive = pipe(
  layer,
  Layer.provide(FullLayer),
);

No View Model SHALL exist without a live layer capable of executing in production.

§ 2.2 Default Key Export

Every View Model file SHALL conclude with a default export via VMRuntime.key().

This export SHALL provide both live and test variants.

export default VMRuntime.key(ChatVM, {
  live: pipe(layer, Layer.provide(FullLayer)),
  test: layer,
});

§ 2.3 Namespace Import Requirement

All View Models SHALL be imported as namespaces.

// Required form:
import ChatVM from "./Chat.vm";

// Access patterns:
// ChatVM.variants.live  - the production layer
// ChatVM.variants.test  - the test layer
// ChatVM.tag            - the Context.Tag

Named imports that fracture the namespace unity are prohibited:

// Prohibited:
import { ChatVM } from "./Chat.vm";

CHAPTER 3: BEHAVIORAL REQUIREMENTS

§ 3.1 Thin Presentational Bridge

View Models SHALL serve as thin bridges between service layers and the user interface.

View Models SHALL NOT contain business logic. A View Model is required to:

(a) Yield services from the Effect context;
(b) Expose atoms for reactive UI binding;
(c) Provide action functions that delegate to services;
(d) Transform domain values into UI-ready formats.

// Compliant: VM delegates to service
const sendMessageAtom = VMRuntime.fn((_: void, get: Atom.FnContext) =>
  Effect.gen(function* () {
    const input = get(inputValue$);
    if (!input.trim()) return;
    get.set(inputValue$, "");
    yield* chatService.handleUserMessage(input);
  }).pipe(Effect.withSpan("Chat.sendMessage"))
);

Business logic SHALL reside in service layers. View Models adapt; they must not compute.

§ 3.2 Service Yield Requirement

View Models SHALL yield services from the Effect context. View Models must not construct services directly.

// Compliant: Services yielded from context
const layer = Layer.effect(
  ChatVM,
  Effect.gen(function* () {
    const registry = yield* AtomRegistry;
    const chatService = yield* ChatService.ChatService;
    const session = yield* EvaluationSession.tag;

    return { /* ... */ };
  })
);

Direct service construction is prohibited:

// Prohibited:
const layer = Layer.effect(
  ChatVM,
  Effect.gen(function* () {
    const chatService = new ChatServiceImpl();
    const session = createSession();

    return { /* ... */ };
  })
);

CHAPTER 4: ATOM REQUIREMENTS

§ 4.1 Atom Suffix Convention

All atom properties SHALL bear the $ suffix.

export interface SessionSetupVM {
  readonly inputValue$: Atom.Atom<string>;
  readonly history$: Atom.Atom<Prompt.Prompt>;
  readonly isLoading$: Atom.Atom<boolean>;
  readonly streamingMode$: Atom.Atom<StreamingMode>;
  readonly setupState$: Atom.Atom<SetupState>;

  // Non-atom members bear no suffix
  readonly setInputValue: (value: string) => void;
  readonly sendMessageAtom: Atom.AtomResultFn<void, void, unknown>;
}

§ 4.2 Confinement of Atoms

Atoms SHALL be defined only inside Effect.gen within the layer factory.

Atoms must not be defined at module scope.

// Compliant: Atoms confined within Effect.gen
const layer = Layer.effect(
  ChatVM,
  Effect.gen(function* () {
    const registry = yield* AtomRegistry;

    const inputValue$ = Atom.make("");
    const debugMode$ = Atom.make(false);
    const history$ = Atom.subscriptionRef(chat.history);

    return { inputValue$, debugMode$, history$ };
  })
);

Module-scope atoms are prohibited:

// Prohibited:
const inputValue$ = Atom.make("");

const layer = Layer.effect(
  ChatVM,
  Effect.gen(function* () {
    return { inputValue$ };
  })
);

CHAPTER 5: ACTION REQUIREMENTS

§ 5.1 Synchronous Setters

Synchronous setters SHALL use registry.set():

const setInputValue = (value: string) => registry.set(inputValue$, value);
const setDebugMode = (enabled: boolean) => registry.set(debugMode$, enabled);

§ 5.2 Asynchronous Actions

Asynchronous actions SHALL use VMRuntime.fn() returning AtomResultFn:

const sendMessageAtom = VMRuntime.fn((_: void, get: Atom.FnContext) =>
  Effect.gen(function* () {
    const input = get(inputValue$);
    if (!input.trim()) return;
    get.set(inputValue$, "");
    yield* chatService.handleUserMessage(input);
  }).pipe(Effect.withSpan("Chat.sendMessage"))
);

§ 5.3 Observability Span Requirement

All asynchronous actions SHALL be wrapped with Effect.withSpan().


CHAPTER 6: VARIANT REQUIREMENTS

§ 6.1 Dual Variant Structure

Every VMKey SHALL provide both live and test variants.

export default VMRuntime.key(SessionSetupVM, {
  live: layerLive,
  test: layer,
});

(a) The live variant SHALL provide the complete dependency graph for production execution.

(b) The test variant SHALL provide the minimal layer, allowing tests to inject mock dependencies.


CHAPTER 7: TESTING REQUIREMENTS

§ 7.1 Testing Protocol

Tests SHALL use the live layer variant with test dependencies injected.

Tests must not test the test layer directly.

// Compliant:
describe("ChatVM", () => {
  const ChatServiceMock = Layer.succeed(ChatService.ChatService, {
    handleUserMessage: () => Effect.succeed(undefined),
    exportChat: () => Effect.succeed({ json: "{}", filename: "test.json" }),
  });

  const TestLayer = pipe(
    ChatVMKey.variants.live,
    Layer.provide(ChatServiceMock),
    Layer.provide(TestSessionLayer),
  );

  it("sends messages via ChatService", () => /* test with TestLayer */);
});

Direct testing of the test variant is prohibited:

// Prohibited:
describe("ChatVM", () => {
  it("tests nothing of value", () => {
    const vm = buildVM(ChatVMKey.variants.test);
  });
});

The test variant exists for dependency injection, not for direct testing.


SCHEDULE A: COMPLIANCE CHECKLIST

Before any View Model is considered complete, the following SHALL be verified:

  • [ ] § 1.1: VM resides in single ComponentName.vm.ts file
  • [ ] § 1.2: Interface and tag share identical name
  • [ ] § 2.1: Live layer exports with full production dependencies
  • [ ] § 2.2: Default export uses VMRuntime.key() with live and test variants
  • [ ] § 2.3: All imports use namespace pattern
  • [ ] § 3.1: No business logic in VM; all logic delegated to services
  • [ ] § 3.2: All services yielded from context, none constructed
  • [ ] § 4.1: All atoms use $ suffix
  • [ ] § 4.2: All atoms defined inside Effect.gen within layer
  • [ ] § 5.1: Sync actions use registry.set()
  • [ ] § 5.2: Async actions use VMRuntime.fn()
  • [ ] § 5.3: Async actions wrapped with Effect.withSpan()
  • [ ] § 6.1: Both live and test variants provided
  • [ ] § 7.1: Tests use live variant with injected test dependencies

SCHEDULE B: CANONICAL TEMPLATE

import * as Atom from "@effect-atom/atom/Atom";
import { AtomRegistry } from "@effect-atom/atom/Registry";
import * as Result from "@effect-atom/atom/Result";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { pipe } from "effect/Function";
import { VMRuntime } from "@/lib/VMRuntime.js";
import * as SomeService from "../../services/SomeService.js";
import { FullLayer } from "../../lib/FullLayer.js";

// =============================================================================
// Interface
// =============================================================================

export interface ComponentNameVM {
  readonly data$: Atom.Atom<SomeData>;
  readonly isLoading$: Atom.Atom<boolean>;
  readonly setValue: (value: string) => void;
  readonly submitAtom: Atom.AtomResultFn<void, void, unknown>;
}

export const ComponentNameVM = Context.GenericTag<ComponentNameVM>("ComponentNameVM");

// =============================================================================
// Layer
// =============================================================================

const layer = Layer.effect(
  ComponentNameVM,
  Effect.gen(function* () {
    const registry = yield* AtomRegistry;
    const service = yield* SomeService.SomeService;

    const data$ = Atom.make<SomeData>(initialData);

    const submitAtom = VMRuntime.fn((_: void, get: Atom.FnContext) =>
      Effect.gen(function* () {
        yield* service.submit(get(data$));
      }).pipe(Effect.withSpan("ComponentName.submit"))
    );

    const isLoading$ = pipe(submitAtom, Atom.map(Result.isWaiting));

    return {
      data$,
      isLoading$,
      setValue: (value: string) => registry.set(data$, value),
      submitAtom,
    };
  })
);

// =============================================================================
// Export
// =============================================================================

export default VMRuntime.key(ComponentNameVM, {
  live: pipe(layer, Layer.provide(FullLayer)),
  test: layer,
});

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.