Skip to content

Manifesto Schema Specification — Foundational Design Rationale (FDR)

Version: 1.0 Status: Normative Purpose: Document the "why" behind every major design decision


Table of Contents

  1. Purpose of This Document
  2. FDR-001: Core as Calculator
  3. FDR-002: Snapshot as Only Medium
  4. FDR-003: No Pause/Resume
  5. FDR-004: Effects as Declarations
  6. FDR-005: Errors as Values
  7. FDR-006: Flow is Not Turing-Complete
  8. FDR-007: No Value Binding in Flows
  9. FDR-008: Call Without Arguments
  10. FDR-009: Schema-First Design
  11. FDR-010: Canonical Form and Hashing
  12. FDR-011: Computed as DAG
  13. FDR-012: Patch Operations Limited to Three
  14. FDR-013: Host Responsibility Boundary
  15. FDR-014: Browser Compatibility
  16. FDR-015: Static Patch Paths
  17. Summary: The Manifesto Identity

1. Purpose of This Document

This document records the foundational design decisions of Manifesto Schema Specification.

For each decision, we document:

SectionContent
DecisionWhat we decided
ContextWhy this decision was needed
AlternativesWhat other options existed
RationaleWhy we chose this option
ConsequencesWhat this enables and constrains

These decisions are constitutional — they define what Manifesto IS and IS NOT.


FDR-001: Core as Calculator

Decision

Core is a pure semantic calculator. It computes state transitions but does not execute them.

Context

Most state management systems conflate two concerns:

  1. Computing what should change
  2. Executing the change (IO, network, side effects)

This conflation creates systems that are:

  • Hard to test (require mocking IO)
  • Hard to explain (side effects hidden in computation)
  • Hard to replay (non-deterministic)

Alternatives Considered

AlternativeDescriptionWhy Rejected
Integrated RuntimeCore executes effects directlyViolates purity, breaks determinism
Plugin ArchitectureCore calls effect handlers via injectionStill couples execution to computation
Actor ModelCore sends messages to effect actorsAdds complexity, still couples concerns

Rationale

By making Core a pure calculator:

  1. Determinism: compute(snapshot, intent, context) always produces the same result.
  2. Testability: No mocking needed; just provide snapshot and intent.
  3. Explainability: Every step can be traced without IO interference.
  4. Portability: Core can run anywhere (browser, server, edge, WASM).

Consequences

EnablesConstrains
Pure unit testingHost must implement effect execution
Time-travel debuggingNo "fire and forget" effects
Deterministic replayAll IO goes through Host
Cross-platform CoreEffect handlers are Host-specific

Canonical Statement

Core computes. Host executes. These concerns never mix.


FDR-002: Snapshot as Only Medium

Decision

Snapshot is the only medium of communication between computations. There is no other channel.

Context

Traditional systems pass values in multiple ways:

  • Function return values
  • Callbacks
  • Events
  • Shared mutable state
  • Context objects
  • Continuation parameters

This multiplicity creates:

  • Hidden state
  • Untraceable data flow
  • Non-reproducible behavior

Alternatives Considered

AlternativeDescriptionWhy Rejected
Return ValuesEffects return results to callerCreates hidden continuation state
Context PassingPass context object through callsImplicit state, hard to trace
Event EmittersPublish results as eventsTemporal coupling, order-dependent
Continuation MonadsChain computations functionallyComplex, still implies suspended state

Rationale

Single communication medium means:

  1. Complete State: Snapshot contains everything needed to understand current state.
  2. No Hidden State: Nothing exists outside Snapshot.
  3. Reproducibility: Same Snapshot + Same Intent = Same Result.
  4. Debuggability: Inspect Snapshot at any point to understand everything.

Consequences

EnablesConstrains
Complete state serializationVerbose state structure
Perfect reproducibilityNo "quick" value passing
Time-travel debuggingAll intermediate values in Snapshot
State diffing and comparisonLarger Snapshot size

Canonical Statement

If it's not in Snapshot, it doesn't exist.


FDR-003: No Pause/Resume

Decision

There is no resume() API. Each compute() call is complete and independent.

Context

Initial design had:

typescript
// Old design
const context = { now: 0, randomSeed: "seed" };
const result = await core.compute(schema, snapshot, intent, context);
if (result.status === 'paused') {
  // Execute effects...
  core.resume(result.context, patches);  // Resume suspended computation
}

