Skip to content

Commit a8cdec0

Browse files
lessuselesslessuseless
authored andcommitted
Add from-toml ingestion and shorthand helpers (Phase 1 & 2)
1 parent 8b4687c commit a8cdec0

12 files changed

Lines changed: 821 additions & 5 deletions

.github/workflows/ci.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ jobs:
1919
with:
2020
typst-version: 'latest' # Uses the latest official Typst release
2121

22-
- name: Compile Test File
23-
# We compile the test file. If it panics due to failed assertions or typing issues, the step fails.
22+
- name: Compile Core Tests
2423
run: typst compile --root . tests/test.typ tests/test.pdf
2524

26-
# (Optional) You can upload the compiled test PDF to inspect if anything visual is rendered
27-
- name: Upload Test PDF
25+
- name: Compile Ingest Tests
26+
run: typst compile --root . tests/test-ingest.typ tests/test-ingest.pdf
27+
28+
- name: Compile Helper Tests
29+
run: typst compile --root . tests/test-helpers.typ tests/test-helpers.pdf
30+
31+
- name: Upload Test PDFs
2832
uses: actions/upload-artifact@v4
2933
with:
3034
name: check-output
31-
path: tests/test.pdf
35+
path: tests/*.pdf
3236
retention-days: 1

lib.typ

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@
2222
// render-checkpoint checkpoint dict → Markdown string
2323
// render-chat-mode chat-mode dict → Markdown string
2424
// render-prompt prompt dict → Markdown string (full canonical block)
25+
//
26+
// TOML ingestion (Phase 1):
27+
// from-toml TOML string → dict (partial or full prompt)
28+
//
29+
// Shorthand helpers (Phase 2, v0 — not under immutable 10-symbol contract):
30+
// ctx shorthand for p-context (positional entries)
31+
// schema shorthand for p-schema (positional fields)
32+
// field builds a schema field dict
33+
// entry builds a context entry dict
34+
// checkpoint shorthand for p-checkpoint (positional args)
2535

2636
#import "src/primitives.typ": p-context, p-schema, p-checkpoint, p-chat-mode, p-prompt
2737
#import "src/render.typ": render-context, render-schema, render-checkpoint, render-chat-mode, render-prompt
38+
#import "src/ingest.typ": from-toml
39+
#import "src/helpers.typ": ctx, schema, field, entry, checkpoint

src/helpers.typ

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// src/helpers.typ
2+
// Shorthand helpers for building prompts in pure Typst (Phase 2).
3+
//
4+
// Ships in-tree alongside core modules. Imports from sibling primitives.typ
5+
// to avoid cyclic imports (lib.typ re-exports these helpers).
6+
// If extracted to a separate package, would import from lib.typ per Boundary.md.
7+
//
8+
// Public symbols: ctx, schema, field, entry, checkpoint
9+
//
10+
// Named `ctx` not `context` — `context` is a Typst keyword and
11+
// shadowing it risks breakage.
12+
13+
#import "primitives.typ": p-context, p-schema, p-checkpoint
14+
15+
16+
// ─────────────────────────────────────────────
17+
// entry(key, value)
18+
// Convenience for building context entry dicts.
19+
// ─────────────────────────────────────────────
20+
21+
#let entry(key, value) = (key: key, value: value)
22+
23+
24+
// ─────────────────────────────────────────────
25+
// field(name, typ, desc)
26+
// Convenience for building schema field dicts.
27+
// Named `typ` not `type` — avoids shadowing Typst's type() builtin.
28+
// ─────────────────────────────────────────────
29+
30+
#let field(name, typ, desc) = (name: name, type: typ, description: desc)
31+
32+
33+
// ─────────────────────────────────────────────
34+
// ctx(id, ..pairs)
35+
// Shorthand for p-context. Accepts positional entry dicts.
36+
//
37+
// ctx("my-ctx", entry("k", "v"), entry("k2", "v2"))
38+
//
39+
// Or with inline tuples:
40+
// ctx("my-ctx", ("k", "v"), ("k2", "v2"))
41+
// ─────────────────────────────────────────────
42+
43+
#let ctx(id, ..entries) = {
44+
let parsed = entries.pos().map(e => {
45+
if type(e) == array {
46+
// Tuple shorthand: ("key", "value")
47+
(key: e.at(0), value: e.at(1))
48+
} else {
49+
// Already a dict from entry()
50+
e
51+
}
52+
})
53+
p-context(id: id, entries: parsed)
54+
}
55+
56+
57+
// ─────────────────────────────────────────────
58+
// schema(id, ..fields)
59+
// Shorthand for p-schema. Accepts positional field dicts.
60+
//
61+
// schema("my-schema", field("name", "string", "desc"))
62+
// ─────────────────────────────────────────────
63+
64+
#let schema(id, ..fields) = p-schema(
65+
id: id,
66+
fields: fields.pos(),
67+
)
68+
69+
70+
// ─────────────────────────────────────────────
71+
// checkpoint(id, after-step, assertion, on-fail)
72+
// Shorthand for p-checkpoint. Positional args for conciseness.
73+
//
74+
// checkpoint("verify", 2, "Data is valid", "halt")
75+
// ─────────────────────────────────────────────
76+
77+
#let checkpoint(id, after-step, assertion, on-fail) = p-checkpoint(
78+
id: id,
79+
after-step: after-step,
80+
assertion: assertion,
81+
on-fail: on-fail,
82+
)

src/ingest.typ

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// src/ingest.typ
2+
// TOML ingestion layer. Parses TOML strings into core constructor dicts.
3+
//
4+
// Public symbol: from-toml(raw)
5+
//
6+
// This is a core module — it imports sibling src/primitives.typ directly
7+
// (same layer as render.typ and validate.typ). It calls constructors to
8+
// ensure all validation fires on the parsed data.
9+
//
10+
// Partial TOML: missing sections produce missing keys in the result dict.
11+
// Full assembly: when all required sections are present, builds a prompt
12+
// via p-prompt(). Metadata ([rationale], constraint severity) is preserved
13+
// in the result but never rendered.
14+
15+
#import "primitives.typ": p-context, p-schema, p-checkpoint, p-prompt
16+
17+
18+
// ─────────────────────────────────────────────
19+
// from-toml
20+
//
21+
// raw: string — TOML-encoded prompt data
22+
//
23+
// Returns a dictionary with optional keys:
24+
// context, schema, constraints, steps, inputs, checkpoints,
25+
// aspect, prompt, meta, constraints-meta
26+
//
27+
// Missing TOML sections → absent keys (no panic).
28+
// Present sections with invalid data → panic via constructor validation.
29+
// ─────────────────────────────────────────────
30+
31+
#let from-toml(raw) = {
32+
let data = toml(bytes(raw))
33+
let result = (:)
34+
35+
// ── aspect metadata ──
36+
let aspect-data = data.at("aspect", default: none)
37+
if aspect-data != none {
38+
result.insert("aspect", aspect-data)
39+
}
40+
41+
// ── context ──
42+
let ctx-data = data.at("context", default: none)
43+
if ctx-data != none {
44+
let ctx = p-context(
45+
id: ctx-data.at("id"),
46+
entries: ctx-data.at("entries"),
47+
)
48+
result.insert("context", ctx)
49+
}
50+
51+
// ── schema ──
52+
let schema-data = data.at("schema", default: none)
53+
if schema-data != none {
54+
let sch = p-schema(
55+
id: schema-data.at("id"),
56+
fields: schema-data.at("fields"),
57+
)
58+
result.insert("schema", sch)
59+
}
60+
61+
// ── constraints (extract text, preserve severity as metadata) ──
62+
let constraints-data = data.at("constraints", default: none)
63+
if constraints-data != none {
64+
let constraint-texts = constraints-data.map(c => c.at("text"))
65+
result.insert("constraints", constraint-texts)
66+
67+
// Preserve per-constraint metadata (severity, etc.)
68+
let has-meta = constraints-data.any(c => c.keys().len() > 1)
69+
if has-meta {
70+
let meta-list = constraints-data.map(c => {
71+
let m = (:)
72+
for key in c.keys() {
73+
if key != "text" {
74+
m.insert(key, c.at(key))
75+
}
76+
}
77+
m
78+
})
79+
result.insert("constraints-meta", meta-list)
80+
}
81+
}
82+
83+
// ── steps (extract text) ──
84+
let steps-data = data.at("steps", default: none)
85+
if steps-data != none {
86+
let step-texts = steps-data.map(s => s.at("text"))
87+
result.insert("steps", step-texts)
88+
}
89+
90+
// ── inputs ──
91+
let inputs-data = data.at("inputs", default: none)
92+
if inputs-data != none {
93+
result.insert("inputs", inputs-data)
94+
}
95+
96+
// ── checkpoints ──
97+
let checkpoints-data = data.at("checkpoints", default: none)
98+
if checkpoints-data != none {
99+
let cps = checkpoints-data.map(c => p-checkpoint(
100+
id: c.at("id"),
101+
after-step: c.at("after-step"),
102+
assertion: c.at("assertion"),
103+
on-fail: c.at("on-fail"),
104+
))
105+
result.insert("checkpoints", cps)
106+
}
107+
108+
// ── rationale (metadata, not rendered) ──
109+
let rationale-data = data.at("rationale", default: none)
110+
if rationale-data != none {
111+
let meta = result.at("meta", default: (:))
112+
meta.insert("rationale", rationale-data)
113+
result.insert("meta", meta)
114+
}
115+
116+
// ── full prompt assembly ──
117+
// Only when all required sections are present.
118+
let has-aspect = result.at("aspect", default: none) != none
119+
let has-context = result.at("context", default: none) != none
120+
let has-constraints = result.at("constraints", default: none) != none
121+
let has-steps = result.at("steps", default: none) != none
122+
let has-inputs = result.at("inputs", default: none) != none
123+
let has-schema = result.at("schema", default: none) != none
124+
125+
if has-aspect and has-context and has-constraints and has-steps and has-inputs and has-schema {
126+
let a = result.at("aspect")
127+
let prompt = p-prompt(
128+
id: a.at("id"),
129+
version: a.at("version"),
130+
role: a.at("role"),
131+
ctx: result.at("context"),
132+
constraints: result.at("constraints"),
133+
steps: result.at("steps"),
134+
inputs: result.at("inputs"),
135+
schema: result.at("schema"),
136+
checkpoints: result.at("checkpoints", default: ()),
137+
)
138+
result.insert("prompt", prompt)
139+
}
140+
141+
result
142+
}

tests/fixtures/checkpoints.toml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Checkpoints declared out of order to verify sorting.
2+
# Expected sort: (after-step ASC, id ASC)
3+
# So: alpha(1), beta(1), zulu(1), gamma(2), delta(3)
4+
5+
[aspect]
6+
id = "sort-test"
7+
version = "0.1.0"
8+
role = "Test checkpoint sorting"
9+
10+
[context]
11+
id = "sort-ctx"
12+
13+
[[context.entries]]
14+
key = "purpose"
15+
value = "Verify checkpoint sort order"
16+
17+
[[constraints]]
18+
text = "Checkpoints must be sorted"
19+
20+
[[steps]]
21+
text = "Step one"
22+
23+
[[steps]]
24+
text = "Step two"
25+
26+
[[steps]]
27+
text = "Step three"
28+
29+
[[inputs]]
30+
name = "x"
31+
type = "string"
32+
description = "Dummy input"
33+
34+
[schema]
35+
id = "sort-schema"
36+
37+
[[schema.fields]]
38+
name = "ok"
39+
type = "bool"
40+
description = "Pass/fail"
41+
42+
[[checkpoints]]
43+
id = "zulu"
44+
after-step = 1
45+
assertion = "Zulu check"
46+
on-fail = "continue"
47+
48+
[[checkpoints]]
49+
id = "alpha"
50+
after-step = 1
51+
assertion = "Alpha check"
52+
on-fail = "halt"
53+
54+
[[checkpoints]]
55+
id = "delta"
56+
after-step = 3
57+
assertion = "Delta check"
58+
on-fail = "halt"
59+
60+
[[checkpoints]]
61+
id = "gamma"
62+
after-step = 2
63+
assertion = "Gamma check"
64+
on-fail = "continue"
65+
66+
[[checkpoints]]
67+
id = "beta"
68+
after-step = 1
69+
assertion = "Beta check"
70+
on-fail = "halt"

tests/fixtures/context-only.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[context]
2+
id = "standalone-ctx"
3+
4+
[[context.entries]]
5+
key = "env"
6+
value = "production"
7+
8+
[[context.entries]]
9+
key = "region"
10+
value = "us-east-1"

tests/fixtures/full-prompt.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[aspect]
2+
id = "ocd.networking"
3+
version = "0.1.0"
4+
role = "OpenClaw-aware networking aspect"
5+
6+
[context]
7+
id = "networking-ctx"
8+
9+
[[context.entries]]
10+
key = "firewall"
11+
value = "Ports 443 (HTTPS) and 22 (SSH) open externally"
12+
13+
[[context.entries]]
14+
key = "gateway-port"
15+
value = "18789 loopback-only (proxied by Caddy)"
16+
17+
[[constraints]]
18+
text = "Gateway and webhook ports stay loopback-only"
19+
20+
[[constraints]]
21+
text = "All external traffic must route through Caddy"
22+
23+
[[steps]]
24+
text = "Configure NetworkManager"
25+
26+
[[steps]]
27+
text = "Open firewall ports 443, 22"
28+
29+
[[inputs]]
30+
name = "hostname"
31+
type = "string"
32+
description = "Target machine hostname"
33+
34+
[schema]
35+
id = "networking-output"
36+
37+
[[schema.fields]]
38+
name = "applied"
39+
type = "bool"
40+
description = "Whether the config was applied"
41+
42+
[[checkpoints]]
43+
id = "verify-loopback"
44+
after-step = 2
45+
assertion = "Gateway port 18789 is not in allowedTCPPorts"
46+
on-fail = "halt"

0 commit comments

Comments
 (0)