From 0ca0baa78ba9c7886ddd5da8ade497690a71f8b3 Mon Sep 17 00:00:00 2001 From: Omri Katz Date: Tue, 4 Aug 2026 11:14:37 +0300 Subject: [PATCH] The last three pages stop describing a language that does not exist The landing page, file-formats, and writing-prompts were pre-pivot fossils: napl gen, the .mapl margin reply, emoji aliases, 0444 locks, lock.json, ir/. None of that is in the binary. Rewritten against the real model: both sides truth, the guarded door, attribution and blame, drift marked never silent, docs fences rendering the reference pages. writing-prompts keeps the load-bearing-sentences doctrine and the war story; the dead mechanics around them are gone. Co-Authored-By: Claude Fable 5 --- apps/site/content/docs/file-formats.mdx | 153 ++++++++----------- apps/site/content/docs/index.mdx | 64 ++++---- apps/site/content/docs/writing-prompts.mdx | 168 ++++++++++----------- 3 files changed, 177 insertions(+), 208 deletions(-) diff --git a/apps/site/content/docs/file-formats.mdx b/apps/site/content/docs/file-formats.mdx index aea88367..450b6a64 100644 --- a/apps/site/content/docs/file-formats.mdx +++ b/apps/site/content/docs/file-formats.mdx @@ -1,25 +1,28 @@ --- title: File formats -description: The complete reference for .napl prompts, the .mapl reply, the emoji aliases, and the .napl/ state directory. +description: The complete reference for .napl prompts, the body directives, and the .napl/ records directory. --- -This page is the exhaustive reference for every file NAPL reads and writes: the `.napl` -prompt you author, the `.mapl` reply the toolchain generates, the emoji spellings of both, -and the `.napl/` directory that holds the derived state. For the narrative behind these, -see [Writing prompts that hold](/docs/writing-prompts). +This page is the exhaustive reference for every file napl reads and writes: the `.napl` +prompt you author, the directives inside its body, and the `.napl/` directory that holds +the records. For the narrative behind these, see +[Writing prompts that hold](/docs/writing-prompts). ## The `.napl` prompt -A prompt is YAML frontmatter (machine-facing metadata) followed by markdown prose (the -contract handed to the coding agent). Edit the example below and the validator runs as you -type. Break the YAML or misspell a field and the squiggle appears. +A prompt is YAML frontmatter (machine-facing metadata) followed by a markdown body (the +contract). It is found by its extension: a `.napl` file inside the crate that receives +its code is already in placement, and nothing registers it. Edit the example below and +the validator runs as you type. Break the YAML or misspell a field and the squiggle +appears. - The prose does not name the return type. Chose a plain string. -``` - -### Entry fields - -| Field | Type | Description | -| ------------- | ----------------- | -------------------------------------------------------------------- | -| `promptLines` | `LineRange` | The 1-based body line(s) the entry refers to (scalar `n` or `[a,b]`).| -| `kind` | see below | One of `ambiguity`, `assumption`, `no-op`, `note`. | -| `message` | `string` | Required, non-empty: the one-line summary. | -| `reasoning` | `string` | Optional prose reasoning (defaults empty). | -| `suggestion` | `string \| null` | Optional suggested rewording, chiefly for `ambiguity`. | - -### Kinds and severities - -The kind maps to an editor diagnostic severity: this is how ambiguity becomes a first-class -compile diagnostic: +A pinned case, equivalent to a frontmatter `tests` entry but living beside the prose it +pins. A duplicate case name across frontmatter and body is a compile error. A `name` +containing commas must be quoted. -| Kind | Severity | Meaning | -| ------------ | ------------- | ---------------------------------------------------------------------------------- | -| `ambiguity` | error | Your words were unclear. Red squiggle on the exact words, usually with a rewording.| -| `assumption` | warning | A decision your prose left open; the model chose one. | -| `no-op` | warning | The prompt changed but the code didn't: the entry must say why, or the gen fails. | -| `note` | information | Hover-visible reasoning about why the code is shaped this way. | - -## Emoji aliases - -Each side of the dialogue has a canonical extension and emoji aliases. Both spellings in a -pair are byte-identical to every tool (discovery, LSP, grammar, the VS Code extension): - -| Side | Canonical | Alias | Voice | -| ------- | --------- | ------- | ---------------- | -| Human | `.napl` | `.🧑` | what you write | -| Machine | `.mapl` | `.🤖` | its margin notes | +```napl title="a pinned case" +::: test +name: "an expired token is rejected, never renewed" +given: { token: expired } +expect: { error: SESSION_EXPIRED } +::: +``` -The human alias is a single code point on purpose. ZWJ sequences (a base -emoji joined to another with a zero-width joiner) are rejected so filenames stay -filesystem-safe and round-trip cleanly across tools and shells. Override the human alias in -`lock.json`: +### `::: docs`, `::: example`, `::: note`, `::: warning` -```json title=".napl/lock.json" -{ - "model": "claude-sonnet-5", - "promptAliases": [".🧑"] -} -``` +Documentation fences. They are prose about the module, excluded from the semantic body: +editing them never re-keys the module, and `napl commit` records such an edit as a +`docs-only` transition. The reference pages on this site, including +[the compiler's own](/docs/reference/napl), are rendered from exactly these fences in +the committed prompts. A `docs` fence carries markdown; an `example` fence carries a +titled code block; `note` and `warning` render as callouts. -Each override entry starts with `.`, carries a single code point after the dot, and may -not contain a ZWJ. **The mirror rule:** `napl gen` writes `.mapl` for a `.napl` prompt and -`.🤖` for an emoji-spelled one. It mirrors your choice. Reading accepts both spellings on -both sides. +## The `.napl/` records directory -## The `.napl/` directory +Committed by design: the durable record of every landing, and what lets `napl status` +and `napl fsck` verify the prompt/code connection in CI from an ordinary checkout. -Committed by design: the durable record of every gen, and what lets `napl status` verify the -prompt/code connection in CI without regenerating. +| Path | Contents | +| --------------------------- | -------------------------------------------------------------------------- | +| `store/HEAD` | The current head pointer. Marked `binary` in git so merges never write conflict markers into it. | +| `store/objects/pack/` | One content-addressed pack and its index. Every successful commit repacks, so the store a reader sees is always these two files plus `HEAD`. | +| `store/notes/` | Notes attached to recorded transitions. | +| `attribution/.yaml` | Prompt-line to code-line claims. A record the toolchain writes; never hand-edit it. | +| `staging/` | Records staged by `napl add`, consumed by `napl commit`. Transient. | -| Path | Contents | -| --------------------------- | -------------------------------------------------------------------- | -| `src//` | Generated code, per target. Locked `0444` after tests pass. | -| `map.json` | Per-module attribution of generated files and locked content hashes. | -| `lock.json` | Pinned model id, backend, agent preset, alias overrides. | -| `ir/.yaml` | Derived contracts: signatures, types, tests. Best-effort. | -| `attribution/.yaml` | Prompt-line to code-line span mappings. A hard gate at gen. | -| `mapl/.mapl` | The machine layer's reply. | -| `journal.jsonl` | Append-only gen history: prompt diff and per-file patch per gen. | +`napl init` writes the `.gitattributes` lines that keep the store quiet in diffs. Store +objects are never text: they carry `-text -diff`, because a text-normalizing clone on +Windows would corrupt them. Notes and attribution records keep their readable diffs, +because reading them is those files' purpose. -Build artifacts (`node_modules`, `dist`, lockfiles, `*.tsbuildinfo`, Rust `target/`) are -excluded from attribution and never locked. +Build artifacts (`node_modules`, `dist`, lockfiles, Rust `target/`) are not records and +never enter the store. Next: [Running it for real](/docs/running) for what reads and writes these files. diff --git a/apps/site/content/docs/index.mdx b/apps/site/content/docs/index.mdx index dde668bc..3fe5505c 100644 --- a/apps/site/content/docs/index.mdx +++ b/apps/site/content/docs/index.mdx @@ -1,60 +1,64 @@ --- title: What is NAPL -description: The prompt is the source of truth. A coding agent generates per-target code, attributed sentence by sentence and locked read-only. +description: The prompt and the code are both source. napl verifies the connection, records every change, and answers for every line. --- -NAPL asks one question: **what if the prompt _was_ the source code?** +NAPL asks one question: **what if the prompt _was_ source code?** -You don't write code. You write what you mean, in English, in a `.napl` file: +You write what you mean, in English, in a `.napl` file: The example above is live. The prompt on the left is editable, with the CLI's own -validator squiggling errors as you type; the code on the right is what -`napl gen typescript` actually produced from it. Try deleting the `module:` line and -watch the validator object before any CLI ever runs. +validator squiggling errors as you type; the code on the right is the code that landed +beside it through `napl commit`. Try deleting the `module:` line and watch the validator +object before any CLI ever runs. ## What the toolchain does that a chat window doesn't Anyone can ask a model for code. The problem is everything _after_: the chat scrolls -away, the code stays, and nobody remembers why any line exists. NAPL inverts that. When -you run `napl gen `, a coding agent: +away, the code stays, and nobody remembers why any line exists. napl inverts that. Both +sides are the truth: the prompt is the contract, the code honors it, and the toolchain +proves and records the connection instead of trusting anyone's memory. -- **writes a real project** and iterates until the tests you declared pass: the tests - are data in the prompt's frontmatter, not an afterthought; -- **maps every sentence to the exact lines that implement it.** Hover a sentence in the - example above to see them. If the agent can't prove that mapping, the build _fails_: - attribution is a hard gate, not a nicety; -- **locks the result read-only.** Edit the generated code by hand and `napl status` - reports drift and exits non-zero, so your CI catches it. +- **One guarded door.** Code enters through `napl add` and `napl commit`, a transaction + that installs the files, records the transition, and refuses with a reason and the + exact next step when something is wrong. Nothing lands half-way. +- **Every sentence maps to lines.** Attribution connects prompt prose to the exact code + that implements it. Hover a sentence in the example above to see it. `napl blame` + answers the reverse direction: which transition wrote this line, and from which words. +- **Drift is visible, never silent.** Edit either side by hand and `napl status` reports + it and `napl diff` renders it. Drift is a legal state the diagnostics mark, and CI can + decide what a red diff means for your team. +- **Tests travel with the prompt.** Pinned cases are data in the prompt itself, realized + as real tests in the code beside it. -The English stays the source of truth. The code becomes a build artifact: inspectable, -tested, traceable, never hand-edited. To change behavior, you change the prompt. +The English stays the source of truth, and so does the code: two sides of one record. +To change behavior, change either side, and the toolchain tells you what moved. -## The compile is a dialogue +## Everything it knows, it answers -Gen doesn't only emit code. The model writes back into the margin of your prompt (a -`.mapl` file per module) flagging where your words were unclear (**ambiguity**, a red -squiggle), what it decided that you left open (**assumption**), and why the code came -out as it did (**note**). A prompt change that produces no code change must say why, or -the gen fails: silence is forbidden. Ambiguity becomes a first-class compile diagnostic, -the way a type error is. +The store behind the door is content-addressed and queryable. `napl q` takes jq-style +expressions over everything recorded: modules, transitions, attribution, history. +`napl log` reads the story back. `napl serve` exposes all of it over local HTTP for +editors and tools. This site's reference pages are rendered from that same store: the +[compiler documents itself](/docs/reference/napl). @@ -62,5 +66,5 @@ the way a type error is. Status: wild experiment, a bet that English can be a real programming language if the toolchain enforces the discipline compilers used to. The voice here is honest and experimental; expect sharp edges, and see [Running it for real](/docs/running) for - what the coding agent actually needs on your machine. + the day-to-day workflow. diff --git a/apps/site/content/docs/writing-prompts.mdx b/apps/site/content/docs/writing-prompts.mdx index 4a6f4368..e10ce2f2 100644 --- a/apps/site/content/docs/writing-prompts.mdx +++ b/apps/site/content/docs/writing-prompts.mdx @@ -1,28 +1,31 @@ --- title: Writing prompts that hold -description: The prompt is source code, so it has to survive regeneration. Spec vs prose, tests as a gate, the machine's replies, and which sentences are load-bearing. +description: The prompt is source code, so it has to survive being read cold. Spec vs prose, tests as data, the toolchain's feedback, and which sentences are load-bearing. --- A `.napl` prompt is not a chat message you send once. It is source code: it will be -regenerated, diffed, and re-attributed every time it changes, and a fresh coding agent -will read it cold each time with no memory of the last run. A prompt _holds_ when that -agent produces the same behavior every time and the toolchain can prove the connection. -This page is about writing prompts that hold: what to put where, what the toolchain -checks, and which of your sentences are actually load-bearing. +read cold, again and again, by people and coding agents with no memory of the last +time, and the toolchain records and verifies what was built against it. A prompt +_holds_ when a cold reading produces the same behavior every time and the toolchain +can prove the connection. This page is about writing prompts that hold: what to put +where, what the toolchain checks, and which of your sentences are actually +load-bearing. ## The body is a contract, not a description -The prose below the frontmatter is a specification handed to the agent. It should say -what the code must _do_ (the public API, the behavior, the edge cases) and leave every -implementation choice to the agent. Name the functions and the shapes; describe the -rules; enumerate the corners. Don't describe an algorithm you don't care about, and don't -paste code: that's an implementation choice masquerading as a spec. +The prose below the frontmatter is a specification handed to whoever writes the code, +human or agent. It should say what the code must _do_ (the public API, the behavior, +the edge cases) and leave every implementation choice open. Name the functions and the +shapes; describe the rules; enumerate the corners. Don't describe an algorithm you +don't care about, and don't paste code: that's an implementation choice masquerading as +a spec. -Every sentence here is a claim the agent must satisfy and the toolchain must be able to -attribute to real lines of code. "Never silently renewed" is not decoration: it forbids -a whole class of implementation. Vague sentences produce vague code and get flagged (see -[the machine's replies](#the-machine-answers-back)); precise ones become the spec. +Every sentence here is a claim the code must satisfy and the toolchain must be able to +attribute to real lines. "Never silently renewed" is not decoration: it forbids a whole +class of implementation. Attribution makes this literal: when the code lands, each +claim in the prompt maps to the lines that honor it, and `napl blame` reads the map +back from either side. -## Tests are data, and they gate +## Tests are data, and they travel -Behavior that can be pinned as an input/output pair belongs in `tests:`, not in the prose. -Each entry is a `name`, a `given`, and an `expect`: plain data, not code. The agent -iterates until every case passes, or `gen` fails loudly after three attempts. This is what -separates NAPL from "AI wrote some code": the tests are part of the source, they travel -with the prompt, and no generation is accepted until they're green. +Behavior that can be pinned as an input/output pair belongs in a test entry, not in the +prose. Each is a `name`, a `given`, and an `expect`: plain data, not code. They live in +the frontmatter under `tests:`, or as a `::: test` fence beside the exact sentence they +pin, and they are realized as real tests in the code that lands. The tests are part of +the source; they travel with the prompt and version with it. Prose and tests divide the work cleanly: -- **Frontmatter tests** pin behavior expressible as data: a given input maps to an - expected output or error. +- **Test entries** pin behavior expressible as data: a given input maps to an expected + output or error. - **Prose** carries everything the data can't: invariants, ordering guarantees, what is _forbidden_, why an edge case exists. -If you find yourself writing "returns X for input Y" in the prose, move it to `tests:` -where the toolchain can enforce it mechanically. +If you find yourself writing "returns X for input Y" in the prose, move it to a test +entry where it is pinned mechanically. -## The machine answers back +## The toolchain answers back -`napl gen` writes a reply in the margin of your prompt: a `.mapl` file, regenerated on -every compile. It is how the toolchain tells you your prompt was ambiguous, what it had to -assume, and why the code is shaped the way it is: +napl's feedback is diagnostics, not conversation, and each surface tells you a +different thing about your prompt: -- **ambiguity**: your words were unclear. Surfaces as a red squiggle on the exact words, - like a syntax error, usually with a suggested rewording. -- **assumption**: a decision your prose left open. A warning you can either accept or - close by making the choice explicit. -- **no-op**: the prompt changed but the code didn't. The entry must say why, or the gen - fails. A changed prompt that quietly reports "clean" is a bug the toolchain refuses to - ship. -- **note**: hover-visible reasoning about why the code came out this way. +- **`napl check`** answers before anything lands: what would stop this prompt from + being added. A parse error names the line; an unknown directive is a compile error, + never silently treated as prose. +- **The guarded door refuses with a reason.** When `napl commit` won't install, the + payload names what is wrong and the exact next step. A refusal is the toolchain + telling you the prompt and the code do not yet agree. +- **`napl status` and `napl diff`** mark drift: either side edited away from the + record. Drift is legal and visible, never an error and never silent. +- **Attribution is the deep feedback.** If a sentence of yours claims no lines, or a + line answers to no sentence, the map says so. A prompt that is all mood and no claims + produces an attribution map that embarrasses it. -```mapl title="greeting.mapl" -- promptLines: [12, 14] - kind: assumption - message: Return type left unspecified - reasoning: > - The prose does not name the return type. Chose a plain string. -``` - -Reading these back is how you tighten a prompt: an `assumption` you didn't intend is a -sentence you need to add. The full kind/severity table is in the -[file formats reference](/docs/file-formats#the-mapl-reply). +Reading these back is how you tighten a prompt: a refusal you didn't expect is a +sentence you need to add or sharpen. ## Deletable sentences and load-bearing sentences -The hardest part of prompt-writing is knowing which sentences carry weight. A sentence is -**deletable** if the toolchain already enforces its content some other way. It is -**load-bearing** if deleting it changes what the agent produces. The two look identical on -the page; the difference only shows up on regeneration. +The hardest part of prompt-writing is knowing which sentences carry weight. A sentence +is **deletable** if the toolchain already enforces its content some other way. It is +**load-bearing** if deleting it changes what a cold reader produces. The two look +identical on the page; the difference only shows up on the next cold reading. The clearest case comes from dependencies. When one module builds on another, its -frontmatter declares `deps:`, and the gen loop surfaces that to the agent as a -`Declared dependencies: …` line automatically. So a prompt sentence that merely _restates_ -the dependency: +frontmatter declares `deps:`, and the build graph carries the _where_ automatically. So +a prompt sentence that merely _restates_ the dependency: -> Add a path dependency on `../schemas_line_range` in your `Cargo.toml`; it is a workspace -> sibling, not a crates.io dependency. +> Add a path dependency on `../schemas_line_range` in your `Cargo.toml`; it is a +> workspace sibling, not a crates.io dependency. -That sentence is **redundant**. The `deps:` frontmatter already carries the _where_. It's deletable. +That sentence is **redundant**. The `deps:` frontmatter already carries the wiring. +It's deletable. -But the sentence that tells the agent _how to relate_ to that dependency: +But the sentence that tells the reader _how to relate_ to that dependency: > Use its public API. Do not reimplement its types or logic. -That sentence is **spec**. It is the only thing standing between the agent and a local -reimplementation. Delete it and a fresh agent, reading the prompt cold, will happily write -its own copy of the sibling's types. This one is load-bearing. - +That sentence is **spec**. It is the only thing standing between a fresh reader and a +local reimplementation. Delete it and an agent, reading the prompt cold, will happily +write its own copy of the sibling's types. This one is load-bearing. ### The war story -This distinction was not theoretical. While the toolchain was generating its own source, a -pass over the prompts trimmed the redundant dependency prose, correctly in most cases. But on the -`incremental` module the trim was too aggressive and deleted the whole imperative, -_"Add a path dependency on each in your `Cargo.toml`… use their public API, and do not -reimplement their logic or depend on any hand-written crate,"_ leaving only two bullets -that _described_ the sibling types. - -On the next regeneration (journal gen #93), the agent did exactly what an unconstrained -prompt invites: it **reimplemented** `LineRange` and `AttributionEntry` locally and emptied -its `Cargo.toml` dependencies. The code compiled. The tests passed. Nothing was red, and -the module had silently stopped composing on the rest of the toolchain. What caught it was -the expected-no-op discipline: a prose trim should regenerate as a no-op, and this regen -produced patches, so the run halted and the diff got read. The fix was to revert the -regen and restore the imperative, in compact form, to the nine prompts that had lost it. - -Here is the module as it stands, imperative intact: the prose that makes it hold is the +This distinction was not theoretical. While the toolchain was generating its own +source, a pass over the prompts trimmed the redundant dependency prose, correctly in +most cases. But on the `incremental` module the trim was too aggressive and deleted the +whole imperative, _"Add a path dependency on each in your `Cargo.toml`… use their +public API, and do not reimplement their logic or depend on any hand-written crate,"_ +leaving only two bullets that _described_ the sibling types. + +On the next regeneration the agent did exactly what an unconstrained prompt invites: it +**reimplemented** `LineRange` and `AttributionEntry` locally and emptied its +`Cargo.toml` dependencies. The code compiled. The tests passed. Nothing was red, and +the module had silently stopped composing on the rest of the toolchain. What caught it +was the expected-no-op discipline: a prose trim should land as a no-op, and this one +produced patches, so the run halted and the diff got read. The fix was to revert and +restore the imperative, in compact form, to the nine prompts that had lost it. + +Here is the module as it stood, imperative intact: the prose that makes it hold is the "do not reimplement its types or logic" line: The reusable lesson: **a prompt that only _describes_ a dependency invites -reimplementation; a prompt must _instruct_ the agent to depend on it.** When you trim a -prompt, ask of every sentence: does the toolchain enforce this already, or is this the -only place it's said? Keep the second kind. +reimplementation; a prompt must _instruct_ the reader to depend on it.** When you trim +a prompt, ask of every sentence: does the toolchain enforce this already, or is this +the only place it's said? Keep the second kind. -Next: [Running it for real](/docs/running): what the toolchain needs on your machine, -and what it enforces once a prompt's output is locked. +Next: [Running it for real](/docs/running): the edit loop, usage capture, and what CI +checks once a prompt's output is recorded.