diff --git a/skills/open-prose/contract-markdown.md b/skills/open-prose/contract-markdown.md index 12ff1df0..bd6948ca 100644 --- a/skills/open-prose/contract-markdown.md +++ b/skills/open-prose/contract-markdown.md @@ -224,6 +224,7 @@ Forme and the Prose VM recognize these `###` sections case-insensitively: | `### Fixtures` | test | Test inputs supplied without prompting | | `### Expects` | test | Positive natural-language assertions | | `### Expects Not` | test | Negative natural-language assertions | +| `### Patterns` | responsibility, function | Instantiates patterns into named nodes via fenced yaml (- name, pattern:, with:, config:). Expanded by Forme at compile time | | `### Slots` | pattern | Responsibilities or functions a pattern requires from its caller | | `### Config` | pattern | Pattern-level parameters and defaults | | `### Delegation` | pattern | ProseScript or pseudocode describing slot interaction | @@ -587,12 +588,26 @@ loop up to config.max_rounds: ``` ```` -A pattern is instantiated with a fenced `yaml` declaration. Use `with:` for slot -bindings and `config:` for pattern parameters: +A pattern is instantiated inside the canonical **`### Patterns`** section of a +responsibility or function using a fenced `yaml` block. + +### Canonical Instantiation Syntax (`### Patterns`) + +The declaration connects five elements: +1. **`use`**: Imports an external or standard library pattern at the file header (e.g. `use "std/patterns/worker-critic"`). When imported, the pattern can be referenced by its local alias (`worker-critic`). Alternatively, specify the full path directly in `pattern:`. +2. **`name:`**: Assigns a local identifier to the expanded node instance. This name is what ProseScript invokes via `call {name}` in `### Execution`, or what Forme wires as a node in the DAG. +3. **`pattern:`**: Identifies the pattern contract (`kind: pattern`). Can be a local contract filename, an imported alias, a standard library shorthand (`std/patterns/...`), or a full git host URI. +4. **`with:`**: Binds each slot declared in the pattern's `### Slots` to a concrete responsibility, function, or nested pattern instance. Forme validates that all required slots are filled. +5. **`config:`**: Supplies parameters that override the defaults declared in the pattern's `### Config`. + +```markdown +use "std/patterns/worker-critic" + +### Patterns ```yaml - name: reviewed-draft - pattern: std/patterns/worker-critic + pattern: worker-critic with: worker: writer critic: reviewer @@ -600,10 +615,23 @@ bindings and `config:` for pattern parameters: max_rounds: 3 ``` -`pattern:` names a `kind: pattern` file. `with:` binds slots to responsibilities, -functions, or nested pattern instances. After expansion, the named instance -behaves like a node. Nested pattern declarations are allowed only as slot values -inside another pattern instance's `with:` block. +### Execution + +```prose +let final_doc = call reviewed-draft + topic: topic + guidelines: guidelines + +return { document: final_doc } +``` +``` + +### Slot Binding and Nesting + +- `with:` binds slots to responsibilities, functions, or nested pattern instances. +- Nested pattern declarations are allowed only as slot values inside another pattern instance's `with:` block. Expansion proceeds inside-out. +- Recursive patterns are strictly prohibited. +- For compile-time resolution, slot validation, and expansion mechanics, see `forme.md#pattern-expansion`. ## Structured Blocks diff --git a/skills/open-prose/forme.md b/skills/open-prose/forme.md index 4d7bcf39..345bcefc 100644 --- a/skills/open-prose/forme.md +++ b/skills/open-prose/forme.md @@ -246,6 +246,56 @@ topology's `edges` to schedule and propagate. --- +## Pattern Expansion + +Patterns are compile-time abstractions: slots, configuration parameters, invariants, +and delegation rules that describe how a set of roles or renders coordinate. By the +time the run-phase reconciler executes, patterns are completely expanded into +concrete nodes and delegation steps. + +### The Expansion Lifecycle + +When Forme processes a contract containing a `### Patterns` section, it performs +compile-time expansion in five steps: + +1. **Resolution:** + Forme resolves the pattern named in `pattern:`. It first checks for a local + `kind: pattern` contract in the package or repository. If not found locally, it + resolves the pattern reference through installed dependencies (under + `/deps/` per `deps.md`) or standard library shorthands + (`std/patterns/...`). An explicit `use "std/patterns/{name}"` declaration + establishes a local alias that `pattern:` may reference by its unqualified name. + +2. **Slot Validation:** + Forme inspects the target pattern's declared `### Slots`. Every declared slot + must be bound in the instance's `with:` block. Forme verifies that each bound + slot references a valid responsibility, function, or nested pattern instance + available in scope. Unbound or mismatched slots are surfaced as compile-time + diagnostics (`unsatisfied-slot`). + +3. **Config Validation:** + Forme validates the instance's `config:` block against the pattern's declared + `### Config` section. Any declared defaults in `### Config` are applied for + omitted optional parameters; missing required values or type mismatches trigger + compile-time validation errors. + +4. **Instantiation & Delegation Lowering:** + The instance's `name:` becomes the assigned identifier for the expanded unit. + Forme lowers the pattern's `### Delegation` block (ProseScript) into the + concrete execution logic of the expanded unit, substituting the bound slot + references and validated config values. In `### Execution`, callers invoke + this expanded unit via ProseScript `call {name}`. + +5. **Nesting and Cycle Prevention:** + Patterns nest cleanly: a slot in `with:` may be filled by another inline + pattern instantiation. Forme resolves and expands nested patterns **inside-out** + (the innermost leaf pattern expands first, then supplies its expanded node to + the outer slot). **Recursive patterns are strictly prohibited:** Forme checks + that pattern instantiations form a directed acyclic hierarchy and halts + compilation if any pattern directly or transitively references itself. + +--- + ## What Forme retired Forme used to be a SKILL-phase dependency-injection container that wired diff --git a/skills/open-prose/prose.md b/skills/open-prose/prose.md index 445855ae..f57b34cd 100644 --- a/skills/open-prose/prose.md +++ b/skills/open-prose/prose.md @@ -1265,15 +1265,20 @@ A pattern file declares its pattern with Contract Markdown sections. Understandi ### Instantiation -Authors instantiate patterns with explicit slot-filling: `pattern:` names the -pattern, `with:` binds slots, and `config:` sets pattern parameters. Instances +Authors instantiate patterns inside the canonical `### Patterns` section with +explicit slot-filling: `name:` assigns the instance identifier, `pattern:` names +the pattern, `with:` binds slots, and `config:` sets pattern parameters. Instances are declared in contract files and expanded by Forme at compile time into -concrete nodes. Nested pattern declarations may appear only as -slot values inside a pattern instance's `with:` block. -For instantiation syntax, see `contract-markdown.md` (Patterns) and `forme.md`, -Pattern Expansion. No shorthand pattern syntax is accepted at runtime. +concrete nodes. Nested pattern declarations may appear only as slot values inside +a pattern instance's `with:` block. -Patterns nest — a slot can be filled by another pattern instantiation. Expansion proceeds inside-out. Recursive patterns are prohibited. For nesting examples, see `forme.md`, Pattern Expansion. +For instantiation syntax, `use` aliases, and complete examples, see +`contract-markdown.md` (Patterns) and `forme.md`, Pattern Expansion. No shorthand +pattern syntax is accepted at runtime. + +Patterns nest — a slot can be filled by another pattern instantiation. Expansion +proceeds inside-out. Recursive patterns are prohibited. For nesting examples and +expansion rules, see `forme.md`, Pattern Expansion. ### Patterns in the Manifest diff --git a/spec/01-Language.md b/spec/01-Language.md index 64594a99..0c864eb3 100644 --- a/spec/01-Language.md +++ b/spec/01-Language.md @@ -438,6 +438,7 @@ Canonical sections include: | `### Shape` | responsibility, function | Capability boundaries: self, delegates, prohibited work | | `### Execution` | responsibility, function | ProseScript render body that pins choreography | | `### Fixtures` / `### Expects` / `### Expects Not` | test | Test data and assertions | +| `### Patterns` | responsibility, function | Pattern instantiation declarations (- name:, pattern:, with:, config:) | | `### Slots` / `### Config` / `### Delegation` | pattern | Pattern interface and algorithm | | `### Schedule` / `### Receives` / `### Emits` / `### Payload` | gateway | Time/event ingress declarations | @@ -459,8 +460,11 @@ Header hierarchy is part of the language, and the `####` level is now load-beari Composition has exactly two forms (there is no `system` graph kind): **intra-node** choreography is a ProseScript `call` inside one render's `### Execution`; **cross-node** composition is a Forme-wired `### Requires` ↔ `### Maintains` -subscription between responsibilities. Pattern instances remain current YAML -syntax, declared in a responsibility's slots: +subscription between responsibilities. Pattern instances are declared in the +canonical `### Patterns` section with fenced YAML syntax: + +```markdown +### Patterns ```yaml - name: reviewed-output @@ -471,6 +475,7 @@ syntax, declared in a responsibility's slots: config: max_rounds: 3 ``` +``` ### Forme diff --git a/tests/open-prose/contract-markdown/patterns.test.ts b/tests/open-prose/contract-markdown/patterns.test.ts new file mode 100644 index 00000000..6cca732a --- /dev/null +++ b/tests/open-prose/contract-markdown/patterns.test.ts @@ -0,0 +1,83 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = fileURLToPath(new URL("../../../", import.meta.url)); + +function read(rel: string): string { + return readFileSync(join(repoRoot, rel), "utf8"); +} + +function flat(rel: string): string { + return read(rel).replace(/\s+/g, " "); +} + +const CONTRACT_MD = "skills/open-prose/contract-markdown.md"; +const FORME_MD = "skills/open-prose/forme.md"; +const PROSE_MD = "skills/open-prose/prose.md"; +const SPEC_LANG = "spec/01-Language.md"; +const SMOKE_PATTERN = "tests/open-prose/smoke/09-local-pattern.prose.md"; + +describe("pattern reference and instantiation syntax canonicalization (Issue #170)", () => { + it("includes ### Patterns in Canonical Sections table of contract-markdown.md", () => { + const source = read(CONTRACT_MD); + const canonicalTable = source.slice( + source.indexOf("## Canonical Sections"), + source.indexOf("### Folded and deleted sections"), + ); + expect(canonicalTable).toContain("### Patterns"); + expect(canonicalTable).toMatch(/responsibility,\s*function/); + expect(canonicalTable).toMatch(/fenced yaml/i); + }); + + it("includes ### Patterns in Canonical Sections table of spec/01-Language.md", () => { + const source = read(SPEC_LANG); + const canonicalTable = source.slice( + source.indexOf("Canonical sections include:"), + source.indexOf("The retired judge-era sections"), + ); + expect(canonicalTable).toContain("### Patterns"); + expect(canonicalTable).toMatch(/Pattern instantiation declarations/i); + }); + + it("defines use, pattern:, with:, and config: together with an end-to-end example", () => { + const doc = flat(CONTRACT_MD); + expect(doc).toMatch(/### Canonical Instantiation Syntax \(`### Patterns`\)/i); + expect(doc).toContain("use"); + expect(doc).toContain("pattern:"); + expect(doc).toContain("with:"); + expect(doc).toContain("config:"); + expect(doc).toMatch(/call reviewed-draft/); + expect(doc).toMatch(/forme\.md#pattern-expansion/); + }); + + it("repairs broken Forme references: forme.md contains ## Pattern Expansion", () => { + const formeDoc = read(FORME_MD); + expect(formeDoc).toContain("## Pattern Expansion"); + + const flatForme = flat(FORME_MD); + expect(flatForme).toMatch(/Resolution:/i); + expect(flatForme).toMatch(/Slot Validation:/i); + expect(flatForme).toMatch(/Config Validation:/i); + expect(flatForme).toMatch(/Instantiation & Delegation Lowering:/i); + expect(flatForme).toMatch(/inside-out/i); + expect(flatForme).toMatch(/Recursive patterns are strictly prohibited/i); + }); + + it("prose.md cross-references resolve to contract-markdown.md and forme.md", () => { + const proseDoc = flat(PROSE_MD); + expect(proseDoc).toMatch(/canonical `### Patterns` section/i); + expect(proseDoc).toMatch(/`contract-markdown\.md`\s*\(Patterns\)/i); + expect(proseDoc).toMatch(/`forme\.md`,\s*Pattern Expansion/i); + }); + + it("smoke test 09-local-pattern.prose.md conforms to canonical ### Patterns section", () => { + const smokeDoc = read(SMOKE_PATTERN); + expect(smokeDoc).toContain("### Patterns"); + expect(smokeDoc).toContain("pattern: worker-critic"); + expect(smokeDoc).toContain("with:"); + expect(smokeDoc).toContain("config:"); + expect(smokeDoc).toContain("call reviewed-result"); + }); +});