Skip to content

Commit 3bd72d8

Browse files
committed
Update README with correct signatures and add examples directory
1 parent a8cdec0 commit 3bd72d8

7 files changed

Lines changed: 367 additions & 12 deletions

File tree

README.md

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# promptyst
22

3-
A contract-first Typst DSL for structured AI prompts.
3+
A contract-first Typst DSL for structured AI prompts.
44
Five primitives. Deterministic Markdown output. No runtime dependencies.
55

66
---
@@ -19,13 +19,13 @@ Exactly five. No others.
1919

2020
| Constructor | Returns |
2121
|-------------|---------|
22-
| `p-context(id, entries)` | context dict |
23-
| `p-schema(id, fields)` | schema dict |
24-
| `p-checkpoint(id, after-step, assertion, on-fail)` | checkpoint dict |
25-
| `p-chat-mode(id, turns, state, prompt)` | chat-mode dict |
26-
| `p-prompt(id, version, role, ctx, constraints, steps, inputs, schema, checkpoints?)` | prompt dict |
22+
| `p-context(id: str, entries: array)` | context dict |
23+
| `p-schema(id: str, fields: array)` | schema dict |
24+
| `p-checkpoint(id: str, after-step: int, assertion: str, on-fail: str)` | checkpoint dict |
25+
| `p-chat-mode(id: str, turns: str, state: str, prompt: dict)` | chat-mode dict |
26+
| `p-prompt(id: str, version: str, role: str, ctx: dict, constraints: array, steps: array, inputs: array, schema: dict, checkpoints?: array)` | prompt dict |
2727

28-
All constructors return plain Typst dictionaries. No rendering occurs at construction time.
28+
All parameters are **named** (keyword arguments). All constructors return plain Typst dictionaries. No rendering occurs at construction time.
2929

3030
---
3131

@@ -43,6 +43,56 @@ All renderers are pure functions. Same input always produces byte-identical outp
4343

4444
---
4545

46+
## TOML Ingestion
47+
48+
```typst
49+
#let result = from-toml(read("my-prompt.toml"))
50+
#raw(render-prompt(result.prompt), lang: "markdown")
51+
```
52+
53+
`from-toml(raw)` parses a TOML string and returns a dictionary. If all required sections are present, the `prompt` key contains a fully assembled prompt dict. Partial TOML (e.g. only `[context]`) returns only the sections found — no panic for missing sections.
54+
55+
Available keys in the result: `aspect`, `context`, `schema`, `constraints`, `steps`, `inputs`, `checkpoints`, `prompt`, `meta`, `constraints-meta`.
56+
57+
Metadata (`[rationale]`, constraint `severity`) is preserved in the result but never rendered.
58+
59+
See `tests/fixtures/full-prompt.toml` for the full TOML schema.
60+
61+
---
62+
63+
## Shorthand Helpers
64+
65+
Lighter syntax for building prompts in pure Typst. These are `v0` — not under the immutable 10-symbol contract.
66+
67+
| Helper | Equivalent to |
68+
|--------|---------------|
69+
| `entry(key, value)` | `(key: key, value: value)` |
70+
| `field(name, typ, desc)` | `(name: name, type: typ, description: desc)` |
71+
| `ctx(id, ..entries)` | `p-context(id: id, entries: ...)` |
72+
| `schema(id, ..fields)` | `p-schema(id: id, fields: ...)` |
73+
| `checkpoint(id, after-step, assertion, on-fail)` | `p-checkpoint(id: ..., ...)` |
74+
75+
```typst
76+
// Before (core constructors)
77+
#let my-ctx = p-context(
78+
id: "net-ctx",
79+
entries: (
80+
(key: "firewall", value: "443 + 22 open"),
81+
(key: "gateway", value: "18789 loopback"),
82+
),
83+
)
84+
85+
// After (helpers)
86+
#let my-ctx = ctx("net-ctx",
87+
entry("firewall", "443 + 22 open"),
88+
entry("gateway", "18789 loopback"),
89+
)
90+
```
91+
92+
`ctx` is named `ctx` not `context``context` is a Typst keyword.
93+
94+
---
95+
4696
## Usage
4797

4898
```typst
@@ -150,7 +200,7 @@ Checkpoints are sorted at construction by `(after-step ASC, id ASC)`. Declaratio
150200
151201
## Output Schema: {schema.id}
152202
153-
## Checkpoint: {id} zero or more, sorted (after-step ASC, id ASC)
203+
## Checkpoint: {id} <- zero or more, sorted (after-step ASC, id ASC)
154204
```
155205

156206
---
@@ -164,10 +214,10 @@ Every missing required field, out-of-range value, or type mismatch is a compile-
164214
## Layering
165215

166216
```
167-
promptyst (this package) DSL scope only
168-
opinionated layers separate package
169-
vendor adapters separate package
170-
runtime external, not in scope
217+
promptyst (this package) <- DSL scope only
218+
-> opinionated layers <- separate package
219+
-> vendor adapters <- separate package
220+
-> runtime <- external, not in scope
171221
```
172222

173223
promptyst has no knowledge of runtimes, vendors, transports, agents, or evaluation pipelines.

examples/basic.typ

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// examples/basic.typ
2+
// Build a prompt using the five core constructors, then render to Markdown.
3+
4+
#import "../lib.typ": *
5+
6+
#let my-ctx = p-context(
7+
id: "support-ctx",
8+
entries: (
9+
(key: "domain", value: "Customer support"),
10+
(key: "tone", value: "Professional but friendly"),
11+
),
12+
)
13+
14+
#let my-schema = p-schema(
15+
id: "reply-schema",
16+
fields: (
17+
(name: "response", type: "string", description: "The reply text"),
18+
(name: "tone", type: "enum(formal|casual)", description: "Detected tone"),
19+
(name: "escalate", type: "bool", description: "Whether to escalate"),
20+
),
21+
)
22+
23+
#let my-checkpoint = p-checkpoint(
24+
id: "tone-check",
25+
after-step: 2,
26+
assertion: "Response tone matches the context tone guideline",
27+
on-fail: "continue",
28+
)
29+
30+
#let my-prompt = p-prompt(
31+
id: "reply-to-ticket",
32+
version: "1.0.0",
33+
role: "You are a customer support agent.",
34+
ctx: my-ctx,
35+
constraints: (
36+
"Keep responses under 150 words.",
37+
"Never promise refunds without manager approval.",
38+
),
39+
steps: (
40+
"Read the ticket.",
41+
"Draft a reply.",
42+
"Check tone against guidelines.",
43+
),
44+
inputs: (
45+
(name: "ticket", type: "string", description: "Raw ticket text"),
46+
),
47+
schema: my-schema,
48+
checkpoints: (my-checkpoint,),
49+
)
50+
51+
// Render to Markdown and display
52+
#raw(render-prompt(my-prompt), lang: "markdown")

examples/e2e-pipeline.typ

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// examples/e2e-pipeline.typ
2+
// End-to-end: TOML in -> partial inspection -> metadata extraction ->
3+
// full prompt render -> Markdown out.
4+
//
5+
// This mirrors the determinate-OCD context-engineering pipeline where
6+
// a Nix build step produces TOML and Promptyst compiles it to Markdown.
7+
8+
#import "../lib.typ": *
9+
10+
// ── Step 1: Ingest TOML ──
11+
// In the real pipeline, this comes from `nix eval` -> nuenv -> file.
12+
#let raw-toml = read("pipeline-aspect.toml")
13+
#let result = from-toml(raw-toml)
14+
15+
// ── Step 2: Inspect partial data ──
16+
// Before rendering the full prompt, you can access individual sections.
17+
// Useful for validation, logging, or routing in the build pipeline.
18+
19+
#let aspect-id = result.aspect.id
20+
#let step-count = result.steps.len()
21+
#let checkpoint-count = result.checkpoints.len()
22+
23+
[
24+
= Pipeline Report
25+
26+
*Aspect:* #aspect-id \
27+
*Steps:* #step-count \
28+
*Checkpoints:* #checkpoint-count \
29+
]
30+
31+
// ── Step 3: Extract metadata ──
32+
// Metadata (rationale, severity) flows through the dict but never
33+
// reaches the rendered output. The pipeline can use it for changelogs,
34+
// APM prioritization, or audit trails.
35+
36+
#if result.at("meta", default: none) != none [
37+
== Rationale
38+
#for (key, val) in result.meta.rationale [
39+
- *#key:* #val \
40+
]
41+
]
42+
43+
#if result.at("constraints-meta", default: none) != none [
44+
== Constraint Severity
45+
#for (i, meta) in result.constraints-meta.enumerate() [
46+
#if meta.keys().len() > 0 [
47+
- Constraint #(i + 1): #meta.at("severity", default: "unspecified") \
48+
]
49+
]
50+
]
51+
52+
// ── Step 4: Render the full prompt ──
53+
// This is the final artifact — canonical Markdown suitable for
54+
// CLAUDE.md, AGENTS.md, or any agent context file.
55+
56+
[== Rendered Prompt]
57+
58+
#raw(render-prompt(result.prompt), lang: "markdown")

examples/helpers.typ

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// examples/helpers.typ
2+
// Same prompt as basic.typ, built with shorthand helpers.
3+
4+
#import "../lib.typ": *
5+
6+
#let my-ctx = ctx("support-ctx",
7+
entry("domain", "Customer support"),
8+
entry("tone", "Professional but friendly"),
9+
)
10+
11+
#let my-schema = schema("reply-schema",
12+
field("response", "string", "The reply text"),
13+
field("tone", "enum(formal|casual)", "Detected tone"),
14+
field("escalate", "bool", "Whether to escalate"),
15+
)
16+
17+
#let my-checkpoint = checkpoint("tone-check", 2,
18+
"Response tone matches the context tone guideline",
19+
"continue",
20+
)
21+
22+
#let my-prompt = p-prompt(
23+
id: "reply-to-ticket",
24+
version: "1.0.0",
25+
role: "You are a customer support agent.",
26+
ctx: my-ctx,
27+
constraints: (
28+
"Keep responses under 150 words.",
29+
"Never promise refunds without manager approval.",
30+
),
31+
steps: (
32+
"Read the ticket.",
33+
"Draft a reply.",
34+
"Check tone against guidelines.",
35+
),
36+
inputs: (
37+
(name: "ticket", type: "string", description: "Raw ticket text"),
38+
),
39+
schema: my-schema,
40+
checkpoints: (my-checkpoint,),
41+
)
42+
43+
#raw(render-prompt(my-prompt), lang: "markdown")

examples/pipeline-aspect.toml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# A realistic aspect description from a determinate-OCD context-engineering
2+
# pipeline. This TOML would be generated by `nix eval` extracting the
3+
# .description field from a den aspect, then processed by nuenv.
4+
5+
[aspect]
6+
id = "ocd.gateway"
7+
version = "0.2.0"
8+
role = "Configure Caddy reverse proxy for OpenClaw TLS termination"
9+
10+
[context]
11+
id = "gateway-ctx"
12+
13+
[[context.entries]]
14+
key = "proxy-target"
15+
value = "127.0.0.1:18789 (OpenClaw gateway, loopback-only)"
16+
17+
[[context.entries]]
18+
key = "tls-provider"
19+
value = "Let's Encrypt via Caddy automatic HTTPS"
20+
21+
[[context.entries]]
22+
key = "webhook-port"
23+
value = "8787 (loopback-only, proxied for Telegram webhooks)"
24+
25+
[[constraints]]
26+
text = "TLS certificates must be valid and auto-renewed"
27+
severity = "security"
28+
29+
[[constraints]]
30+
text = "Proxy timeout must not exceed 30 seconds"
31+
severity = "performance"
32+
33+
[[constraints]]
34+
text = "Gateway port 18789 must never be exposed externally"
35+
severity = "security"
36+
37+
[rationale]
38+
design = "External access routes through Caddy reverse proxy to avoid exposing OpenClaw directly"
39+
motivation = "Defense in depth: TLS termination + loopback isolation + capability dropping"
40+
41+
[[steps]]
42+
text = "Enable Caddy service with systemd"
43+
44+
[[steps]]
45+
text = "Configure reverse proxy rules for gateway and webhook endpoints"
46+
47+
[[steps]]
48+
text = "Verify TLS certificate issuance for the configured domain"
49+
50+
[[inputs]]
51+
name = "domain"
52+
type = "string"
53+
description = "Public domain for TLS certificate (e.g. gateway.example.com)"
54+
55+
[[inputs]]
56+
name = "webhook-path"
57+
type = "string"
58+
description = "URL path for Telegram webhook endpoint"
59+
60+
[schema]
61+
id = "gateway-output"
62+
63+
[[schema.fields]]
64+
name = "tls-active"
65+
type = "bool"
66+
description = "Whether TLS is active and certificate is valid"
67+
68+
[[schema.fields]]
69+
name = "proxy-status"
70+
type = "enum(healthy|degraded|down)"
71+
description = "Current reverse proxy health status"
72+
73+
[[checkpoints]]
74+
id = "verify-tls"
75+
after-step = 3
76+
assertion = "TLS certificate is valid and not expiring within 7 days"
77+
on-fail = "halt"
78+
79+
[[checkpoints]]
80+
id = "verify-loopback"
81+
after-step = 2
82+
assertion = "Gateway port 18789 is not in allowedTCPPorts"
83+
on-fail = "halt"

0 commit comments

Comments
 (0)