Skip to content

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.

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


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 IREnglish 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/httprepository/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.


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

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages