Skip to content
choiyounggiPublic

About

An LLM-native programming language: you declare intent, and it lowers to a semantic IR that runs on an interpreter or compiles through MLIR to a native binary.

Topics

Resources

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

linkly

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.


Two design axes

1. You write intent, not code

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.

2. The Semantic IR is the hub

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.


Try it

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.lnpl

The 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".

Claude Code plugin

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-doctor and its tests still look for the lnpl console script in .venv. Run .venv/bin/pip install . once after creating the venv, and pip install --force-reinstall --no-deps . after editing impl/lnpl/.

Mode B — a native binary

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.lnpl
PASS 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.


Status

All three roadmap phases are complete.

  • Mode A — .lnpl parses, lowers to Semantic IR, and runs on the IR interpreter.
  • Mode B — the same source compiles through MLIR to a native binary. The custom lnpl dialect is declared in IRDL and loaded into stock mlir-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.

~5,390 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).

65 RFCs — 46 Accepted, RFC-0000 Superseded by RFC-0007, RFC-0034 Draft, RFC-0048 Draft, RFC-0049 Draft, RFC-0050 Draft, RFC-0051 Draft, RFC-0052 Draft, RFC-0053 Draft, RFC-0054 Draft, RFC-0055 Draft, RFC-0056 Draft, RFC-0057 Draft, RFC-0058 Draft, RFC-0059 Draft, RFC-0060 Draft, RFC-0061 Draft, RFC-0062 Draft, RFC-0063 Draft, RFC-0064 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.


