English | 한국어
An LLM-native programming platform: a language, a semantic IR, a native compiler, a runtime, a shared knowledge base, and an agent protocol — designed as one system.
Existing languages were designed to be easy for humans to write. From here on, most code is generated by LLMs. So a language should be designed to be easy for an LLM to understand, reason about, and optimize — not for a human typist.
linkly is not one new language. It is the whole platform that premise implies:
Developer → Intent (what) → LLM → Semantic IR → Native Optimizer → Machine Code
The developer does not write implementations. They declare goals and business rules (what); the compiler and a pipeline of AI agents design, implement, verify, optimize, and ship the rest (how).
This is a working implementation, not a proposal. .lnpl runs on an IR interpreter
and compiles through MLIR to a native binary, and a differential check holds the two
to the same observable behaviour. Full status below.
The surface language — LNPL (working name, .lnpl) — carries declarations only:
entity User
field
id UUID
email Email
password Password
createdAt DateTime
entity Session
field
id UUID
issuedAt DateTime
service LoginService
policy
retry 3
timeout 3s
security
jwt
performance
response < 50ms
cache 5m
workflow Login
validate user
authenticate user
cache user
create session
Every step there derives a real effect — Validation, RepositoryCall,
CacheAccess, RepositoryCall — and the workflow runs to completed. Two of the
declarations do not change execution, and the compiler says so rather than
letting you assume otherwise: security jwt issues and verifies nothing on the
default path (lnpl serve --jwt-secret-env NAME is what verifies a bearer token
per request), and performance response is measured and reported but never
blocks an over-budget run. lnpl compile prints both as diagnostics.
The verb vocabulary is closed. A word outside it is not an error — it compiles
into a step that derives no effect and does nothing, with an unknown-verb
diagnostic beside the document. The lexicon lives in
plugins/lnpl/skills/lnpl-authoring/references/verbs.md,
generated from the compiler's own table.
No if / for / while / switch — those are reserved and unusable. Control
vocabulary is when / repeat / parallel / until / pipeline. Blocks are
delimited by keywords, and indentation is not semantically significant
(4 spaces is a style convention only), so neither brace-pairing nor indentation
errors are grammatically expressible.
The AST is discarded. Meaning is first-class: BusinessRule, Validation,
NetworkCall, RepositoryCall, CacheAccess, Transaction, Authorization,
EventEmit, Policy, Security, Performance, and more. The grammar is merely a
surface notation that lowers into the IR; the compiler, the runtime, and the nine
AI agents are all consumers of it.
The IR is not a nested tree but a flat node table with id references. That structurally satisfies the nesting limits of constrained decoding, makes per-node diffs and fragment exchange cheap, and keeps serialization order stable so KV-cache prefixes stay reusable.
The commands below run examples/login.lnpl, which is the regression fixture for
issue #36 — it deliberately holds three verbs outside the lexicon (generate /
audit / return), so its output shows what a no-op step looks like and what the
unknown-verb diagnostic says about it. It is a reproduction case, not a model to
copy; examples/checkout.lnpl is the clean one.
# The project pins its Python in a venv, so verification behaves the same no matter
# what `python3` resolves to on your PATH.
python3 -m venv .venv && .venv/bin/pip install jsonschema
export PYTHONPATH=impl
# intent -> Semantic IR
.venv/bin/python -m lnpl compile examples/login.lnpl | head -20
# mode A — run it on the IR interpreter
.venv/bin/python -m lnpl run examples/login.lnpl
# `spec` blocks become a test manifest, and the runner executes it
.venv/bin/python -m lnpl spec examples/login.lnpl --run
# an OpenAPI 3.1 document, generated from the IR
.venv/bin/python -m lnpl openapi examples/login.lnpl | head -30
# the knowledge base, and the agent cycle that consults it
.venv/bin/python -m lnpl kb --lint
.venv/bin/python -m lnpl agents examples/login.lnplThe agent cycle is the part worth watching:
agent cycle over Login (6 step(s))
validate input kb=(none) (nothing proposed)
authenticate kb=patterns-repository-call (nothing proposed)
cache user kb=cloud-redis-cache-provisioning (nothing proposed)
generate token kb=security-jwt-issuance proposal=prop-0001 -> completed applied=['wf.login.step.4', 'wf.login.step.4.authz']
audit login kb=(none) (nothing proposed)
return token kb=security-jwt-issuance (nothing proposed)
IR nodes: 19 -> 20 | proposals applied: ['prop-0001']
Four of the six steps propose nothing, and that is the design working. The Coder
does not decide what to emit from its own knowledge — it routes the step to the KB, and
where the KB prescribes nothing it stops. The one step that does gain something goes
through ir.propose (which never mutates) and a Reviewer's agent.report approval
before the node reaches the document, carrying provenance:
meta.source = "kb:security-jwt-issuance@0.1.0".
The closed vocabulary is the reason this exists: a plausible-looking word parses fine and then does nothing at runtime, so Claude has to be routed to the real lexicon rather than left to guess. Installing the plugin does that, and adds a compile diagnostic that fires on save:
/plugin marketplace add choiyounggi/linkly
/plugin install lnpl@linkly
The repo doubles as the marketplace and hosts three plugins with different audiences:
| Plugin | Audience | Contents |
|---|---|---|
lnpl |
people writing .lnpl |
vocabulary routing, compile diagnostics on save, spec derivation, a completion gate, KB lookup |
lnpl-dev |
people building linkly itself | environment-prerequisite diagnosis, RFC process lint, the mutation harness's traps |
lnpl-mcp |
anyone who would rather call the compiler than shell out to it | lnpl compile and kb.route as MCP tools, returning diagnostics as records instead of stderr prose |
Contributors install /plugin install lnpl-dev@linkly as well.
The diagnostics hook does not need lnpl on PATH. It resolves the compiler
in this order and stops at the first hit: $LNPL_BIN, the nearest
.venv/bin/lnpl found walking up from the edited file, $CLAUDE_PROJECT_DIR/.venv/bin/lnpl,
PATH, then python3 -m lnpl. A project-local .venv/bin/pip install . is
enough — a hook runs in the agent's process environment, which does not carry the
PATH your shell rc sets. The session entry point is AGENTS.md;
CLAUDE.md is one line that loads it.
For developers:
lnpl-doctorand its tests still look for thelnplconsole script in.venv. Run.venv/bin/pip install .once after creating the venv, andpip install --force-reinstall --no-deps .after editingimpl/lnpl/.
Mode B needs the MLIR/LLVM tools (brew install llvm; ~1.8 GB, keg-only). That is
the whole prerequisite — the custom lnpl dialect is defined declaratively in
mlir/lnpl.irdl.mlir and loaded into stock mlir-opt with --irdl-file, so no
C++ compiler, cmake, or TableGen build is involved.
# IR -> MLIR -> LLVM IR -> native binary, then run it
.venv/bin/python -m lnpl build examples/login.lnpl --run
# and the check that matters: do the two modes agree?
.venv/bin/python -m lnpl diff examples/login.lnplPASS 1/4 execution order — 6 step(s): validate input -> authenticate -> cache user -> generate token -> audit login -> return token
PASS 2/4 policy outcome — status=completed
PASS 3/4 observability signals — 3 effect(s) per step match
PASS 4/4 masking — no secret marker in either mode's output
differential: EQUIVALENT
Those four classes are exactly what RFC-0004 requires the modes to agree on — and it is equally explicit about what they may differ in: scheduler shape, memory placement, instruction selection, op count, wall-clock time. The check compares the former and ignores the latter, because a check that compared timings would fail for reasons the contract permits.
workflow Login -> completed (33ms, correlation_id=cid-0001)
step validate input 6ms attempts=1 [Validation -]
step authenticate 6ms attempts=1 [RepositoryCall found=True]
step cache user 6ms attempts=1 [CacheAccess ttl_ms=300000]
step generate token 5ms attempts=1
step audit login 5ms attempts=1
step return token 5ms attempts=1
response SLO 50ms: met (measured, not enforced)
Nothing in that source says how to validate, read, or cache. The retry 3,
timeout 3s, and cache 5m declarations become real runtime behaviour — start with
an empty repository (--no-row) and you see attempts=4 (one try plus retry 3),
capped exponential backoff, and the response SLO reported as exceeded but not
enforced, exactly as RFC-0003 specifies.
All three roadmap phases are complete.
- Mode A —
.lnplparses, lowers to Semantic IR, and runs on the IR interpreter. - Mode B — the same source compiles through MLIR to a native binary. The custom
lnpldialect is declared in IRDL and loaded into stockmlir-opt, so no C++ TableGen build is involved (RFC-0004 S4). - Differential check — the two modes are held to the four observable classes RFC-0004 names: execution order, policy outcome, observability signals, masking.
- Guard conditions (
when/until) evaluate at runtime in both modes; RFC-0008 G8 extracts the condition field from the payload via argv parameter passing. - OpenAPI is generated from the IR, and so is the golden scenario — it is compiled, not hand-maintained. All nine agent roles are implemented.
~3,670 tests, all passing, plus a 77-mutation harness that proves the suite can actually fail. Both are reproduced by the commands under Verification.
Real backend drivers live outside this repo, by design. The core owns only the
driver contracts and their TCK; actual bindings are external packages registered
through the lnpl.drivers entry-points group and selected with
--backend <scheme>:<arg> (docs/backends.md §8). The first such
package is lnpl-postgres — a
PostgreSQL RepositoryDriver (psycopg 3) that runs this repo's TCK against a real
postgres server in its own Testcontainers CI, per the "no binding without integration
tests" rule (issue #121). The
cache-side counterpart is lnpl-redis — a
Redis CacheDriver (redis-py) registered under lnpl.caches and selected with
--cache redis:<url>, running this repo's CacheDriverTCK against a real redis
server in its own Testcontainers CI (issue #143)
— the first real user of RFC-0043's cache_scope self-report ("shared"). The
trace-side counterpart is lnpl-otel — a
TraceExporter shipping completed workflow traces over OTLP, registered under
lnpl.exporters and selected with --trace-exporter otlp, verified against a real
otel-collector in its own Testcontainers CI
(issue #144).
49 RFCs — 46 Accepted, RFC-0000 Superseded by RFC-0007, RFC-0034 Draft, RFC-0048 Draft. RFC-0007 was formally
accepted 2026-08-03, having been the binding process since RFC-0000 was superseded on
2026-07-31 (issue #11). See the
roadmap.
RFC bodies are written in Korean; identifiers, keywords, and schema fields are English. The central one has an English summary: RFC-0001 Semantic IR — the document the rest of the suite is defined against.
| RFC | Contents |
|---|---|
| 0000 RFC Process | Superseded by 0007 — lifecycle, numbering, the fixed 7-section template |
| 0007 RFC Process v2 | Adds the Updates relation: revise a named section without replacing the RFC |
| 0001 Semantic IR — English summary | 21 node kinds, 18 semantic types, flat structure, canonical JSON serialization |
| 0002 Syntax | Line-oriented, keyword-delimited EBNF (58 productions) + grammar→IR lowering map |
| 0003 Runtime | Actors, structured concurrency, policy enforcement, memory primitives, observability contract |
| 0004 Compiler | MLIR progressive lowering (7 stages), pass invariants, three optimizer responsibility axes |
| 0005 Knowledge Base | 12 categories, 3-tier progressive-disclosure routing, consumption interface |
| 0006 Agent Protocol | 9 roles, 8 JSON-RPC methods, structured errors, idempotency, task lifecycle |
| 0008 Guard Conditions | Guard conditions: presence check & comparison (2 forms), spec correction, mode B compilation. Updates 0002 §Full grammar, 0003 §Guard |
| 0009 Guard Condition OQ | Retires RFC-0002's open question ② now that the grammar is settled. Updates 0002 §Open Questions |
| 0010 Proposal Intent | How a role attaches a node it may not author, and what happens to a reference that moves. Updates 0006 §Agent Roles & IR Access, §Methods/ir.propose |
| 0011 Refinement enum & name collisions | Which refinement names are legal, and what happens when two declarations claim one. Updates 0001 §부록 A.6.3, §부록 A.7 |
| 0012 Execution Scope | What a guard condition may name, and how a step's result binds for the next one. Updates 0002 §Full grammar, 0008 §Reference-level Specification/1. Full Grammar, 0003 §Guard |
| 0013 Step Attempt Ceiling | An absolute bound on step attempts that does not read the declared retry budget — so losing that budget is a failure, not an infinite loop. Updates 0003 §Policy Enforcement |
| 0014 Guard Skip Observability | A skipped step no longer passes for a completed one — the skip becomes a recorded, contracted signal instead of an INFO line. Updates 0008 §Guard Runtime Semantics |
| 0015 Value Semantics | What a guard may compare and what set may write: field references, one binary arithmetic term, and-composition, the input. payload namespace. Aggregation (sum/count) needs a row-set expression first, so its roadmap lives in §Alternatives. Updates 0001 §A.4, 0002 §Full grammar, 0008 §1 |
| 0016 Time and Schedule Semantics | DateTime as an epoch-ms codec so a duration is an i64 and both modes compare it identically; event … on schedule daily at HH:MM UTC reaches the IR and OpenAPI, and stays unenforced. Updates 0001 §A.4, 0002 §Full grammar, 0008 §1 |
| 0017 Guarded Example Correction | Why the shipped guarded.lnpl was rewritten rather than exempted from the guard that went red on it. Updates 0008 §5.2 |
| 0018 Repeated Step Observation Fold | How a step that runs more than once folds into one observation, so mode A and mode B can be compared by name. Updates 0017 §Open Questions 1 |
| 0019 Misleading Indentation | Indentation carries no meaning, yet indentation that contradicts the block structure is refused — a silent lie about scope is worse than a missing convention. Updates 0002 §Block structure |
0020 Spec given Input Namespace |
How a spec case names a field of the run payload, so a guard on input. can be contracted at all. |
| 0021 Diagnostic Severity Levels | The severity ladder and what --strict[=LEVEL] gates. warning is what a fixed program stops emitting; info is the platform stating what it does. |
| 0022 Mode B Observation Surface | What a native build must say about skipped steps and --field reach, so "it ran" and "it was skipped" stay distinguishable. Updates 0014 §2.5·§2.6, 0021 §codes |
| 0023 Guard Scope Diagnostic | A guard owns only the next item, so a later step can change the very state the guard protected. guard-orphaned-steps reports that at compile time — judged by consequence, not by shape. Updates 0021 §codes |
| 0024 Enforcement Diagnostic Line | Enforcement diagnostics (declared-not-enforced/declared-measured-only/authorization-not-verified) now carry (line N) alongside the node id, so two declarations sharing one clause's node id are no longer indistinguishable by location. Updates 0023 §5 |
| 0025 Row Sets and Aggregation | list reads an entity's every row into a RowSet — a namespace of its own, apart from single-row bindings — and sum/count aggregate it in a set. Mode B computes no aggregate values; lnpl diff still proves the four observable classes agree. Updates 0012 §G12.2, 0015 §1 |
| 0026 Unknown-Verb Line and Suggestion | unknown-verb/guard-orphaned-steps/guard-skipped-steps carry a structured line, and an unknown verb gets a two-tier did-you-mean: a curated alias table for semantic near-synonyms (persist -> create), difflib for spelling typos. Updates 0024 §Scope |
| 0027 Network Driver and Result Binding | call/request become real outbound calls behind a NetworkDriver (--network fake|http), and as <name> binds the response so a guard can branch on status — a transport failure on a bound call is a value (status 0), not a run failure. Updates 0003 §Execution Model, 0012 §G12.2, 0014 |
| 0028 Arithmetic and Alternative Guards | *// join +/- (integer, truncating; 0 divisor is a RunError, not a compile error unless it is the literal 0), and when A / or B promotes two guard lines into an alternative guard — a structure, not a Condition-grammar operator. Updates 0001 §Node catalogue/Guard, 0014 §2, 0015 §1 §4 |
0029 Clock Contract and --clock real Binding |
Names the shared time source timeout/retry/CacheAccess already read as a Clock contract, and adds a second binding: --clock real ties CacheAccess TTL to actual wall-clock time. The default virtual binding — and diff/spec, which never see --clock — are unchanged. Updates 0003 §Execution Model |
0030 create Result Binding and Payload Seed |
create <noun> as <name> extends RFC-0027's result-binding notation so the created row can be addressed by set/format/respond; separately, and regardless of as, the created row is seeded from same-named non-derived payload fields — the fix for the "skeleton row" gap. as-less create is unchanged at the compile surface (no result field, no new scope entry). Updates 0012 §G12.2, §G12.5 |
| 0031 Multi-File Compilation Unit | A compilation unit becomes a set of files: lnpl <cmd> <src...> merges explicit files in argument order, lnpl <cmd> <dir> collects its *.lnpl filename-sorted. Declared names stay globally unique — a name repeated across files is rejected, naming both <file>:<line> locations. Grammar and the lexer are unchanged, and one source argument stays byte-identical. Updates 0004 §Reference-level Specification (pipeline table, S1 row) |
| 0032 Transaction Boundary and Rollback Enforcement | Workflow execution becomes one implicit transaction (no explicit Transaction IR node yet): commit on success, rollback — including a failed run's registered event emissions — on any failure. policy rollback moves from unenforced to enforced. Updates 0003 §Execution Model, §Policy Enforcement, §Examples |
| 0033 Namespace Directories | A directory holding subdirectories becomes a namespace root: each first-level subdirectory names its declarations' namespace, and a directory literally named internal/ restricts visibility to its parent — zero grammar changes, derived entirely from path. Declared names stay unique within a namespace rather than globally; a namespace-less compilation unit (today's shape) is byte-identical. Measured first (docs/scale-pressure-measurement.md): 11 name-collision events at 50 entities, 5 domains independently drawing from a 4-word shared-noun pool (Order/Item/Status/Event) — domain-specific names never collided. Updates 0031 §Guide-level Explanation, §Reference-level Specification (load_sources) |
| 0034 NetworkCall Compensation | (Draft) Decides how to compensate a NetworkCall step outside the transaction boundary policy rollback protects: a future compensate clause silences the compiler's rollback-escapes-network warning (issue #112) when present, and stays the reported default otherwise. Rejects the outbox alternative — an async call cannot satisfy call ... as <name>'s synchronous result binding (RFC-0027 §2, RFC-0030). Decision only; no grammar changes yet. |
| 0035 Authorization Enforcement — Deferred Scope | Answers the three questions issue #119 left open once security role became real: workflow-level security role stays out for now (no measured need yet, revisit conditions stated); the authorize verb keeps its promoted warning grade with its (a)-vs-(b) final fate deferred to observed usage (criteria table included); security encrypt is decided for removal (its "driver-dependent" framing describes an always-empty set — zero external drivers exist), with migration guidance, actual removal scoped to a follow-up tech-debt issue. |
0036 policy rollback Declaration Effect |
Corrects the documented effect of policy rollback: run_workflow rolls back every failed execution's writes unconditionally, regardless of declaration — the declaration only gates an INFO trace line and the compile-time rollback-escapes-network diagnostic (issue #112). enforced status is kept (the guarantee itself is real); only the rationale text was wrong. No behavior change. Updates 0032 §실행 경계, §docs/ENFORCEMENT-MATRIX.md §B — policy rollback 행 |
| 0037 Outbound HTTP Resilience | capability http gains retry <N> backoff <duration> [jitter] (exponential backoff, full jitter, Retry-After-aware) and breaker after <N> within <duration> (in-process circuit breaker), method widens to get/post/put/patch/delete, and path "<template>" + call ... with <ref>... assembles an escaped URL path. NetworkDriver.call becomes a breaking 3-tuple, (status, body, headers), done once for both implementations; NetworkDriverTCK checks the two never grade a declaration differently. No declaration is byte-identical to before. Updates 0027 §Reference-level Specification/1 |
0038 list where Query Predicate + order by/limit |
list <Entity> gains where <cond> [order by <field> [desc]] [limit <N>], reusing the guard condition grammar (condition.py) verbatim — no new expression language. Equality (==/!=) allows any matching declared type (UUID/Text/Email included); order comparisons (<,<=,>,>=) keep the Integer/DateTime dimension restriction. RepositoryDriver.query gains predicate/order/limit (all default None, byte-identical when absent); a driver opts into pushdown via supports_predicate, or the core over-fetches and filters in Python, logging one predicate-not-pushed-down INFO trace line. Updates 0016 §Reference-level Specification/3, 0025 §Reference-level Specification/1 |
0039 note Verb + Canonical Log Line |
note "<template>" [with <ref>...] lowers to a non-Effect Annotation node (same treatment respond/Response gets), reusing condition._parse_format_rhs verbatim. An unresolved reference records null, never fails the run; a Password-typed value is masked through the existing mask_payload chokepoint. Over 16 notes per workflow is a note-cap-exceeded warning, not a compile error. --log-format json's canonical line gains notes/effects/input_digest (appended only when present); lnpl serve --capture-on-failure (default off) adds the masked input payload only to a failed/500 run's line. _call_with_json_log is now exception-safe end to end — a request that dies before _respond is reached still emits exactly one line. |
| 0040 Event Consumption Contract | event ... consume by <Workflow> runs a workflow on arrival — the missing consume half of the publish side RFC-0032 already settled. POST /-/events/<slug> (reserved space, same shape as RFC-0016's schedule trigger) accepts a CloudEvents v1.0 structured envelope; its id is the idempotency key, reusing issue #113's lnpl_idempotency table/API/TTL as-is (no second store). The execution result maps to exactly 3 buckets — 200 success, 503 + Retry-After transient (deadline, or a RepositoryCall/NetworkCall-effect failure), 422 permanent (Validation rejection, business/guard RunError, create conflict) — and a transient 503 deliberately skips idempotency_finish so a retry gets a fresh run instead of replaying 503 forever. A cycle between consume by and a workflow's own emit is a static warning (event-consume-cycle), never a compile error. lnpl relay is the reference relay (urllib only, no broker dependency) draining the outbox into the consume route. |
0041 parallel Block Execution |
Mode A finally executes what RFC-0003 already promised structured concurrency would mean: a parallel block's steps run on a block-scoped ThreadPoolExecutor, fail-fast (one branch failing cancels the unstarted rest and fails the block), capped by policy parallel <N> (new optional arity on an existing policy name — falls back to the block's own step count when bare). Two steps writing the same entity inside one block is a compile-time LowerError citing both line numbers — RFC-0012's binding is order-dependent and a parallel block has none. Reporting stays declared-order, not completion-order, so spec.py's steps <N> reads the same shape sequential execution always gave it; each step's span uses real wall-clock timestamps (not the virtual Clock) so overlapping siblings are visible proof of actual concurrency. Mode B is untouched (RFC-0004 §5(#7) stays open) — differential now flags a parallel-bearing workflow's report as an unverified dimension for exactly that reason. |
| 0042 Extension Diagnostics — Namespace, Ownership, Gating | Extensions get a place to name their own failure modes: registered codes take the form <prefix>/<code> (ESLint's plugin/rule convention), bare (unslashed) codes stay permanently core-reserved, and a prefix is the extension's own entry-point registration name — one prefix, one owner, duplicate registration refused at load time (the ownership rule dotnet/roslyn#40351 shows Roslyn never set for 3rd parties). Extension codes may declare info/warning only; error is refused at registration, and extension codes do not participate in --strict gating by default — installing an extension must not flip an existing program's exit code. Extension diagnostics see the compiled IR and their own config only, never source text. No code changes; implementation is a follow-up task. Updates RFC-0021 §Reference-level Specification/코드 → 등급 (정본), §Reference-level Specification/--strict[=LEVEL] |
| 0043 Driver Enforcement Reporting | Installed drivers get a place to state what they actually guarantee: a driver factory may carry a class attribute lnpl_enforcement (a dict over four core-owned axes — delivery/isolation/cache_scope/token_claims) read at entry-point load time, no instantiation or connection required. Compilation checks every driver installed under a capability-activated slot (postgres/redis/jwt/http → repository/cache/token/network, reusing issue #134's slot vocabulary) rather than guessing which one --backend will pick at runtime, and synthesizes one info diagnostic per matching IR node under <entry-point-name>/<axis-code> — the driver's own registration name, not the capability keyword, so a capability postgres program can still surface a diagnostic prefixed kafka/ if that is what is installed. --strict does not gate these codes (same RFC-0042 rule). lnpl capabilities --json gains an additive enforcement field, and docs/ENFORCEMENT-MATRIX.md documents the reporting-is-the-source contract. No code changes; implementation is a follow-up task. Resolves RFC-0042's Open Question 2 — an independent RFC (References RFC-0042/RFC-0021), not an Update. |
| 0044 Money Arithmetic — minor-unit codec and rounding policy | Money gets an evaluator without touching the wire: the JSON shape ({amount, currency}) is unchanged, but an evaluation-only channel encodes it as a signed 64-bit minor-unit integer, with the decimal exponent (0/2/3 places) looked up from a closed ISO 4217 bucket table. A new MoneyLiteral token (100.50USD) lets spec seed/assert Money fields — decimal digits must match the currency's exponent exactly, no rounding of authored literals. Aggregate division (avg, RFC-0045) rounds half-to-even, deliberately asymmetric with /'s existing truncating rule (RFC-0028, left untouched). Cross-currency comparison or addition is a runtime RunError, not a compile error — currency is row data, not a declared dimension (RFC-0016 §3's dimension table is unchanged; Money still cannot appear in guard/set arithmetic). No code changes; implementation is a follow-up task. An independent RFC — resolves RFC-0015's Open Question 4 and RFC-0028's Open Question 2. |
| 0045 RowSet Aggregation Extension — avg/min/max | AggFunc grows from sum/count to sum/count/avg/min/max: sum/avg now also accept same-currency Money fields (RFC-0044's evaluator), min/max newly accept Integer/DateTime/Money, and sum(DateTime) stays rejected as meaningless. An empty RowSet still gives sum/count their existing 0, but avg/min/max have no natural identity element there and fail with a RunError instead of guessing a silent value. group by and avg(DateTime) are explicitly left open — no measured need yet, closing either is a small follow-up RFC. Mode B still computes no aggregate values (RFC-0025 §10's reasoning is unchanged, so this RFC does not re-cite it). No code changes; implementation is a follow-up task. Updates RFC-0025 §Reference-level Specification/2. 집계 표현식 문법, §Reference-level Specification/3. 정적 거부 |
| 0046 RFC Example Realignment — RFC-0037/0008/0014 §Examples | Three Accepted RFCs' ## Examples blocks silently stopped being runnable: RFC-0037's representative example indents two steps under a when guard, which owns exactly one step (a compile error once the new doc-snippet gate compiles it); RFC-0008 and RFC-0014's examples use pre-RFC-0002 retired syntax (inputs/step <name>/guard when/effect/kind), which an unknown verb parses as a silent no-op instead of failing. This RFC updates all three §Examples sections with the substituted-final-text each now needs — RFC-0037's wrapped in a pipeline block, RFC-0008's synced to examples/guarded.lnpl, RFC-0014's round-counting example rewritten in current syntax — while leaving the three RFCs' bodies otherwise untouched (Accepted RFCs are never edited directly). No code changes; implementation is a follow-up task. Updates RFC-0037 §Examples, RFC-0008 §Examples, RFC-0014 §Examples |
0047 Aggregate Field Type Carriage — agg_field_type |
An empty RowSet's Money sum returned plain integer 0 instead of RFC-0045 §5's {"amount": "0", "currency": null} — eval_aggregate dispatches on each row's Python shape, and an empty RowSet has no row to inspect. lower.py's _check_aggregate already knows the aggregated field's declared base type statically, so this RFC carries it to run time on a new optional nodeAssignment.agg_field_type key (Integer/DateTime/Money, absent for count and non-aggregate assignments); eval_aggregate gains a matching optional keyword. An IR document compiled before this RFC (no key present) keeps the old plain-0 result until recompiled — a deliberate backward-compatibility floor. Mode B does not compile Assignment nodes at all, so this RFC is mode-A only. Updates RFC-0045 §Reference-level Specification/1. AggFunc 문법, §Reference-level Specification/5. sum의 Money 확장 |
0048 Collections Non-Goal and RowSet group by |
Closes RFC-0001's open question 1 (generic/collection field types) as a permanent non-goal — v1 never adds List/Map production rules to FieldType; relations stay entity references and multi-row reads stay list where (RFC-0038). Also resolves the group by question RFC-0038 and RFC-0045 each carried forward: list <alias> from <entity> where <cond> group by <key> aggregate <func> lowers to a new GroupedQuery IR kind (not RepositoryCall) and binds <alias> to a fixed 2-column (key, value) derived RowSet reusing the existing 5 aggregate functions per group — consumed only by existing RowSet operations (order by/limit/aggregate), re-grouping rejected in v1. Design-only (Draft); implementation is a follow-up issue. Updates RFC-0001 §Open Questions/1, RFC-0025 §Reference-level Specification/1, §Reference-level Specification/3, RFC-0012 §G12.2, RFC-0038 §Reference-level Specification/2, §Open Questions/1, RFC-0045 §Open Questions/1 |
Forty-six are Accepted, two (0034, 0048) are Draft; 0000 is superseded by 0007, which was itself formally
accepted 2026-08-03 (issue #11). Every cross-consistency check passes and the owner
approved. From here a substantive change is never made by editing an RFC. There are
two ways to change one, and they are sized to the change (RFC-0007 §2.2): Supersedes
replaces an RFC whole and closes it; Updates revises named sections while the RFC
stays Accepted. The second relation exists because the first one alone made a one-line
revision cost a full restatement — and a cost that high is what turns "don't edit an
Accepted RFC" into a rule people break. The promotion basis is recorded in
docs/CONSISTENCY-CHECK.md.
Grounded in external evidence, not intuition. Full sourcing in docs/RESEARCH-NOTES.md.
| Decision | Basis |
|---|---|
| Indentation is not significant (offside rule rejected) | Whitespace, indentation, and newlines are ~24.5% of code tokens, and offside-rule languages cannot strip them (arXiv:2508.13666) |
| Reduced nesting, explicit top-level declarations | MoonBit, an AI-native language — less nesting is KV-cache friendly |
| IR canonical form = RFC 8785 (JCS) | Don't invent a canonicalization scheme |
| IR schema = constrained-decoding-compatible subset | No oneOf, no default, nesting ≤ 5 — agents must be able to emit IR fragments as structured output |
| MLIR instead of lowering straight to LLVM IR | Optimize while high-level semantics are still present, then lower progressively |
| Protocol = JSON-RPC 2.0, aligned with A2A / MCP | Agent↔agent follows A2A, agent↔tool follows MCP — same base |
| KB = 3-tier progressive disclosure | Anthropic Agent Skills pattern (metadata → body → resources) |
| MVP is an interpreter before LLVM | WebAssembly convention — the reference interpreter is an executable specification |
The specification is not prose alone. One golden scenario, "Login", runs through all seven documents — grammar → IR → runtime → compiler passes → KB → agent messages — and its two ends are machine-checked against each other.
python3 -m pip install --user jsonschema
# Is the golden IR valid against the schema?
python3 scripts/validate_ir.py examples/login.lir.json
# Can the validator itself fail? (1 positive + 3 negatives)
python3 scripts/validate_ir.py --self-test--self-test does not merely confirm that the golden example passes. Three
deliberate corruptions — a required field removed, an undefined kind injected, an
undefined extra field injected — must all be rejected for it to exit 0. A check
that only ever confirms success is not a check.
# the implementation's own suite
PYTHONPATH=impl .venv/bin/python -m unittest discover -s impl/tests -t impl
# and a mutation check: can that suite actually fail?
.venv/bin/python impl/tests/mutation_check.pyRan 3670 tests in 144.498s
OK
The mode B tests need the MLIR/LLVM tools on PATH (see Mode B);
without them that number is the same but a few dozen cases error out on the missing
toolchain. bash scripts/dev_doctor.sh exits 0 when the environment is complete, and
otherwise prints exactly what is missing.
mutation_check.py removes one specification rule at a time — 77 of them — and
requires the suite to go red for each. It begins with a no-op control: a mutation
that provably cannot change behaviour, which must survive. That control is not
ceremony. An earlier version of this harness copied only impl/ into the mutant tree
while the tests resolve the repo from __file__, so every mutant died on a missing
data file before any rule ran — it reported "all caught" while proving nothing, and an
independent audit caught it. If the control goes red the harness stops and reports
that nothing else was measured.
Between them the suite, the harness, and two rounds of adversarial audit have found real defects at every level:
- Runtime. Retries bounded only by the attempt cap and not the workflow deadline.
A mode B pipeline that stopped lowering at the
cfdialect, sowhenguards failed to compile. - Three rules no test asserted at all — non-idempotent retry, SLO-measurement, and
at-least-once emit. The first two hid behind the broken harness. The third hid behind
something worse: a test that named the rule but seeded its fixture so nothing
failed, making
attempts == 1true no matter what the runtime did. A test can be green, well-named, and still assert nothing. - The Reviewer, five times. Its first ownership check looked only at new nodes, so a
removal expressed as an edit passed. Then: provenance checked for presence but not
form; provenance that matched the form and resolved to nothing; the same removal
reached through
constraintsinstead ofchildren, because the review gate and the apply gate asked different questions; ownership cycles longer than one hop; a kind swap under an existing id; and one node given two owners. Each fix was defeated by the adjacent variant until the checks were rewritten from RFC-0001's structure rules (2, 4, 6) instead of from the cases already known to fail.
The lesson that generalises: a gate written per-case gets walked case by case. A gate written from the rule closes the family.
Cross-consistency verdicts (C1–C9, each with a negative control) live in docs/CONSISTENCY-CHECK.md.
All eight gaps that RFC-0002 Appendix A.4 recorded at design time are now closed:
| Gap | Resolution |
|---|---|
| Effect nodes had no surface notation | A closed verb lexicon derives them deterministically; an unlisted verb derives nothing |
No node id derivation rule |
One uniform rule (kind prefix + PascalCase split + redundant-kind-word strip) that reproduces every golden id |
| No heap primitive contract | RFC-0003's transfer primitive: created only at declared transfer boundaries, reference-counted, no GC scan |
| Guards had no IR kind | One Guard kind with a mode (when/until/repeat), not three kinds |
spec had no IR kind |
It is not meant to have one: spec becomes a declarative test manifest, executed by a runner |
Pipeline.name unsupplied by the grammar |
Optional name in the grammar; lowering derives one when absent |
Value-less performance metrics unserializable |
budgets[].value is optional — a flag has no value |
| Capability attribution provisional | A rule: the service's own database clause, or (single-service module) all of them, or a compile error — never a guess |
Three further spec-implementation gaps were closed after the RFCs were accepted, all of the same shape — the specification prescribed something the implementation did not do:
goalclauses silently vanished. RFC-0002 Appendix A.2 mapsGoalLineto aBusinessRulenode; the lowering ignored the clause entirely, so a declaration the author wrote did nothing at all. Now each goal line becomes aBusinessRuleowned by its service.- Modules were limited to one entity, refused with a citation to a gap that had
since been closed for an unrelated reason. A module may now declare several: the step
object names the entity (
load order), a single-entity module may omit it, and an ambiguous step is an error that lists the candidates rather than picking by declaration order. emitrefused to lower. It now takes the event as its object (emit userCreated→event.user.created) and the interpreter registers the publication with a unique dedupable id and a masked payload, per RFC-0003.
What remains open is recorded in each RFC's ## Open Questions and tracked as issues:
| Issue | What is deferred |
|---|---|
| #7 | RFC-0004 S5 — lowering the lnpl module with a real MLIR pass, and giving the dialect regions so it can represent concurrency |
| #9 | Mode B does not enforce RFC-0003's cache-TTL contract; mode A refuses without a budget and mode B does not |
| #11 | RFC-0007 is Status: Draft while being the effective process two Accepted RFCs build on |
| #12 | Mode A reads a Presence guard's condition from the payload while mode B takes a separate skip flag |
Those are deferred decisions, not holes in a contract. The reference implementation refuses what it cannot decide instead of guessing: an unknown verb, an unevaluable condition, an unattributable capability, and an unsupported spec expectation all produce an error that cites the RFC clause that owns the question.
The full set is indexed in RFC-0002 Appendix A.4 (8 items) and as Phase 1 risks R1–R6 in the roadmap.
| Phase | Contents | Done when |
|---|---|---|
| 1 | Parser (.lnpl → .lir.json) + IR interpreter running the golden scenario |
Golden run matches the RFC-0003 timeline; test suite established |
| 2 | LLVM backend (mode B) + one generated artifact (OpenAPI) | Both execution modes are equivalent in observable behavior |
| 3 | KB seeded across 12 categories + a two-agent protocol round trip | The RFC-0006 example cycle reproduces |
The reference implementation is Python. The roadmap originally picked Rust for it, on the grounds of LLVM bindings and single-binary distribution — but those are Phase 2 concerns, and a Phase 1 reference interpreter is better served by clarity, in the WebAssembly sense of an executable specification. The revision is recorded in docs/ROADMAP.md §0 (decision D10).
Other projects address the same problem: lhaig/intent
(a contract-based language for AI-generated code),
l3yx/intentlang (an intent language embedded in
Python), and pboueri/intentc. linkly differs in
four ways: its IR is semantic rather than syntactic (BusinessRule / Effect
nodes), its lowering path is MLIR → native, the knowledge base is a
first-class component, and the protocol is aligned with A2A / MCP.
CHARTER.md Stage-0 vision document (preserved verbatim — the RFCs are canonical)
rfcs/0000~0023 The RFCs (0000 Superseded, the other 23 Accepted)
schemas/lir.schema.json IR JSON Schema (draft 2020-12)
examples/login.lnpl Golden scenario source
examples/login.lir.json The same scenario as IR
scripts/validate_ir.py Schema validation + self-test
docs/rfc-0001-semantic-ir.en.md English summary of RFC-0001 (the RFCs themselves are Korean)
docs/GLOSSARY.md Canonical terminology
docs/RESEARCH-NOTES.md External basis for design decisions
docs/CONSISTENCY-CHECK.md Cross-consistency verdicts (C1–C9)
docs/ROADMAP.md Three phases + risks
plans/rfc-suite/ The plan that produced this suite (20 decisions, 10 tasks)
impl/lnpl/ Phase 1 reference implementation (lexer, parser, lowering, interpreter)
impl/tests/ Unit suite + mutation_check.py (proves the suite can fail)
kb/ The seeded knowledge base (12 categories, RFC-0005 layout)
MIT — see LICENSE.