Create a New COPF Concept
Design and implement a new concept named $ARGUMENTS following Daniel Jackson's concept design methodology from "The Essence of Software."
Core Design Principles (Always Apply)
Before writing any code, internalize these three principles:
- •Singularity — Every concept has exactly ONE purpose. If you find yourself writing "and also..." in the purpose, you need two concepts.
- •Independence — A concept can be understood entirely on its own. It never references another concept's types or calls another concept's actions. Use type parameters for polymorphism.
- •Sufficiency & Necessity — State must contain everything actions need (sufficiency), and nothing they don't (necessity). If a field is never read by any action, remove it.
Step-by-Step Design Process
Step 1: Articulate the Purpose
The purpose answers: What is this concept for? Not what it does — what it's for.
Write 1-3 sentences. Use imperative present tense. Be specific about the value delivered.
Read references/jackson-methodology.md for Jackson's full methodology on purpose, the operational principle, and the three design criteria.
Purpose checklist:
- • Can you state the purpose in one sentence?
- • Does it describe a why, not just a what?
- • Would a user understand this purpose without seeing the implementation?
- • Is there exactly ONE purpose, not two stitched together?
Purpose wording patterns from this codebase:
| Pattern | Example |
|---|---|
| Entity management | "Manage articles with slugs, titles, bodies, and metadata." |
| Relationship tracking | "Track follower relationships between users." |
| Secure operation | "Securely store and validate user credentials using salted hashing." |
| Transformation | "Transform parsed concept ASTs into language-neutral ConceptManifests." |
| Generation | "Generate TypeScript skeleton code from a ConceptManifest." |
Step 2: Identify the Type Parameter
Every COPF concept is parameterized by exactly one type parameter — the primary entity or resource it manages. This makes the concept polymorphic and independent.
Choose a single uppercase letter:
- •U for user-keyed concepts (Password, Profile, Follow, JWT)
- •A for article-like content entities
- •C for comments or items
- •T for tags or tokens
- •S for specs or schemas
- •F for flows
- •R for records
- •M for messages or manifests
The type parameter appears in state relations and action signatures. On the wire, it is always an opaque string identifier.
Step 3: Design the State
Read references/state-design.md for the complete state design reference.
State is what the concept remembers. Apply these rules:
- •Start from the purpose — What must the concept remember to fulfill its purpose?
- •Primary collection — Does the concept manage a set of entities? If yes, declare
items: set T(orusers: set U, etc.) - •Properties as relations — Each property of an entity becomes a relation:
title: A -> String - •User-to-many mappings — For relationships:
following: U -> set String - •No dead state — Every field must be read or written by at least one action
State checklist:
- • Every action's input can be served from this state
- • Every action's output can be produced from this state
- • No field is unused by all actions
- • Type parameter is used in at least one relation
- • State is visible/understandable to users (not internal bookkeeping)
Step 4: Design the Actions
Read references/action-design.md for action design patterns and variant conventions.
Actions are the concept's API. Each action:
- •Reads and/or modifies state
- •Returns a discriminated union of variants (at minimum
ok) - •Has a prose description explaining when each variant occurs
Action design rules:
- •Complete coverage — For every state field, there must be at least one action that writes it and one that reads it
- •Return variants —
okis always first. Addnotfoundwhen looking up by ID,errorfor validation failures,invalidfor constraint violations - •Mutations return the entity —
-> ok(article: A)echoes back the entity ID - •Queries return the data —
-> ok(following: Bool)returns the queried value - •Prose in variants — Describe the condition for each variant, not the implementation
Step 5: Write the Operational Principle (Invariants)
Read references/invariant-design.md for invariant patterns, anti-patterns, and detailed guidance.
The operational principle is a "defining story" — a scenario that proves the concept fulfills its purpose. In COPF, these are expressed as invariant blocks.
Every concept MUST have at least one invariant. A concept without invariants has no machine-verifiable behavioral contract and cannot generate meaningful conformance tests.
invariant {
after <setup-action>(...) -> ok(...)
then <verification-action>(...) -> ok(...)
and <additional-check>(...) -> <expected-variant>(...)
}
What makes an invariant meaningful
A meaningful invariant would catch a real bug if the implementation were broken. The litmus test: if you replaced the handler with a stub that always returns { variant: 'ok' }, would this invariant fail? If not, it's testing nothing.
Three rules for meaningful invariants:
- •
Exercise real logic — Input must be rich enough to pass through the handler's actual code paths, not just hit a guard clause. For a code generator that iterates over
manifest.actions[].variants[], you need at least one action with one variant. An emptyactions: []tests nothing. - •
Test different behaviors — The
afterandthensteps must exercise different code paths. Good pairings:- •Create → Query (state was stored correctly)
- •Register → Match (registration enables matching)
- •Valid input → Invalid input (happy path vs error path)
- •Bad pairings: same action twice with slightly different strings, or two identical error checks
- •
Match the handler's interface — Field names and nesting must match what the handler code actually reads. If the handler reads
variant.tag, don't writetag: "ok"in the spec butname: "ok"in the invariant. Read the handler code or type definitions.
Common anti-patterns to avoid
// BAD: Guard clause only — both steps do the same thing
invariant {
after generate(spec: "x") -> ok(manifest: m)
then generate(spec: "y") -> ok(manifest: n)
}
// BAD: Degenerate input — empty object skips all logic
invariant {
after generate(spec: "s1", ast: {}) -> error(message: e1)
then generate(spec: "s2", ast: {}) -> error(message: e2)
}
Domain concept patterns
| Pattern | What it proves | Example |
|---|---|---|
| Create → Query | "What you store is what you get back" | Article: create then get |
| Mutate → Verify → Reverse | "Changes are observable and reversible" | Follow: follow → isFollowing → unfollow |
| Set → Check correct → Check wrong | "Validation distinguishes good from bad" | Password: set → check right → check wrong |
| Create → Create duplicate | "Constraints are enforced" | User: register → register same name |
Framework/infrastructure concept patterns
For concepts that process structured data (ASTs, manifests, configs), use record { } and list [ ] literals:
invariant {
after generate(spec: "s1", manifest: {
name: "Ping", uri: "urn:copf/Ping", typeParams: [],
actions: [{ name: "ping", params: [],
variants: [{ tag: "ok", fields: [] }] }],
invariants: [], graphqlSchema: "",
jsonSchemas: { invocations: {}, completions: {} },
capabilities: [], purpose: "A test."
}) -> ok(files: f)
then generate(spec: "s2", manifest: { name: "" }) -> error(message: e)
}
The first step passes the smallest complete input that exercises the handler's core logic. The second step passes structurally broken input.
For multi-step flow concepts (SyncEngine, FlowTrace), the second step should depend on state from the first:
invariant {
after registerSync(sync: { ... when-clause matching concept A ... }) -> ok()
then onCompletion(completion: { concept: A, action: "act", ... }) -> ok(invocations: inv)
}
Final quality check
Before finalizing, answer these questions:
- •Would a trivial stub handler pass this invariant? It must not.
- •Does the first step input include enough structure to exercise the handler's core transformation/logic? Not just guard clauses.
- •Does the second step test genuinely different behavior from the first? Not the same call with a different string.
- •Do field names exactly match the types the handler reads? Read the code or types.
Step 6: Declare Capabilities (If Needed)
Only add capabilities if the concept requires external runtime support:
capabilities {
requires crypto // Needs cryptographic operations
requires persistent-storage // Needs durable storage
}
Most concepts don't need this section.
Step 7: Check Against Anti-Patterns
Read references/anti-patterns.md for common mistakes.
Run through this final checklist:
- • Not overloaded — Concept has exactly one purpose
- • Not over-scoped — No state fields or actions that serve a different purpose
- • Not coupled — No references to other concept types (use type params instead)
- • Not under-specified — All state fields are covered by actions
- • Not missing invariants — At least one operational principle for EVERY concept (domain and framework)
- • Proper naming — Actions use verb-first names, variants use lowercase tags
Step 8: Write the .concept File
Place the file at:
- •
specs/app/<name>.conceptfor domain/application concepts - •
specs/framework/<name>.conceptfor infrastructure/framework concepts
See references/concept-grammar.md for the complete grammar reference.
Section order is always:
- •
purpose { ... } - •
state { ... } - •
capabilities { ... }(optional) - •
actions { ... } - •
invariant { ... }(one block per invariant)
Step 9: Validate by Parsing
Run the parser to verify your concept is syntactically valid:
npx tsx -e "
import { readFileSync } from 'fs';
import { parseConceptFile } from './implementations/typescript/framework/spec-parser.impl.js';
const source = readFileSync('specs/<domain>/<name>.concept', 'utf-8');
const ast = parseConceptFile(source);
console.log('Parsed:', ast.name);
console.log('Type params:', ast.typeParams);
console.log('State fields:', ast.state.length);
console.log('Actions:', ast.actions.map(a => a.name));
console.log('Invariants:', ast.invariants.length);
console.log('Capabilities:', ast.capabilities);
"
Step 10: Generate Schema and Code
Run the full pipeline to verify your concept produces valid output:
npx tsx -e "
import { readFileSync } from 'fs';
import { parseConceptFile } from './implementations/typescript/framework/spec-parser.impl.js';
import { createInMemoryStorage } from './kernel/src/storage.js';
import { schemaGenHandler } from './implementations/typescript/framework/schema-gen.impl.js';
import { typescriptGenHandler } from './implementations/typescript/framework/typescript-gen.impl.js';
const source = readFileSync('specs/<domain>/<name>.concept', 'utf-8');
const ast = parseConceptFile(source);
// Generate manifest
const s1 = createInMemoryStorage();
const schema = await schemaGenHandler.generate({ spec: 'test', ast }, s1);
if (schema.variant !== 'ok') { console.error('Schema error:', schema.message); process.exit(1); }
console.log('Manifest URI:', schema.manifest.uri);
console.log('Relations:', schema.manifest.relations.length);
console.log('Actions:', schema.manifest.actions.map(a => a.name));
// Generate TypeScript
const s2 = createInMemoryStorage();
const code = await typescriptGenHandler.generate({ spec: 'test', manifest: schema.manifest }, s2);
if (code.variant !== 'ok') { console.error('Codegen error:', code.message); process.exit(1); }
for (const f of code.files) {
console.log('---', f.path, '---');
console.log(f.content.slice(0, 200) + '...');
}
"
Step 11: Run Full Check
npx tsx tools/copf-cli/src/index.ts check
This validates all concepts in the project parse correctly.
Quick Reference: Concept Structure
concept Name [T] {
purpose {
One to three sentences explaining what this concept is FOR.
Not what it does — what value it delivers.
}
state {
items: set T // Primary entity collection
property: T -> String // Entity property (single value)
relation: T -> set String // Entity to many (set of IDs)
metadata: T -> DateTime // Typed property
}
capabilities {
requires <capability> // Only if needed
}
actions {
action create(item: T, property: String) {
-> ok(item: T) {
What happens on success.
}
-> error(message: String) {
When this variant is returned.
}
}
action get(item: T) {
-> ok(item: T, property: String) {
Returns the item's data.
}
-> notfound(message: String) {
If the item does not exist.
}
}
}
invariant {
after create(item: x, property: "test") -> ok(item: x)
then get(item: x) -> ok(item: x, property: "test")
}
}
Example Walkthroughs
For complete examples with design rationale:
- •examples/domain-concepts.md — Password, Follow, Article, User
- •examples/framework-concepts.md — SchemaGen, Registry, SyncEngine
Related Skills
| Skill | When to Use |
|---|---|
/create-concept-kit | Bundle multiple related concepts into a reusable kit |
/create-sync | Write sync rules that connect this concept to others |
/create-implementation | Write the TypeScript implementation for this concept |
/decompose-feature | Break down a feature into concepts before designing each one |