The RFC suite

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
0049 emit ... with and payloadMap payloadMap sat unused in the IR schema since RFC-0001 — no authoring syntax filled it and interp.py never read it, so emit's trailing words past the event name were silently dropped. Adds emit/publish <Event> with <ref> <ref>..., mapping the emitted payload from three admitted sources (a create ... as/read-bound declared field — a derived one only when a preceding set/format in the same guard scope fills it (issue #204) —, input.<field>, or a call ... as <name> network-result binding, unchecked) into an ordered payloadMap array; rejects a bare ref, a derived field no such set/format fills, a Password field, a duplicate mapped name, and any other trailing words (a compatibility break — a full corpus sweep found zero existing occurrences at risk). Mode B is unaffected (it never lowered EventEmit payload content for either form). Design-only (Draft); implementation ships with this RFC. Updates RFC-0002 §Full grammar
0050 Numeric-shape guard predicate A guard comparing a non-numeric value (an external response's rate=abc) raises RunError by design (RFC-0015 §4), and the language had no way to ask "is this a number?" first — so a fallback branch for a malformed response could not be written (probe-v0.8 s2 F-5). Adds the pair <ref> is-numeric / <ref> is-not-numeric (exact complements, including for an absent reference; never fails). Unlike exists/missing it may be an and term (when r.status == 200 and r.rate is-numeric), and the fallback is an or alternative (or r.rate is-not-numeric). Static rule is the comparison's: declared Integer/DateTime accepted, Text/Money refused, undeclared refs decided at runtime; list where refuses it. Mode B refuses to build a workflow using it, recorded as a differential exemption. Draft; implementation ships with this RFC. Updates RFC-0015 §1, RFC-0014 §2.4, RFC-0028 §6
0051 Money arithmetic in set and guards Money could only be aggregated, so set line.lineTotal to line.unitPrice * line.qty and set report.net to report.gross - report.fees (even the plain copy set report.net to input.net) were compile refusals and the computation left the language (probe-v0.8 s1 F-1, s3 F-1). Makes money a dimension beside scalar/instant: copy, Money ± Money, Money × Integer (either order, a literal included) and Money-vs-Money comparison under all six comparators; Money × Money, any division, Money ± a number and Money-vs-number stay refused naming RFC-0051, as does a set whose target and value differ in dimension; Decimal stays refused (RFC-0044 OQ 3); exists/missing and is-numeric on Money are refused. Specifies mode A evaluation in exact minor units (different currencies fail the step with money-currency-mismatch, equality across currencies is false), and mode B refusing a Money guard as a recorded differential exemption; Money set needs no mode B change. No grammar delta. Draft. Updates RFC-0016 §3 (+RFC-0038 §3), RFC-0015 §3, RFC-0044 §5, RFC-0016 §5, RFC-0028 §6 (+RFC-0050)
0052 Lookup-key clause by <ref> Every entity's row in one run lived under the one payload id, so find product + create order made two orders for the same product collide (entity.order already exists) and Stock.onHand could never be found and decremented under the product's id (probe-v0.8 s1 F-3/F-6). Adds an optional by <ref> to find/read/load/authenticate/update/delete: the ref's run-time value becomes the row key (find stock by input.productId), a set on a row read that way persists under that key, and a ref that resolves to nothing fails the step instead of falling back to -. Admits bare, input.<field>, caller.*, network-result and bound non-derived/non-Password fields; any other trailing word on those verbs is now a compile error (was silently dropped); create keeps as <name> only. Seeds a first read by input.<field> under that field's value; mode B refuses a by workflow as a recorded differential exemption. Draft. Updates RFC-0002 §Full grammar (+RFC-0049), RFC-0012 §G12.2/§G12.5/§G12.6, RFC-0016 §5 (+RFC-0051)
0053 optional field modifier Every declared entity field was required, so adding one field turned every request from a client that did not send it yet into a 400 (issue #208, AIP-180). Adds a second field modifier, <name> <Type> optional (optional derived together, a repeated modifier, or id ... optional is a compile error). An absent or JSON-null optional field passes validate (a present value is still type-checked), is never stored (no invented default), is left out of OpenAPI required (request type becomes nullable), is not a db check/stored-row-shape-mismatch mismatch, and is omitted from respond/emit ... with instead of sent as null; lnpl migrate --set still backfills it; _schema_gen ignores the flag. Opens exists/missing on an optional field of any type and registers optional-field-unguarded-arithmetic (warning); mode B refuses a guard that reads an optional field. Default values stay out of scope. Draft. Updates RFC-0001 §노드 카탈로그/Entity, RFC-0016 §3 (+RFC-0038, +RFC-0051), RFC-0015 §3 (+RFC-0051)
0054 Text and enum equality in guards A guard could not branch on a status string — when order.status == input.expected was refused as "neither Integer nor DateTime", so CancelOrder could cancel an already-cancelled order and a spec could not assert a status (issue #207). Extends RFC-0038's list where equality to when/until guards and their or alternatives: ==/!= on a Text-family field (base UUID, Email, Phone, Currency, Html, Markdown or Text, enum refinements included) against a reference of the same base or a bare-name literal. A bare name is a literal only when paired with such a field (elsewhere it is still a payload field); the compiler records that decision on the Guard as textEqualityOperands, and against an enum refinement the literal must be a member (the refusal lists the members and adds a did-you-mean for a close typo). Ordering on Text, arithmetic on either side, and Password fields (RFC-0001 masking) stay refused. spec result <ref> ==/!= <value> evaluates the same way; the set refusal on a Text field now names format. Mode B refuses such a workflow as a recorded differential exemption, after the Money, lookup and optional checks. Draft. Updates RFC-0016 §3 (+RFC-0038, +RFC-0051, +RFC-0053), RFC-0038 §3, RFC-0015 §3 (+RFC-0051, +RFC-0053), RFC-0001 §노드 카탈로그/Guard (+RFC-0028)
0055 Run-filled fields: derived generated and derived clock The server could make neither a row id nor the current time, so a create without a payload id put every row under <entity>#- — the second create was a 409 forever, and a UUID field stored the row-key string (issue #209). Resolves RFC-0016 Open Question 6 with two fill-source markers on a derived field: derived generated (UUID base, a UUIDv4 per run) and derived clock (DateTime base, the run's start instant from the RFC-0029 Clock). Values are decided once per run and applied only at a create of the marked entity — never written into the input payload; a generated id keys the row. A create with no payload id and no generated id now fails id-required (400) before writing (a corpus sweep found nothing relying on -); an undeclared bare name in a set value is a compile error with a marker suggestion; spec pins values with given run.generated/run.clock; mode B refuses a fill-source create. Draft. Updates RFC-0030 §4
0056 fail — declared business rejection A workflow had no way to fail on a business rule: an out-of-stock or over-limit check skipped its step and the run still ended completed (HTTP 200), so a client had to parse skipped[] to tell a rejection apart and spec could not contract it (issue #206; RFC-0014 Open Question 1). Adds one verb, fail <kebab-code>, written under a guard: a false guard skips it as before, a true one ends the run failed with failure_kind rejected and the code as failure_reason, rolls the run's writes back (RFC-0032) and is never retried. The code is a compile-time kebab-case literal; a missing, malformed or extra-word code, one equal to a server problem code (not-found, write-conflict, ...), and a fail no when/until guard owns (repeat N always runs its body) are compile errors. lnpl serve answers 422 problem+json whose code is the author's code (422 for every code, typed by failure_kind); consume by classifies it as E7 (422 event-rejected, no retry); OpenAPI lists the codes under the operation's 422; spec contracts it with expect failed + error reason <code>. Mode B refuses such a workflow as a recorded differential exemption, after the Money, lookup, optional and Text checks. Workflows without fail are byte-identical. Draft. Updates RFC-0001 §노드 카탈로그 (Effect, WorkflowStep), RFC-0003 §Execution Model (+RFC-0032), RFC-0014 §Open Questions/1
0057 call ... send — outbound body mapping An outbound call/request always sent the whole run input as its body: a value the workflow computed (set o.total to ...) could not be sent, fields the peer never asked for leaked out, and with already meant path substitution (issue #200). Adds a send <ref>... clause, combinable with with (path) and as (result binding) only in the fixed order send, with, as — any other order is a compile error. The body is exactly the listed fields, each named by its reference's last segment, built and masked by the same code as emit ... with (RFC-0049); the reference rules are RFC-0049's, reused: bare names, unknown bindings, Password-family fields, duplicate field names and a derived field with no preceding set/format are compile errors, while a run-filled field (RFC-0055) is accepted — for emit ... with too. guard-scoped-binding-escape covers send. With no send the body is byte-identical (the whole input, unmasked — which still carries a Password-family input field in clear text; measured and left to a separate issue). No spec body assertion and no new diagnostic. Mode B supports it unchanged. Draft. Updates RFC-0027 §Reference-level Specification/2
0058 Reject indentation that contradicts a pipeline's implicit close A pipeline closes at the next keyword, never by indentation, so a when/until/repeat/pipeline/parallel indented inside an open pipeline closed it silently with zero diagnostics — in issue #211 (3) a step written under the stock guard ran even when stock was short. Adds a third case to RFC-0019's closed list of layout contradictions: a control keyword written deeper than the open pipeline line it closes is a compile error naming that pipeline and the fix (a guard: dedent it to the pipeline's column or wrap the following steps in a new pipeline; a block opener: dedent it). Layout that says nothing (keyword at the pipeline's column) is unaffected. States as contract that a comparison on an unbound reference is false (RFC-0012 §G12.4), which the new canonical multi-stage example examples/staged.lnpl relies on: guarded pipelines chained, where the second guard reads pay.status that only the first pipeline binds. Draft. Updates RFC-0002 §Block structure (+RFC-0019)
0059 respond named aggregate terms and list terms A read-only workflow could not answer a count or a filtered list: respond took only single-row <binding>.<field> paths, so the answer had to be created into a row first — the read wrote to the store and the second identical call failed 409 (issue #210). respond now also takes named aggregate terms <name> as <func> <ref> (the existing five aggregates only, same type rules as set, answered as flat keys) and, alone on its line, respond list <binding>, answered as expose list's {items, next} envelope with next always null; every list feeding it must declare limit. Neither writes a row, so calling it twice gives the same 200. OpenAPI derives both; spec asserts a term with result <name> == <value>; mode B refuses both (last, after fail, in build and diff). Draft. Updates RFC-0025 §5 (+RFC-0048)
0060 Guards on assigned fields, and otherwise Two gaps made multi-stage conditional flows hard (issue #211 (1), (4)). A guard could not read a field an earlier step assigned (when product.stock >= 0 after set product.stock to product.stock - input.quantity was a compile error, RFC-0015 §3), and there was no "otherwise" branch (else was a no-op step whose suggestion was delete). Resolves RFC-0015 Open Question 1: mode A evaluates such a guard against the value at that point; mode B refuses the workflow as a recorded exemption, because it fixes condition fields at entry. Adds otherwise on the line after a when guard's item: it owns exactly one item (a step or a block), runs only when the guard and every or alternative are false, and is the guard's sibling, so nesting stays at depth 2. The branch that did not run is always in skipped[] (mode: "otherwise" when the guard held). otherwise with no preceding guard, a second otherwise, otherwise after until/repeat, or inside parallel is a compile error; else now suggests otherwise. Mode B refuses any workflow using otherwise. Draft. Updates RFC-0015 §3 (+RFC-0051/0053/0054) and §Open Questions, RFC-0001 §노드 카탈로그/Guard (+RFC-0028/0054), RFC-0002 §Full grammar (+RFC-0012/0052) and §Block structure (+RFC-0019/0058), RFC-0014 §2.1 (+RFC-0028) and §2.4 (+RFC-0028/0050)
0061 Event Publisher SPI — lnpl.publishers Outbox events had two ways out of a process: hand-rolled consumer code after outbox drain, or lnpl relay --target <base-url> (HTTP-only, CloudEvents, to another lnpl process). Neither reaches a real broker without bespoke retry/ordering/DLQ code (issue #191). Adds the lnpl.publishers entry-point group and an EventPublisher contract (publish, publish_batch, close) lnpl relay --target <scheme>://... dispatches to by URL scheme — http(s):// stays byte-identical, any other scheme loads a registered driver; ack happens only after a confirmed publish, a PublishRejected exception acks-and-dead-letters (mirroring RFC-0040 §7's 422 bucket), any other failure leaves the row un-acked for the next drain. EventPublisherTCK proves publish-then-ack, failure->no-ack, a restart re-publishing, and ordering by the outbox seq — a discriminating test fails an ack-before-publish driver and a drop-on-error driver. Consumption stays unchanged: a driver provides its own broker -> HTTP /-/events bridge; this RFC adds no consume-side SPI. A real broker driver ships in a separate repository, out of scope here. Draft. Updates RFC-0040 §Motivation, §Reference-level Specification/7. 오류 분류, §Reference-level Specification/8. 레퍼런스 릴레이, §Reference-level Specification/9. 문서, §Alternatives
0062 cached read-through clause .lnpl could write a cache (cache) and invalidate it but never read it, so a read-through could not be written and redis MONITOR showed no GET across repeated reads (probe-v0.8 s4 F-2, issue #188). Adds an optional trailing cached to find/read/load/authenticate, after any by <ref> (find product by input.sku cached): a hit binds the cached row and skips the repository; a miss reads the repository and records the row under the same key with the owning service's performance cache TTL. A cached step on a service with no performance cache budget, or in a workflow that also writes that entity (update/delete/set), is a compile error; a cache outage counts as a miss. Writes this document makes invalidate the key after the repository commit, and a rolled-back run discards the keys it cached. Spec blocks observe cache hit / cache miss; mode B refuses a cached workflow as a recorded differential exemption. Draft. Updates RFC-0002 §Full grammar (+RFC-0012, RFC-0052, RFC-0060), RFC-0003 §Reference-level Specification/Execution Model (+RFC-0032, RFC-0056)
0063 Default call body leaves out Password-family fields A call/request with no send clause sent the whole run input, so a Password-family input field reached the peer in clear text — the one output channel issue #43's masking contract did not cover (issue #214). The default body now leaves out every input key that any entity declares as Password or a refine ... of Password (the key is absent, never ***); an input with no such field is passed through as the same object, byte-identical. One filter at the one network-call site, sharing mask_payload's name derivation. send and mode B are unchanged. Path arguments (with input.<Password field>) and the compile-time same-name gap in send/emit ... with are now refused at compile time over every declaring entity (issues #218, #219); the first-entity view of the other masking channels is a named follow-up. Draft. Updates RFC-0057 §Guide-level Explanation, §Reference-level Specification/4, /5, /8
0064 Update/delete that affects no row fails not-found update/delete (bare or by <ref>) that touched no row ended completed (serve 200) while the same key's find was a 404 since issue #197 (issue #215). A write whose driver answers {"affected": 0} now fails its step with failure_kind not-found — serve 404, consume path 422 — on fake, sqlite: and mode B alike, and earlier writes of the run roll back (RFC-0032). Delete fails by default; an idempotent-delete notation is not offered in this change. The driver TCK gains an update-on-absent-row case external drivers must pass. Draft.

Forty-six are Accepted, eighteen (0034, 0048, 0049, 0050, 0051, 0052, 0053, 0054, 0055, 0056, 0057, 0058, 0059, 0060, 0061, 0062, 0063, 0064) 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.


Why each decision was made

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

Verification

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.py
Ran 5394 tests in 154.320s
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 cf dialect, so when guards 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 == 1 true 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 constraints instead of children, 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.


Closed gaps, and what is still open

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:

  • goal clauses silently vanished. RFC-0002 Appendix A.2 maps GoalLine to a BusinessRule node; the lowering ignored the clause entirely, so a declaration the author wrote did nothing at all. Now each goal line becomes a BusinessRule owned 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.
  • emit refused 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.


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).


Prior art

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.


Structure

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)

License

MIT — see LICENSE.

About

An LLM-native programming language: you declare intent, and it lowers to a semantic IR that runs on an interpreter or compiles through MLIR to a native binary.

Topics

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages