Metadata-Version: 2.5
Name: lnpl
Version: 0.9.0
Summary: LNPL — the surface language, Semantic IR, and runtime of the linkly LLM-native programming platform
License: MIT License
        
        Copyright (c) 2026 choiyounggi
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: jsonschema
Provides-Extra: lint
Requires-Dist: ruff==0.16.5; extra == 'lint'
Description-Content-Type: text/markdown

# linkly

**English** | [한국어](README.ko.md)

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](#status).

---

## Two design axes

### 1. You write intent, not code

The surface language — **LNPL** (working name, `.lnpl`) — carries declarations only:

```lnpl
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`](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.

```bash
# 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`](plugins/lnpl/README.md) | people writing `.lnpl` | vocabulary routing, compile diagnostics on save, spec derivation, a completion gate, KB lookup |
| [`lnpl-dev`](plugins/lnpl-dev/README.md) | people building linkly itself | environment-prerequisite diagnosis, RFC process lint, the mutation harness's traps |
| [`lnpl-mcp`](plugins/lnpl-mcp/README.md) | 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](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.

```bash
# 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](#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](docs/backends.md) §8). The first such
package is [lnpl-postgres](https://github.com/choiyounggi/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](https://github.com/choiyounggi/linkly/issues/121)). The
cache-side counterpart is [lnpl-redis](https://github.com/choiyounggi/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](https://github.com/choiyounggi/linkly/issues/143))
— the first real user of RFC-0043's `cache_scope` self-report (`"shared"`). The
trace-side counterpart is [lnpl-otel](https://github.com/choiyounggi/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](https://github.com/choiyounggi/linkly/issues/144)).

**65 RFCs — 62 `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](https://github.com/choiyounggi/linkly/issues/11)). See the
[roadmap](docs/ROADMAP.md).

RFC bodies are written in Korean; identifiers, keywords, and schema fields are English.
The central one has an English summary:
[RFC-0001 Semantic IR](docs/rfc-0001-semantic-ir.en.md) — the document the rest of the
suite is defined against.

---

## The RFC suite

| RFC | Contents |
|-----|----------|
| [0000 RFC Process](rfcs/0000-rfc-process.md) | *Superseded by 0007* — lifecycle, numbering, the fixed 7-section template |
| [0007 RFC Process v2](rfcs/0007-rfc-process-v2.md) | Adds the `Updates` relation: revise a named *section* without replacing the RFC |
| [0001 Semantic IR](rfcs/0001-semantic-ir.md) — [English summary](docs/rfc-0001-semantic-ir.en.md) | 21 node kinds, 18 semantic types, flat structure, canonical JSON serialization |
| [0002 Syntax](rfcs/0002-syntax.md) | Line-oriented, keyword-delimited EBNF (58 productions) + grammar→IR lowering map |
| [0003 Runtime](rfcs/0003-runtime.md) | Actors, structured concurrency, policy enforcement, memory primitives, observability contract |
| [0004 Compiler](rfcs/0004-compiler.md) | MLIR progressive lowering (7 stages), pass invariants, three optimizer responsibility axes |
| [0005 Knowledge Base](rfcs/0005-knowledge-base.md) | 12 categories, 3-tier progressive-disclosure routing, consumption interface |
| [0006 Agent Protocol](rfcs/0006-agent-protocol.md) | 9 roles, 8 JSON-RPC methods, structured errors, idempotency, task lifecycle |
| [0008 Guard Conditions](rfcs/0008-guard-conditions.md) | Guard conditions: presence check & comparison (2 forms), spec correction, mode B compilation. *Updates 0002 §Full grammar, 0003 §Guard* |
| [0009 Guard Condition OQ](rfcs/0009-guard-condition-open-question.md) | Retires RFC-0002's open question ② now that the grammar is settled. *Updates 0002 §Open Questions* |
| [0010 Proposal Intent](rfcs/0010-proposal-intent.md) | 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](rfcs/0011-refinement-enum-and-name-collisions.md) | Which refinement names are legal, and what happens when two declarations claim one. *Updates 0001 §부록 A.6.3, §부록 A.7* |
| [0012 Execution Scope](rfcs/0012-execution-scope.md) | 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](rfcs/0013-step-attempt-ceiling.md) | 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](rfcs/0014-guard-skip-observability.md) | 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](rfcs/0015-value-semantics.md) | 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](rfcs/0016-time-and-schedule-semantics.md) | 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](rfcs/0017-guarded-example-correction.md) | 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](rfcs/0018-repeated-step-observation-fold.md) | 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](rfcs/0019-misleading-indentation.md) | 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](rfcs/0020-spec-given-input-namespace.md) | 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](rfcs/0021-diagnostic-severity-levels.md) | 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](rfcs/0022-mode-b-observation-surface.md) | 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](rfcs/0023-guard-scope-diagnostic.md) | 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](rfcs/0024-enforcement-diagnostic-line.md) | 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](rfcs/0025-row-sets-and-aggregation.md) | `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](rfcs/0026-unknown-verb-line-and-suggestion.md) | `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](rfcs/0027-network-driver-and-result-binding.md) | `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](rfcs/0028-arithmetic-and-alternative-guards.md) | `*`/`/` 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](rfcs/0029-clock-contract-and-real-binding.md) | 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](rfcs/0030-create-result-binding-and-payload-seed.md) | `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](rfcs/0031-multi-file-compilation-unit.md) | 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](rfcs/0032-transaction-boundary-and-rollback-enforcement.md) | 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](rfcs/0033-namespace-directories.md) | 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](rfcs/0034-network-call-compensation.md) | *(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](rfcs/0035-authorization-enforcement-deferred-scope.md) | 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](rfcs/0036-policy-rollback-declaration-effect.md) | 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](rfcs/0037-http-resilience.md) | `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](rfcs/0038-list-where-predicate.md) | `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](rfcs/0039-note-verb-and-canonical-line.md) | `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 `note`s 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](rfcs/0040-event-consumption-contract.md) | `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](rfcs/0041-parallel-block-execution.md) | 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](rfcs/0042-extension-diagnostics.md) | 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](rfcs/0043-driver-enforcement-reporting.md) | 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](rfcs/0044-money-arithmetic.md) | 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](rfcs/0045-rowset-aggregation-extension.md) | `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](rfcs/0046-rfc-example-realignment.md) | 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`](rfcs/0047-aggregate-field-type-carriage.md) | 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`](rfcs/0048-collections-non-goal-and-rowset-group-by.md) | 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`](rfcs/0049-emit-payload-mapping.md) | `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). Implementation ships with this RFC. *Updates RFC-0002 §Full grammar* |
| [0050 Numeric-shape guard predicate](rfcs/0050-numeric-guard-predicate.md) | 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. Implementation ships with this RFC. *Updates RFC-0015 §1, RFC-0014 §2.4, RFC-0028 §6* |
| [0051 Money arithmetic in `set` and guards](rfcs/0051-money-arithmetic-in-set-and-guards.md) | 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. *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>`](rfcs/0052-lookup-key-clause.md) | 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. *Updates RFC-0002 §Full grammar (+RFC-0049), RFC-0012 §G12.2/§G12.5/§G12.6, RFC-0016 §5 (+RFC-0051)* |
| [0053 `optional` field modifier](rfcs/0053-optional-field-modifier.md) | 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. *Updates RFC-0001 §노드 카탈로그/Entity, RFC-0016 §3 (+RFC-0038, +RFC-0051), RFC-0015 §3 (+RFC-0051)* |
| [0054 Text and enum equality in guards](rfcs/0054-text-equality-guards.md) | 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. *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`](rfcs/0055-injected-now-and-generated-id-field-markers.md) | 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. *Updates RFC-0030 §4* |
| [0056 `fail` — declared business rejection](rfcs/0056-fail-business-rejection.md) | 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. *Updates RFC-0001 §노드 카탈로그 (Effect, WorkflowStep), RFC-0003 §Execution Model (+RFC-0032), RFC-0014 §Open Questions/1* |
| [0057 `call ... send` — outbound body mapping](rfcs/0057-body-mapping-clause.md) | 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. *Updates RFC-0027 §Reference-level Specification/2* |
| [0058 Reject indentation that contradicts a pipeline's implicit close](rfcs/0058-pipeline-implicit-close-indentation.md) | 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. *Updates RFC-0002 §Block structure (+RFC-0019)* |
| [0059 `respond` named aggregate terms and list terms](rfcs/0059-respond-aggregate-and-list-terms.md) | 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 `create`d 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`). *Updates RFC-0025 §5 (+RFC-0048)* |
| [0060 Guards on assigned fields, and `otherwise`](rfcs/0060-assigned-field-guard-and-otherwise.md) | 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`. *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`](rfcs/0061-event-publisher-spi.md) | 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. *Updates RFC-0040 §Motivation, §Reference-level Specification/7. 오류 분류, §Reference-level Specification/8. 레퍼런스 릴레이, §Reference-level Specification/9. 문서, §Alternatives* |
| [0062 `cached` read-through clause](rfcs/0062-cached-read-through-clause.md) | `.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. *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](rfcs/0063-default-body-omits-password-fields.md) | 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. *Updates RFC-0057 §Guide-level Explanation, §Reference-level Specification/4, /5, /8* |
| [0064 Update/delete that affects no row fails not-found](rfcs/0064-write-miss-not-found.md) | `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. |

