Compiling To Data
JSON, YAML and TOML output — plus determinism, headers, manifests and how stale artifacts get pruned.
Psy is a general declarative language, so the most ordinary thing to do with a resolved program is emit it as data.
import { defineConfig } from "@psy/config";
import { json, yaml, toml } from "@psy/adapter-data";
export default defineConfig({
source: ["./config"],
targets: [
json({ output: "generated/json" }),
yaml({ output: "generated/yaml" }),
toml({ output: "generated/toml" }),
],
});psy build
psy build --checkThe input
Take the deployment example, which uses multiple inheritance and composition:
export const Region = eu-west-1
export spec BaseService:
region: Region
replicas: 2
resources:
cpu: 500m
memory: 512Mi
tags:
- managed
- psy
export spec Observability:
tags:
- metrics
- traces
resources:
memory: 768Mi
export spec Api extends BaseService, Observability:
name: api
replicas: 4
resources:
+ super
tags:
+ super
- publicTargets see resolved IR. Inheritance, super, composition and references are
already applied. There is no templating step, and there is nothing left to
evaluate — which is the entire reason a config language with no runtime is worth
having.
JSON
{
"name": "api",
"replicas": 4,
"resources": {
"cpu": "500m",
"memory": "512Mi"
},
"tags": [
"managed",
"psy",
"metrics",
"traces",
"public"
],
"region": "eu-west-1"
}BaseService outranks Observability, so its memory won the conflict, and the
tag list is deduplicated in precedence order.
YAML
# Generated by Psy from examples/deployment/main.psy. Do not edit by hand.
name: api
replicas: 4
resources:
cpu: 500m
memory: 512Mi
tags:
- managed
- psy
- metrics
- traces
- public
region: eu-west-1Multiline strings become literal block scalars, which is the main reason to prefer YAML for anything prose-shaped:
prompt: |-
Read the relevant Psy specification before modifying code.
Preserve compiler stage boundaries.
Add or update tests for every semantic change.Scalars are quoted only when they would otherwise change meaning — "yes", "3",
"has: colon" — and keys that YAML 1.1 reads as booleans get quoted too. (YAML
1.1 thinks no is false. YAML 1.1 has opinions about Norway.)
TOML
# Generated by Psy from examples/deployment/main.psy. Do not edit by hand.
name = "api"
replicas = 4
tags = ["managed", "psy", "metrics", "traces", "public"]
region = "eu-west-1"
[resources]
cpu = "500m"
memory = "512Mi"Scalars come before tables, arrays of objects become [[array-of-tables]]
sections, and multiline strings use """ blocks.
TOML has no null. A property that resolves to null cannot be represented, so the
emitter reports it rather than inventing a value:
psy warning PSY6001: Api: missing — TOML has no null; the key was omitted.Options
All three take the same ones:
| Option | Meaning |
|---|---|
output | output directory, relative to the project root |
keyword | only specs declared with this framework keyword |
includeAbstract | include abstract specs (default false) |
includeUnexported | include specs that are not exported (default false) |
modules | only specs whose module path starts with one of these prefixes |
mode | "per-spec" (default) or "single" |
file | file name used by mode: "single" |
specRefs | "name" (default) or "inline" |
metadata | add a $psy block naming the spec, module, keyword and bases |
fileName | "slug" (default) or "name" |
select | an extra predicate, applied after the built-in filters |
By default only exported, concrete specs are emitted.
Selecting a subset
yaml({
output: "generated/agents",
keyword: "agent", // only `agent Foo:` declarations
modules: ["build/"], // only from this part of the tree
})One document instead of many
json({
output: "generated",
mode: "single",
file: "psy-spec.json",
modules: ["spec/"],
metadata: true,
}){
"ParentPrecedence": {
"$psy": {
"name": "ParentPrecedence",
"module": "spec/syntax/inheritance.psy",
"keyword": "semantic-rule",
"bases": ["SemanticRule"]
},
"description": "Defines precedence for multiple inheritance.",
"semantics": "In extends A, B, C, A has higher precedence than B and B has\nhigher precedence than C."
}
}That is exactly how Psy publishes its own language specification as machine-readable data. See Psy Describes Psy.
Spec references
A spec reference renders as the spec's name by default, which keeps
use: [ReadPsySpec] readable:
use:
- ReadPsySpec
- RunTestsWith specRefs: "inline" the referenced spec is embedded instead. Cycles are
broken by falling back to the name, so a self-referential spec cannot loop
forever.
Determinism
Every target is deterministic: the same source always produces byte-identical output. Module order, spec order, property order and object key order are all stable functions of the source, file paths are relative to the project root, and nothing reads the clock.
psy build --checkfails when anything on disk differs from what would be generated. That is what makes generated files safe to commit, diff and review.
Headers and manifests
By default a generated file contains only your content. Nothing announces itself, because the files belong to your project, not to Psy.
export default defineConfig({
source: ["./src"],
artifacts: {
header: true,
manifest: true,
index: true,
},
});| Field | Default | Effect |
|---|---|---|
header | false | a comment naming Psy and the source module, in formats that have comments |
manifest | false | .psy-manifest, listing every path a target wrote |
index | false | aggregate index documents |
With header: true, each artifact carries a line naming the module it came from:
<!-- Generated by Psy from src/skills/ReviewDiff.psy. Do not edit by hand; edit the .psy source and run `psy build`. -->JSON never carries a header, because JSON has no comment syntax. Which is exactly
why manifest exists.
Pruning
psy build removes artifacts it previously wrote but no longer generates. It
identifies them two ways: a path listed in last time's .psy-manifest, or a file
still carrying the generation header.
With neither enabled it can prove nothing, so it does not prune, and it tells you so:
note: stale artifacts were not removed; a build can only remove what it can
prove it wrote. Set `artifacts.manifest` or `artifacts.header` in psy.config.ts
to enable pruning.Rename a declaration in that state and the old artifact is left behind forever.
Turn on manifest — it is a single dotfile — and renaming just works.
Targets may nest their output directories. Each one prunes only its own. And a scoped build never prunes at all, because it cannot tell a stale artifact from one it simply did not produce this time.
Next: Day To Day