This implied:

  • Suspended execution context
  • Hidden continuation state
  • Complex lifecycle management

Alternatives Considered

AlternativeDescriptionWhy Rejected
Explicit Resumeresume(context, patches)Implies suspended state exists
Continuation TokensReturn token, pass to continueHidden state in token
Callback RegistrationRegister callback for effect resultTemporal coupling

Rationale

No pause/resume means:

  1. Stateless Core: Core holds no state between calls.
  2. Simple Mental Model: Each compute() is a pure function.
  3. Easy Serialization: No continuation to serialize.
  4. Clear Responsibility: Host owns all lifecycle.

The key insight:

Continuity is expressed through Snapshot, not through execution context.

When an effect is needed:

  1. Core records Requirement in Snapshot.
  2. Core returns.
  3. Host executes effect.
  4. Host applies patches to Snapshot.
  5. Host calls compute() again.
  6. Flow reads result from Snapshot and proceeds.

Consequences

EnablesConstrains
Stateless CoreFlow must check Snapshot for effect results
Simple serializationHost must track which intent to re-dispatch
Easy horizontal scalingSlightly more verbose Flow logic
No memory leaks from suspended contexts

Canonical Statement

There is no suspended execution context. All continuity is expressed through Snapshot.


FDR-004: Effects as Declarations

Decision

Effects are declarations that something external is needed. They are not executions.

Context

The word "effect" in programming usually means "side effect" — something that happens.

In Manifesto, an effect is a requirement declaration:

typescript
// NOT this (execution)
await fetch('/api/data');

// THIS (declaration)
{ kind: 'effect', type: 'api:fetch', params: { url: '/api/data' } }

Alternatives Considered

AlternativeDescriptionWhy Rejected
Direct ExecutionCore executes effectsBreaks purity
Effect Handlers in CoreCore calls injected handlersCouples Core to execution
Promise-basedEffects return PromisesImplies async execution in Core

Rationale

Effects as declarations:

  1. Purity: Core remains pure; no IO inside.
  2. Testability: Test Flow without executing real effects.
  3. Flexibility: Host decides how/when/whether to execute.
  4. Batching: Host can batch multiple effects.
  5. Retry Logic: Host can implement retry without Core knowing.

Consequences

EnablesConstrains
Mock-free testingHost must implement all effect handlers
Effect batchingCannot use familiar async/await in Flows
Retry and circuit breakersMore verbose than inline calls
Effect substitution

Canonical Statement

Core declares requirements. Host fulfills them. Core never executes IO.


FDR-005: Errors as Values

Decision

Errors are values in Snapshot, not exceptions.

Context

Exception-based error handling:

typescript
try {
  await doSomething();
} catch (error) {
  handleError(error);
}

Problems:

  • Control flow is non-local
  • Hard to trace
  • Difficult to serialize
  • Cannot be inspected without catching

Alternatives Considered

AlternativeDescriptionWhy Rejected
Exceptionsthrow/catchNon-local control flow, hard to trace
Result TypesResult<T, E> return valuesImplies value passing, which we rejected
Error CallbacksSeparate error channelTemporal coupling

Rationale

Errors as values:

  1. Traceability: Errors are in Snapshot, visible at any time.
  2. Locality: Error handling is just reading Snapshot.
  3. Serializability: Errors survive serialization.
  4. Explainability: Trace shows when/where error occurred.
json
{
  "system": {
    "lastError": {
      "code": "VALIDATION_ERROR",
      "message": "Title cannot be empty",
      "source": { "actionId": "addTodo", "nodePath": "flow.steps[0]" },
      "timestamp": 1704067200000
    }
  }
}

Consequences

EnablesConstrains
Error inspection without catchingMust check for errors explicitly
Error historySlightly verbose error handling
Serializable error stateNo stack traces (by design)
Error recovery patterns

Canonical Statement

Errors are values. They live in Snapshot. They never throw.


FDR-006: Flow is Not Turing-Complete

Decision

FlowSpec does NOT include unbounded loops (while, for, recursion).

Context

Turing-complete languages can express any computation, but:

  1. Halting Problem: Cannot statically determine if program terminates.
  2. Unbounded Resources: May consume infinite time/memory.
  3. Analysis Difficulty: Cannot prove properties without execution.

Alternatives Considered

AlternativeDescriptionWhy Rejected
Full Turing-CompletenessInclude while/recursionHalting problem, hard to analyze
Bounded Loopsrepeat(n) { ... }Still complex, arbitrary limits
Tail Recursion OnlyAllow TCO recursionComplex implementation, hard to trace

Rationale

By limiting expressiveness:

  1. Guaranteed Termination: All Flows finish in finite steps.
  2. Static Analysis: Can verify properties without execution.
  3. Complete Traces: Trace is always finite.
  4. Predictable Resources: Bounded memory and time.

For unbounded iteration, Host controls the loop:

typescript
const context = { now: 0, randomSeed: "seed" };
while (needsMoreWork(snapshot)) {
  const result = await core.compute(schema, snapshot, intent, context);
  snapshot = result.snapshot;
}

Consequences

EnablesConstrains
Guaranteed terminationCannot express unbounded loops in Flow
Static analysisHost must implement iteration
Bounded resource usageMore code in Host for loops
Complete, finite traces

Canonical Statement

Flows always terminate. Unbounded iteration is Host's responsibility.


FDR-007: No Value Binding in Flows

Decision

Flows do NOT have variable binding (let, bind, ).

Context

Initial design considered:

typescript
// Considered and rejected
{
  kind: 'bind',
  name: 'result',
  value: { kind: 'effect', type: 'api:call', ... },
  then: {
    kind: 'if',
    cond: { kind: 'get', path: 'result.ok' },
    ...
  }
}

This implies:

  • Effects "return" values
  • Local scope exists
  • Hidden state in bindings

Alternatives Considered

AlternativeDescriptionWhy Rejected
Let Bindingslet x = expr in bodyImplies value passing
Pattern Matchingmatch result with ...Complex, implies returned values
Monadic Do-Notationdo { x <- effect; ... }Implies continuation state

Rationale

No value binding enforces the Snapshot principle:

  1. Single Source: All values come from Snapshot.
  2. No Hidden State: No local variables to track.
  3. Explicit State: If you need a value later, put it in Snapshot.
json
{
  "kind": "seq",
  "steps": [
    { "kind": "effect", "type": "api:call", "params": {} },
    { "kind": "if", 
      "cond": { "kind": "get", "path": "api.lastResult.ok" },
      "then": { "...": "..." }
    }
  ]
}

Consequences

EnablesConstrains
Simple mental modelMore paths in Snapshot
Complete state visibilityVerbose for simple value passing
Easy serializationMust design Snapshot carefully
No scope-related bugs

Canonical Statement

If you need a value, read it from Snapshot. There is no other place.


FDR-008: Call Without Arguments

Decision

call invokes another Flow but does NOT pass arguments.

Context

Initial design considered:

typescript
// Considered and rejected
{ kind: 'call', flow: 'validate', input: { kind: 'get', path: 'user' } }

This implies:

  • Parameter passing mechanism
  • Local scope in called Flow
  • Implicit value channel

Alternatives Considered

AlternativeDescriptionWhy Rejected
Argument Passingcall(flow, args)Implies value channel
Input MappingMap input to called flow's expected shapeComplex, hidden transformation
Partial ApplicationPre-bind some argumentsFunctional complexity

Rationale

No arguments enforces:

  1. Snapshot is the only medium: Called Flow reads same Snapshot.
  2. Explicit data flow: If called Flow needs data, write it to Snapshot first.
  3. Traceable: All "arguments" are visible in Snapshot.

Pattern for passing context:

json
{
  "kind": "seq",
  "steps": [
    { "kind": "patch", "op": "set", "path": "system.callContext", 
      "value": { "kind": "get", "path": "input" } },
    { "kind": "call", "flow": "shared.validate" }
  ]
}

Called Flow reads system.callContext.

Consequences

EnablesConstrains
Simple call semanticsMust set up context before call
Traceable "arguments"More patches for context setup
No parameter mismatch bugsSlightly verbose
Consistent mental model

Canonical Statement

call means "continue with another Flow on the same Snapshot." Nothing more.


FDR-009: Schema-First Design

Decision

All semantics are expressed as JSON-serializable Schema, not code.

Context

Code-first approaches:

typescript
// Code-first
const computed = {
  isValid: (state) => state.email.length > 0 && state.password.length >= 8
}

Problems:

  • Cannot serialize
  • Cannot analyze without execution
  • Cannot share across languages
  • Hidden complexity in functions

Alternatives Considered

AlternativeDescriptionWhy Rejected
Code-FirstDefine logic in host languageNot serializable, not portable
DSL with Code EscapeSchema + embedded codeBreaks purity at escape points
HybridSchema for structure, code for logicInconsistent, hard to analyze

Rationale

Schema-first:

  1. Serializable: Store, transmit, version schemas as data.
  2. Analyzable: Static analysis without execution.
  3. Portable: Same schema works in any host language.
  4. Toolable: Generate docs, visualizations, validators.
  5. AI-Friendly: LLMs can read, write, and reason about schemas.

Consequences

EnablesConstrains
Cross-language portabilityMore verbose than code
Static analysisLearning curve for schema language
Schema versioningNeed builder DSL for ergonomics
AI-readable semantics

Canonical Statement

Code is for humans. Schema is for machines. Manifesto speaks Schema.


FDR-010: Canonical Form and Hashing

Decision

Every Schema has a canonical form and a deterministic hash.

Context

For content-addressable storage and integrity:

  • Same content should produce same hash
  • Order of keys should not matter
  • Formatting should not matter

Alternatives Considered

AlternativeDescriptionWhy Rejected
JSON as-isHash raw JSONKey order affects hash
Schema ID onlyUse ID as identifierCannot detect content changes
Custom Binary FormatCompact binary representationComplexity, debugging difficulty

Rationale

Canonical form + hashing enables:

  1. Deduplication: Same content = same hash = store once.
  2. Integrity: Detect tampering or corruption.
  3. Caching: Memoize computation by schema hash.
  4. Versioning: Track schema evolution.
  5. Comparison: Diff schemas semantically.

Algorithm:

  1. Sort all object keys alphabetically (recursive).
  2. Remove undefined values.
  3. Serialize as JSON with no whitespace.
  4. Hash with SHA-256.

Consequences

EnablesConstrains
Content-addressable storageMust normalize before hashing
Integrity verificationSlightly more processing
Deterministic caching
Schema comparison

Canonical Statement

Same meaning, same hash. Always.


FDR-011: Computed as DAG

Decision

Computed values form a Directed Acyclic Graph (DAG).

Context

Computed values depend on other values:

activeCount depends on todos
canClear depends on completedCount
completedCount depends on todos

Cycles would cause:

  • Infinite computation
  • Undefined order
  • Non-deterministic results

Alternatives Considered

AlternativeDescriptionWhy Rejected
Allow CyclesIterate until stableNon-termination risk
Lazy EvaluationCompute on demandHidden computation order
Flat ComputedNo computed-to-computed depsToo limiting

Rationale

DAG ensures:

  1. Termination: Topological order guarantees finite computation.
  2. Determinism: Same order every time.
  3. Incremental Update: Only recompute affected nodes.
  4. Static Verification: Detect cycles at schema validation time.

Consequences

EnablesConstrains
Guaranteed terminationCannot express circular dependencies
Incremental recomputationMust structure computed carefully
Static cycle detection
Predictable evaluation order

Canonical Statement

Computed values flow downward. They never cycle back.


FDR-012: Patch Operations Limited to Three

Decision

Only three patch operations: set, unset, merge.

Context

Rich patch operations could include:

  • Array push/pop/splice
  • Object deep merge
  • Increment/decrement
  • Custom transformations

Alternatives Considered

AlternativeDescriptionWhy Rejected
JSON Patch (RFC 6902)add, remove, replace, move, copy, testComplex, move/copy are confusing
Rich Array Operationspush, pop, splice, etc.Can be composed from set
Deep MergeRecursive object mergeSemantic ambiguity

Rationale

Three operations cover all cases:

  1. set: Replace value at path.
  2. unset: Remove value at path.
  3. merge: Shallow merge object at path.

Array operations are expressed as:

json
// Push
{ "op": "set", "path": "items", "value": { "kind": "concat", "args": [{ "kind": "get", "path": "items" }, [newItem]] } }

// Remove at index
{ "op": "set", "path": "items", "value": { "kind": "filter", ... } }

Benefits:

  1. Simplicity: Easy to implement in any language.
  2. Predictability: Clear semantics, no edge cases.
  3. Composability: Complex operations from simple primitives.

Consequences

EnablesConstrains
Simple implementationArray operations more verbose
Clear semanticsNo atomic increment
Easy verificationMust compose from primitives
Portable across hosts

Canonical Statement

Three operations are enough. Complexity is composed, not built-in.


FDR-013: Host Responsibility Boundary

Decision

Clear boundary between Core (compute) and Host (execute).

Context

Without clear boundary:

  • Testing requires mocking
  • Behavior depends on environment
  • Non-determinism creeps in

Responsibilities

Core (Compute)Host (Execute)
Evaluate expressionsExecute effects
Apply patches to snapshotPerform IO
Generate tracesControl loops
Validate schemasManage persistence
Record requirementsHandle user interaction
Implement effect handlers

Rationale

Clear boundary:

  1. Testability: Core is pure, test without mocks.
  2. Portability: Same Core, different Hosts.
  3. Flexibility: Host can implement any execution strategy.
  4. Predictability: Core behavior is deterministic.

Consequences

EnablesConstrains
Pure Core testingHost has more responsibilities
Multi-platform CoreMust define clear interface
Execution strategy flexibilityTwo components to maintain
Deterministic replay

Canonical Statement

Core is pure and portable. Host is practical and platform-specific.


FDR-014: Browser Compatibility

Decision

All packages in the Manifesto stack MUST use browser-compatible APIs only. Node.js-specific APIs are forbidden.

Context

The Manifesto stack was initially developed in Node.js, using Node.js-specific APIs like crypto.createHash() for SHA-256 hashing in the Compiler's IR generator.

When building a React application with Vite for browser deployment, this caused a critical runtime error:

Uncaught Error: Module "crypto" has been externalized for browser compatibility.
Cannot access "crypto.createHash" in client code.

Dependency chain:

React App (browser)
    → @manifesto-ai/app
        → @manifesto-ai/host
            → @manifesto-ai/compiler (build-time only expected)
                → crypto.createHash() ← FAILS in browser

Alternatives Considered

AlternativeDescriptionWhy Rejected
Bundle Node.js polyfillsInclude crypto-browserifyIncreases bundle size significantly
Make Compiler Node-onlyStrict boundary at build timeBreaks runtime MEL compilation use cases
Use Web Crypto APIcrypto.subtle.digest()Async-only, not suitable for synchronous computeHash()
Pure JS ImplementationJavaScript-only SHA-256Chosen: Works everywhere, deterministic

Rationale

Using browser-compatible APIs ensures:

  1. Universal Execution: Core, Host, Compiler can run in browser, Node.js, Deno, Bun, edge workers.
  2. No Polyfills: Zero external crypto polyfills needed.
  3. Bundle Size: No bloated crypto libraries.
  4. Build Simplicity: No special Vite/Webpack configuration for Node.js modules.

Implementation:

typescript
// BEFORE (Node.js only)
import { createHash } from "crypto";
function computeHash(schema: Omit<DomainSchema, "hash">): string {
  return createHash("sha256").update(toCanonical(schema)).digest("hex");
}

// AFTER (Browser-compatible)
import { sha256Sync, toCanonical } from "@manifesto-ai/core";
function computeHash(schema: Omit<DomainSchema, "hash">): string {
  return sha256Sync(toCanonical(schema));
}

@manifesto-ai/core provides:

  • sha256Sync(data): Pure JavaScript, synchronous
  • sha256(data): Web Crypto API, async (for larger payloads)

Consequences

EnablesConstrains
Browser React appsSlightly slower than native crypto
Edge worker deploymentMust use Core's hash utilities
No build configurationCannot use Node.js crypto directly
Universal package compatibility

Canonical Statement

Manifesto runs everywhere. Browser compatibility is non-negotiable.


FDR-015: Static Patch Paths

Decision

Patch paths MUST be statically resolvable at apply-time. core.apply() does NOT evaluate expressions in paths.

Context

MEL syntax allows dynamic path expressions:

mel
// MEL allows this syntax
patch items[$system.uuid] = { id: $system.uuid, name: "New Item" }

However, when this is lowered to IR and eventually reaches core.apply(), the path cannot contain unresolved expressions.

The question arose: Should core.apply() evaluate path expressions?

Alternatives Considered

AlternativeDescriptionWhy Rejected
Dynamic Path Evaluationapply() evaluates path expressions at runtimeBreaks purity, determinism, introduces hidden execution
Compiler-time ResolutionCompiler resolves all paths staticallyImpossible for runtime values like $system.uuid
Two-phase LoweringCompiler lowers to effect + static patchChosen: Maintains purity, explicit IO

Rationale

If core.apply() evaluated dynamic paths:

  1. Determinism Lost: Path depends on snapshot state at apply-time
  2. Purity Violated: $system.uuid requires IO (it's non-deterministic)
  3. Hidden Execution: Path construction becomes an implicit computation
  4. Replay Broken: Same IR with different state → different paths → different outcomes

By requiring static paths:

  1. Core Stays Pure: No expression evaluation in apply()
  2. IO is Explicit: Dynamic values come through Effects
  3. Replay Works: Same snapshot + same patches = same result
  4. Traceability: Every path is visible in the Patch

Implementation Pattern

Dynamic paths in MEL MUST be lowered to a two-step pattern:

mel
// STEP 1: Fix the dynamic value to Snapshot
once(creating) {
  patch creating = $meta.intentId
  patch newId = $system.uuid  // Effect: system.get → stores UUID in snapshot
}

// STEP 2: Use the now-static value from Snapshot
when isNotNull(newId) {
  // At this point, newId is a known string value in Snapshot
  // Compiler/Host can resolve `items[newId]` to `items.abc-123`
  patch items[newId] = { id: newId, ... }
}

The second patch becomes static because:

  1. newId is a state field (not $system.*)
  2. Its value is known after Step 1
  3. The compiler/host can resolve items[newId] to items.{actual-value}

Consequences

EnablesConstrains
Pure apply() functionTwo-step pattern for dynamic keys
Deterministic replayCompiler must handle lowering
Transparent path constructionMore verbose MEL for dynamic cases
No hidden computationUsers must understand the pattern

Canonical Statement

Patch paths are data, not computation. Dynamic resolution is IO, and IO belongs to Host.


Summary: The Manifesto Identity

These design decisions collectively define what Manifesto IS:

Manifesto IS:
  ✓ A semantic calculator for domain state
  ✓ Schema-first and JSON-serializable
  ✓ Deterministic and reproducible
  ✓ Explainable at every step
  ✓ Pure (no side effects in Core)
  ✓ Host-agnostic

Manifesto IS NOT:
  ✗ An execution runtime
  ✗ A Turing-complete language
  ✗ An exception-throwing system
  ✗ A framework with hidden state
  ✗ A workflow orchestrator
  ✗ An agent framework

The One-Sentence Summary

Manifesto computes what the world should become; Host makes it so.

The Fundamental Equation

compute(schema, snapshot, intent, context) → (snapshot', requirements, trace)

This equation is:

  • Pure: Same inputs always produce same outputs.
  • Total: Always returns a result (never throws).
  • Traceable: Every step is recorded.
  • Complete: Snapshot is the whole truth.

Appendix: Decision Dependency Graph

FDR-001 (Core as Calculator)

    ├─► FDR-002 (Snapshot as Only Medium)
    │       │
    │       ├─► FDR-003 (No Pause/Resume)
    │       │
    │       ├─► FDR-007 (No Value Binding)
    │       │
    │       └─► FDR-008 (Call Without Arguments)

    ├─► FDR-004 (Effects as Declarations)
    │       │
    │       └─► FDR-013 (Host Responsibility Boundary)

    ├─► FDR-005 (Errors as Values)

    └─► FDR-006 (Flow Not Turing-Complete)

FDR-009 (Schema-First)

    ├─► FDR-010 (Canonical Form)

    └─► FDR-011 (Computed as DAG)

FDR-012 (Three Patch Operations)

    └─► FDR-015 (Static Patch Paths)

FDR-015 (Static Patch Paths)

    ├─► Depends on FDR-001 (Core is pure, no execution in apply)
    ├─► Depends on FDR-002 (All info through Snapshot)
    └─► Depends on FDR-004 (Dynamic values = IO = Effects)

End of FDR Document