Sixty-two 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](docs/CONSISTENCY-CHECK.md).

---

## Why each decision was made

Grounded in external evidence, not intuition. Full sourcing in
[docs/RESEARCH-NOTES.md](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](https://arxiv.org/html/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.

```bash
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.

```bash
# 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](#mode-b--a-native-binary));
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](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](https://github.com/choiyounggi/linkly/issues/7) | RFC-0004 S5 — lowering the `lnpl` module with a real MLIR pass, and giving the dialect regions so it can represent concurrency |
| [#9](https://github.com/choiyounggi/linkly/issues/9) | Mode B does not enforce RFC-0003's cache-TTL contract; mode A refuses without a budget and mode B does not |
| [#11](https://github.com/choiyounggi/linkly/issues/11) | RFC-0007 is `Status: Draft` while being the effective process two Accepted RFCs build on |
| [#12](https://github.com/choiyounggi/linkly/issues/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](docs/ROADMAP.md).

---

## 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](docs/ROADMAP.md) §0 (decision D10).

---

## Prior art

Other projects address the same problem: [lhaig/intent](https://github.com/lhaig/intent)
(a contract-based language for AI-generated code),
[l3yx/intentlang](https://github.com/l3yx/intentlang) (an intent language embedded in
Python), and [pboueri/intentc](https://github.com/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~0064              The RFCs (0000 Superseded, 62 Accepted, 2 Draft)
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](LICENSE).
