Polychro — Tutorial¶
You run a maritime shipping company called Shipyard. Your fleet — ships, crews, voyages — is exposed through a REST contract: the Modern Maritime Registry API. Before that contract reaches an SDK generator, an AI agent, or a partner integration, it has to be correct. Not "looks fine in the editor" correct — provably correct, the same way on every machine and in CI.
That is what Polychro does. This track takes one OpenAPI contract — scoped to
just /ships, /ships/{imo_number}, and /crew — and lints it through the
whole pipeline, one layer at a time. You start with a single command and zero
configuration. By the end you have a home-made ruleset, an external schema, a
custom polyglot rule, and agent-native output ready to drop into a generation
loop.
No deploy. No backend. Just polychro lint and a contract that gets stricter at
every step.
⚓ Prerequisites. A
polychrobinary on yourPATH(see Installation). All example files for this track live inexamples/tutorial/track-1-yaml/in the public Polychro repository. Run every command from inside that directory so the relative paths (openapi-ruleset.yml,.polychro.yml,functions/…) resolve.🚧 Beta release & current limitations. Polychro is in
1.0.0-beta2. Every command and every diagnostic on this page was produced by running the real1.0.0-beta2CLI from the example directory — what you see here is exactly what you will get. Two things are worth knowing up front:
- A spurious
non-string-keywarning on every step. The contract quotes its HTTP status codes ("200":) — the idiomatic, valid OpenAPI form — but the well-formedness validator currently flags any key that looks numeric, regardless of quoting. It is a known false positive, not a defect in the contract, and its only effect here is to raise Steps 1 and 7 from a clean exit0to exit1. (polychro#78)- Step 4's external schema is not wired yet. The
config.json-schemablock in.polychro.ymlis not consumed: the CLI reports a configuration error instead of thetypeviolation the narrative expects. (polychro#79)Both will close in a later release; until then the page shows the real output and points out exactly where it differs from the intended behaviour.
Step 1 — Just run it¶
File: step-1-openapi.yml
No config, no schema, no ruleset. Point polychro at the contract and read
what comes back. This is Polychro's "zero to signal" moment: the
well-formedness validator runs implicitly, on every file, before anything else.
# step-1-openapi.yml — the Modern Maritime Registry, three read-only paths
openapi: 3.0.3
info:
title: Modern Maritime Registry API
version: 1.0.0
paths:
/ships:
summary: Collection of registered ships.
get:
operationId: list-ships
summary: List all registered ships.
responses:
"200":
description: A list of ships.
/ships/{imo_number}: { ... } # get-ship
/crew: { ... } # list-crew
Note: the two last lines are abridged for readability. The downloaded file is the source of truth — copy that, not this block.
A clean contract. The only thing Polychro flags is the non-string-key
false positive described in the beta note above.
Run it¶
# Download the file
curl -L -o step-1-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-1-openapi.yml
polychro lint step-1-openapi.yml
Expected output:
Exit code 1 — warnings only, no errors. (The intended result is a clean exit
0; the single warning is the known non-string-key false positive.)
🧭 What you learned:
polychro lint <file>with zero configuration; well-formedness runs implicitly first; what a diagnostic is (severity, code, message); the exit-code contract (0clean,1warnings,2errors).
Step 2 — Well-formedness as the first gate¶
File: step-2-openapi.yml
Semantic checks are pointless on a structurally broken file — so Polychro gates
everything behind well-formedness. To prove it, the /ships path item here
declares summary twice:
/ships:
summary: Collection of registered ships.
summary: Collection of all registered ships. # ← duplicate key
get:
operationId: list-ships
# ...
A duplicated mapping key is a structural defect. Polychro raises it as an ERROR — and because it is a well-formedness error, the pipeline does not proceed to deeper checks.
Run it¶
curl -L -o step-2-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-2-openapi.yml
polychro lint step-2-openapi.yml
Expected output:
ERROR [duplicate-key]: Duplicate key: summary
WARN [non-string-key]: Non-string YAML key: 200
2 issue(s) found.
Exit code 2 — at least one error is present.
🧭 What you learned: well-formedness is the first gate;
duplicate-keyfires as an ERROR on a duplicated mapping key; an ERROR drives the exit code to2.
Step 3 — A home-made OpenAPI ruleset¶
Files: step-3-openapi.yml ·
openapi-ruleset.yml
Polychro's built-in rulesets (polychro:governance, polychro:ai-safety,
polychro:security) are capability-centric — they match an Ikanos
capability document, not OpenAPI's $.paths and operationId. So to govern an
OpenAPI contract you bring your own ruleset. One is provided for you; you don't
author anything yet, you just pass it.
The first rule it carries: every path item must declare a summary.
# openapi-ruleset.yml (excerpt — Step 3's rule)
aliases:
Paths: "$.paths[*]"
rules:
path-summary-present:
message: "Each path item must declare a `summary`."
severity: warn
given: "#Paths"
then:
field: "summary"
function: truthy
In step-3-openapi.yml the /ships/{imo_number} path item has its summary
removed, so the rule bites exactly once, at that JSONPath location.
Run it¶
curl -L -o step-3-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-3-openapi.yml
curl -L -o openapi-ruleset.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/openapi-ruleset.yml
polychro lint --ruleset openapi-ruleset.yml step-3-openapi.yml
Expected output:
WARN at $.paths./ships/{imo_number}.summary [path-summary-present]: Each path item must declare a `summary`.
WARN [non-string-key]: Non-string YAML key: 200
2 issue(s) found.
Exit code 1 — path-summary-present is a warning, and the rule fires once,
with the diagnostic anchored to $.paths./ships/{imo_number}.summary.
🧭 What you learned: the built-in rulesets are capability-centric and do not bite on OpenAPI;
--ruleset <file>runs a custom ruleset;given+thenwith thetruthyfunction; rules fire once, anchored to a JSONPath.
Step 4 — Pin the structure with an external schema¶
Files: step-4-openapi.yml ·
.polychro.yml ·
openapi-schema.json
A ruleset checks conventions; a schema pins shape. This step introduces a
project config file (.polychro.yml) that points the schema-model stage at a
small JSON Schema for the contract. The schema requires info.version to be a
string — and step-4-openapi.yml deliberately writes it unquoted:
# step-4-openapi.yml (excerpt — the planted defect)
info:
title: Modern Maritime Registry API
version: 1.0 # ← parses as a float, not a string
The intended result is a JSON-Schema type ERROR at $.info.version — the
float 1.0 violating the schema's "type": "string".
🚧 Current limitation — not wired yet in
1.0.0-beta2. Theconfig.json-schemablock in.polychro.ymlis not consumed yet: the schema-model stage does not run, and the CLI reports a configuration error instead of thetypeviolation. This is a known, tracked limitation — not a defect in the example files. The narrative above describes the intended behaviour; the actual1.0.0-beta2output is below.
Run it¶
curl -L -o step-4-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-4-openapi.yml
curl -L -o .polychro.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/.polychro.yml
curl -L -o openapi-schema.json \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/openapi-schema.json
polychro lint --config .polychro.yml step-4-openapi.yml
Expected output (1.0.0-beta2, the known limitation):
Error: JsonStructureValidatorFactory requires 'schemaNode', 'schemaPath', or an explicit 'mode' in config
Exit code 2. Intended output (once the config wiring is fixed): a JSON-Schema
type ERROR at $.info.version.
🧭 What you learned: the role of the schema-model stage and a
.polychro.ymlproject config; how a JSON Schema pins the shape of a contract (here,info.versionmust be a string); and an honest look at a feature that is declared but not yet wired end-to-end in1.0.0-beta2.
Step 5 — Extend the ruleset with house rules¶
Files: step-5-openapi.yml ·
openapi-ruleset.yml
Now you write the rules people actually write in Spectral — API-style
governance. The same ruleset gains two: every operation must declare an
operationId, and every operation must declare a responses object.
# openapi-ruleset.yml (excerpt — Step 5's rules)
operation-id-present:
message: "Each operation must declare an `operationId`."
severity: error
given:
- "$.paths[*].get"
- "$.paths[*].post"
# ...put / delete / patch
then:
field: "operationId"
function: truthy
operation-responses-present:
message: "Each operation must declare a `responses` object."
severity: error
given:
- "$.paths[*].get"
# ...
then:
field: "responses"
function: defined
In step-5-openapi.yml the GET /crew operation has its operationId removed,
so operation-id-present fires as an ERROR at that operation.
Run it¶
curl -L -o step-5-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-5-openapi.yml
# (openapi-ruleset.yml already downloaded in Step 3)
polychro lint --ruleset openapi-ruleset.yml step-5-openapi.yml
Expected output:
ERROR at $.paths./crew.get.operationId [operation-id-present]: Each operation must declare an `operationId`.
WARN [non-string-key]: Non-string YAML key: 200
2 issue(s) found.
Exit code 2 — a missing operationId is an error.
🧭 What you learned: authoring your own
given/thenrules; multi-pathgivenselectors; thetruthyanddefinedfunctions; promoting governance from WARN (Step 3) to ERROR; rules anchored to the offending operation.
Step 6 — Leave native: a custom polyglot function¶
Files: step-6-openapi.yml ·
openapi-ruleset.yml ·
functions/operation-id-unique.js
Some rules a schema simply cannot express. "No two operations may share an
operationId" is cross-cutting — it has to walk the whole document. For that,
you leave the declarative given/then world and write a polyglot
JavaScript function that runs on GraalVM.
The ruleset wires it in by name:
# openapi-ruleset.yml (excerpt — Step 6's wiring)
functionsDir: functions
functions:
- operation-id-unique
rules:
operation-id-unique:
message: "Every `operationId` must be unique across the whole contract."
severity: error
given: "$"
then:
function: operation-id-unique
// functions/operation-id-unique.js (excerpt)
export default function operationIdUnique(targetVal) {
const seen = new Map();
const results = [];
// ...walk every paths.<path>.<method>.operationId
// push a result {message, path} on each duplicate
return results;
}
In step-6-openapi.yml the GET /ships operation reuses get-ship (already
used by GET /ships/{imo_number}), so the function reports the duplicate. Note
the ruleset's own message overrides the message returned by the function.
Run it¶
curl -L -o step-6-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-6-openapi.yml
mkdir -p functions
curl -L -o functions/operation-id-unique.js \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/functions/operation-id-unique.js
polychro lint --ruleset openapi-ruleset.yml step-6-openapi.yml
Expected output:
ERROR at $.paths./ships/{imo_number}.get.operationId [operation-id-unique]: Every `operationId` must be unique across the whole contract.
WARN [non-string-key]: Non-string YAML key: 200
2 issue(s) found.
Exit code 2. (When the polyglot function runs, GraalVM/Truffle may print
interpreter warnings to stderr — these are not diagnostics and do not affect
the result.)
🧭 What you learned: when a rule needs more than
given/then; wiring a polyglot function withfunctionsDir+functions; the function contract (return{message, path}per violation); the rulesetmessageoverriding the function's own.
Step 7 — Close the loop: agent-native output¶
File: step-7-openapi.yml
The final layer is integration. AI agents don't want pretty text — they want
compact, structured JSON they can act on and a token cost they can budget
against. The --format agent formatter gives them exactly that.
curl -L -o step-7-openapi.yml \
https://raw.githubusercontent.com/naftiko/polychro/main/examples/tutorial/track-1-yaml/step-7-openapi.yml
polychro lint --format agent step-7-openapi.yml
Expected output:
{
"diagnostics": [
{
"severity": "warning",
"rule": "non-string-key",
"message": "Non-string YAML key: 200"
}
],
"summary": { "errors": 0, "warnings": 1, "info": 0 },
"tokens": 55
}
Exit code 1. The contract itself is clean; the single warning is the known
non-string-key false positive (the intended result here is exit 0). The
tokens field pre-computes the context-window cost so an agent can decide
whether to inline every diagnostic or summarize.
This is also the bridge to the rest of the platform. You can run the same engine as an MCP server —
— so an agent calls a lint tool, gets these structured diagnostics back, and
self-corrects before its output is ever deployed.
🧭 What you learned:
--format agentfor token-efficient, agent-native JSON; thesummaryandtokensfields;polychro serveto expose linting as an MCP tool inside an agent loop.
Recap¶
You started with one command and ended with a fully governed maritime contract. Along the way Polychro:
- Gated everything behind well-formedness (Steps 1–2)
- Governed the contract with a home-made OpenAPI ruleset (Steps 3, 5)
- Pinned its shape with an external schema (Step 4 — intended)
- Enforced a cross-cutting invariant with a polyglot function (Step 6)
- Emitted agent-native output ready for a generation loop (Step 7)
All from polychro lint. All deterministic. All reproducible in CI.
Two 1.0.0-beta2 limitations were surfaced honestly: the non-string-key false
positive on quoted status codes, and the unconsumed schema config in Step 4.
Both are tracked and will close in a later release — this page will be updated to
match the day they do.
Where to next¶
- The built-in, capability-centric rulesets (
polychro:governance,polychro:ai-safety,polychro:security) govern the Ikanos capabilities that consume contracts like this one — see Guide: Rulesets. - CLI reference — every command, option, and exit code.
- Guide: MCP server — wire
polychro serveinto Claude Desktop, Cursor, and other MCP clients.