Skip to content

conciv-dev/napl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

84 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

NAPL β€” NAPL Ain't a Programming Language

What if the prompt was the source code?

You don't write code. You write what you mean, in English, in a .napl file:

---
module: greeting
targets: [typescript]
tests:
  - name: greets by name
    given: { name: World }
    expect: { message: "Hello, World!" }
---
# Greeting

Expose a `greet` function that takes a person's name and returns a friendly
greeting message.

- The greeting has the form `Hello, <name>!`.
- Leading and trailing whitespace in the name is trimmed before use.
- An empty or whitespace-only name is rejected with an error.

napl gen typescript hands that file to a full coding agent. It scaffolds the project, writes the implementation and its tests, runs them until green β€” and then the toolchain does the part nobody else does: it proves the connection. Every generated file is attributed to the prompt that caused it. Every sentence is mapped to the exact lines of code that implement it. Then the code is locked read-only. The English stays the source of truth; the code becomes a build artifact β€” inspectable, tested, and traceable, but not hand-edited.

The name has a nap in it on purpose: write English, take a nap, wake to tested code.

The part that feels like magic

Open a .napl file in VS Code (extension included):

  • Hover a sentence β€” see the exact generated lines that implement it. Hover "whitespace is trimmed" β†’ const trimmed = name.trim();
  • Cmd+click a sentence β€” jump into the hidden generated source, at the line.
  • Inside generated code, every function carries a breadcrumb back to English: β‡  greeting.napl:19 β€” trims name whitespace. Click it. You're back at the sentence.
  • Find all references, in reverse β€” which prompts caused this line of code?
  • Edit generated code by hand? napl gen refuses to run and hands you a guided fix; napl status flags DRIFT (exit 1, CI fails). All change flows through prompts β€” see Drift is a compile error.

A todo app whose entire human-written source is one English file lives in examples/todo-app/ β€” Vite + React + TypeScript, component tests, generated by the toolchain, attributed line by line.

Prompt blame β€” mechanical line history, like git blame

Under the semantic attribution sits a mechanical, append-only record. Every successful napl gen writes one line to .napl/journal.jsonl β€” the gen number, the timestamp, the prompt diff that drove it, and a per-file patch.

  • Hover any generated line and the first thing you see is mechanical: caused by gen #12 Β· 2026-07-23 Β· prompt edit: <what changed> β€” which gen produced this line, when, and which prompt edit caused it. The semantic "implements this sentence" content stays right below it: mechanical like git blame, semantic on top.
  • napl blame <file> β€” for each line, the gen that last touched it and its module, reconstructed by replaying the journal patches newest-to-oldest.
  • napl blame <file> --line N --verbose β€” adds the prompt edit that caused that line (the "why").
  • napl blame --gen N β€” a single gen's summary: module, prompt diff, files touched.

The machine layer β€” the compile is a dialogue

napl gen doesn't only emit code. Alongside it, the model writes back β€” in the margin of your prompt. Every module gets a machine counterpart at .napl/mapl/<module>.mapl recording where your English was unclear, what the model assumed, and why the code came out the way it did. Open the .napl file and the answers surface inline:

  • ambiguity β†’ a red squiggle on the exact words, like a syntax error. Hover to see what was unclear and a suggested rewording.
  • assumption β†’ a warning: a decision your prompt left open.
  • note β†’ hover-visible reasoning about why the code is shaped this way.
  • no-op β†’ a warning when a prompt change produced no code change β€” and the model must say why. A changed prompt with an empty diff and no explanation fails the gen: silently reporting "clean" without implementing your change is forbidden.

You write English; the machine answers in the margin; ambiguity becomes a first-class compile diagnostic.

Two spellings, one dialogue

The extensions name the two voices. Both spellings in each row are byte-identical and fully equivalent to every tool β€” discovery, LSP, grammar, the VS Code extension:

Side Canonical Emoji aliases The voice
The human speaks .napl .πŸ§‘ .πŸ§“ .πŸ‘€ .πŸ‘¨ .πŸ‘© .πŸ§’ what you write
The machine answers .mapl .πŸ€– its margin notes

So greeting.πŸ§‘ is what you write and greeting.πŸ€– is the machine's reply β€” the ambiguities, assumptions, and reasoning from the latest compile. The living example in this repo, examples/greeting.πŸ§‘, is spelled with the person emoji; examples/todo-app/ stays canonical .napl β€” both idioms are first-class.

The human aliases are a designed, curated single-codepoint list (.πŸ§‘ .πŸ§“ .πŸ‘€ .πŸ‘¨ .πŸ‘© .πŸ§’) β€” single-codepoint is deliberate, and ZWJ sequences (like .πŸ‘¨β€πŸ’») are rejected so filenames stay filesystem-safe. Override the list in .napl/lock.json with a promptAliases array (each entry starts with ., one or two code points after the dot, no ZWJ):

{
  "model": "claude-sonnet-5",
  "promptAliases": [".πŸ§‘", ".πŸ§“"]
}

The mirror rule. A prompt may use any alias; the module name and all derived state (the .napl/ layout, map.json paths, the journal) simply record the actual filename. napl gen writes the machine file as canonical .mapl for a canonical .napl prompt, and mirrors the choice β€” writing .πŸ€– β€” when the prompt uses an emoji alias. Reading always accepts both spellings.

Filesystem note. Emoji filenames require a UTF-8 filesystem, which every modern OS provides (APFS, ext4, NTFS, Btrfs). The curated aliases are single Unicode code points on purpose β€” no zero-width-joiner (ZWJ) multi-person sequences β€” so the names round-trip cleanly across tools and shells.

Drift is a compile error β€” with a guided fix

Generated code is locked read-only (0444) once its tests pass. Edit a locked file anyway and the toolchain treats it as drift: napl gen refuses to run and napl status exits non-zero. But a compile error you can't act on is just a wall, so drift ships with the fix attached. Before it runs any agent, napl gen <target> checks every module of that target for drift; if the files in scope carry un-prompted edits it hard-blocks with a guided report per drifted module:

  • the module and the exact file(s) that drifted,
  • a unified diff of the recorded baseline β†’ your current content (reconstructed by replaying the prompt-blame journal; when the baseline predates the journal it falls back to showing the two hashes),
  • and the three ways out, verbatim:
    1. napl reconcile <module> β€” fold the edit back into your prompt (coming soon)
    2. napl gen <target> --module <module> --force β€” discard the edit, the prompt wins
    3. edit the prompt to describe the change, then napl gen <target>

--force is resolution #2: it unlocks, regenerates from the prompt, and re-locks β€” the prompt always wins.

Friction at compile time, not edit time

Locks stay 0444 on purpose. Editors offer a one-click "make writable", so poking at generated code to read or experiment with it stays frictionless β€” the friction is placed deliberately at compile and commit time, not edit time. Read it, run it, tinker with it; you just can't slip an un-prompted change past napl gen or a commit.

Humans edit freely, AIs can't

The guard rails are asymmetric by design:

  • Your app's own toolchain is never blocked. npm run dev, the test runner, the bundler β€” none of them care about drift. Only napl gen and the commit gate confront it, so day-to-day app development is untouched.
  • Humans get the one-click unlock (the 0444 bit) for a quick look; drift only bites at napl gen / commit.
  • AI coding agents get mechanically stopped. napl init writes an AGENTS.md + CLAUDE.md into each generated tree ("this is generated β€” edit the prompt, not this file"), a project .claude/settings.json that denies the file-editing tools on .napl/src/** (Edit(/.napl/src/**), which Claude Code applies to Edit/Write/MultiEdit alike), and every generated source file opens with a GENERATED BY NAPL from <prompt> β€” edit the prompt, not this file banner. An agent that reaches for generated code is refused before it types.
  • The commit gate. napl init installs a tiny POSIX pre-commit hook that runs napl status and blocks the commit on drift (WIP escape hatch: git commit --no-verify).

Why this isn't just "AI writes code"

Anyone can ask a model for code. The problem is what happens after: the chat scrolls away, the code stays, and six months later nobody knows why anything exists. NAPL inverts that:

  • Prompts are files, versioned and reviewed β€” not chat history.
  • Attribution is a hard gate β€” if the toolchain can't map the generated code back to your sentences, the build fails. Code without a traceable prompt is never accepted.
  • Tests gate every generation β€” test cases live in the prompt's frontmatter; the agent iterates until they pass or the build fails loudly.
  • Language-agnostic by construction β€” a target is a config object, not a compiler. TypeScript and React ship today; adding Swift or Go is declaring idioms and a test command, not months of work.
  • No API key β€” the compiler backend is your existing Claude Code login.

Status: wild experiment. This is a bet on a question β€” can English be a real programming language if the toolchain enforces the discipline compilers used to? β€” not a production tool. Expect sharp edges. Watch the repo if you want to see where it goes.

Layers

prompt files (*.napl)              ← what humans read, write, review
        β”‚  napl gen <target>   (agentic LLM session β€” writes src directly)
        β–Ό
target code (.napl/src/<target>/)  ← real code, locked read-only after tests pass
        β”‚  observed diff β†’ attribution
        β–Ό
map (.napl/map.json)               ← prompt ↔ file attribution, from reality
IR (.napl/ir/*.yaml)               ← derived contract layer (signatures + tests)
attribution (.napl/attribution/*)  ← derived prompt-line ↔ code-line mappings
  • Prompt files (*.napl) β€” markdown prose with YAML frontmatter. The frontmatter is machine-facing metadata; the body is the contract in prose.
  • Target code (.napl/src/<target>/) β€” per-target directory the agent owns. Made read-only (0444) after its tests pass, enforced by napl status as a CI drift gate. Build artifacts (node_modules, dist, lockfiles, *.tsbuildinfo) are excluded from attribution and never locked.
  • IR (.napl/ir/<module>.yaml) β€” derived after gen. Captures contracts, not implementation: module name, deps, exported types, function signatures with prose behavior contracts, and test cases. Powers module hover in the LSP.
  • Attribution (.napl/attribution/<module>.yaml) β€” derived after gen. Maps 1-based prompt-body line ranges to the exact source line ranges that implement them, with a short note each. Powers body-line hover / go-to-definition.

.napl/map.json records the prompt ↔ file attribution (many-to-many) and every locked content hash used for staleness and drift detection. .napl/lock.json pins the model id and the LLM backend. .napl/ is committed by design.

Install

The toolchain is a single native binary, napl. Pick whichever channel suits you.

npm (installs a matching prebuilt binary via optionalDependencies, no build step):

npm i -g napl-lang     # global `napl` on your PATH
napl --help

npx napl-lang init     # or run once, without installing

curl (downloads the release binary into ~/.local/bin, or $NAPL_INSTALL):

curl -fsSL https://raw.githubusercontent.com/conciv-dev/napl/main/install.sh | sh

cargo (build from source):

cargo install --git https://github.com/conciv-dev/napl napl-cli

Prebuilt binaries cover macOS (arm64, x64), Linux glibc (x64, arm64), and Windows x64. On any other platform, install from source with cargo.

Quickstart

npx napl-lang init         # create .napl/, lock.json (backend: claude-cli), and examples/greeting.napl
# ...write or edit *.napl prompt files...
npx napl-lang gen react    # coding agent writes .napl/src/react/, runs its tests, locks + derives IR
npx napl-lang status       # clean / prompt-stale / DRIFT

Backends

Model calls read the model id and the backend from .napl/lock.json. Two backends are supported; napl init selects claude-cli by default.

The coding agent always runs via the claude CLI. The backend below selects how the derived IR + attribution completions are made.

  • claude-cli (default) β€” shells out to the Claude Code CLI (claude), reusing your existing Claude Code login. No API key is needed. gen verifies the claude binary is on your PATH before doing any work and exits with a clear error if it is not installed.

  • anthropic-api β€” makes the derived-artifact completions via the Anthropic API directly (@anthropic-ai/sdk). Requires ANTHROPIC_API_KEY; if the key is missing, gen exits with a clear error before doing any work.

    export ANTHROPIC_API_KEY=sk-ant-...

To switch backends, edit backend in .napl/lock.json:

{
  "model": "claude-sonnet-5",
  "backend": "anthropic-api"
}

An existing lock.json without a backend field is treated as claude-cli.

Commands

Command What it does
napl init Create the .napl/ structure, lock.json (pins claude-sonnet-5, backend claude-cli), and an example prompt.
napl build Deprecated. Prints a notice β€” gen now works directly from prompts; there is no separate IR build step.
napl gen <target> For each prompt whose content hash changed for that target (or all, with --force), snapshot the target src tree, run a claude coding agent (cwd = .napl/src/<target>/) that may scaffold and edit many files, then diff the tree and attribute changed files to the prompt. Run the target's test command; on failure feed the output back to the agent and retry (up to 3 times) before failing loudly. On green, derive the IR (best-effort) and the span attribution (a hard gate, retried up to 3 times); only if attribution succeeds are the files locked 0444 and hashes recorded. If attribution fails, gen fails for that module (files left unlocked, target marked unattributed).
napl status Report per prompt: clean, prompt-stale (prompt changed since last gen), DRIFT (a locked attributed file was edited β€” forbidden), or unattributed (generated files exist but attribution derivation failed). Exits 1 on any drift or unattributed prompt, so it works as a CI gate.
napl test [target] Run the generated tests for a target (default typescript) inside .napl/src/<target>/ without regenerating.

Attribution is a hard gate

Deriving the span-level attribution (.napl/attribution/<module>.yaml) is a required step, not best-effort: after tests pass, gen asks the model to map prompt lines to code lines and retries up to three times with progressively stricter instructions (later attempts include the previous invalid output and the validation error). A result is rejected β€” and the attempt retried β€” if it fails schema validation, has no entries while source files were attributed, or references a file outside the attributed set. If all three attempts fail, gen fails for that module with a non-zero exit: the module's files are left unlocked, its promptHashAtGen is not recorded (so the next gen re-runs it), and the target entry is flagged unattributed: true in map.json so the tree is tracked, not orphaned. napl status then reports that prompt as unattributed (exit 1) until a later successful gen clears the marker. IR derivation (.napl/ir/<module>.yaml) stays best-effort β€” a failure there only logs a warning and gen still succeeds.

Prompt format reference

---
module: auth/session          # required β€” module name (may be a path)
deps: [auth/tokens]           # optional β€” dependency module names
targets: [typescript]         # optional β€” targets to generate for
tests:                        # optional β€” given/expect test cases as data
  - name: expired token rejected
    given: { token: expired }
    expect: { error: SESSION_EXPIRED }
---
Manage user sessions. Sessions expire after 30 minutes of inactivity.
Refreshing a session extends it. An expired token is rejected with
SESSION_EXPIRED, never silently renewed.

The body below the frontmatter is the prose contract passed to the model.

Target adapters

Each target is a config object under packages/core/src/targets/ declaring idiom guidance for the coding agent, the tools the agent may use, the test-run command, and the paths excluded from attribution (node_modules, dist, lockfiles, *.tsbuildinfo, …). Adding a language is a config-only change β€” register a new adapter in packages/core/src/targets/registry.ts. typescript (vitest) and react (Vite + React + TypeScript, vitest + @testing-library/react + jsdom) ship today.

Conformance

conformance/ is a language-agnostic golden-fixture suite that freezes the toolchain's observable behavior β€” exact stdout/stderr, exit codes, and the byte content of every state file (map.json, journal.jsonl, .mapl, attribution, IR) for 30 end-to-end scenarios covering init, gen (happy path, attribution gate, no-op rule, incremental, --force, --module, gen-lock contention), the full status matrix, blame, emoji aliases, and error paths.

Each scenario (conformance/scenarios/*.yaml) runs the real built CLI in a fresh temp dir. A stub claude (and npx) placed first on PATH (conformance/fake-claude/) makes the coding agent, the derived-artifact completions, and the test command deterministic and offline. Timestamps are pinned via a NAPL_FIXED_NOW env override read only at the CLI entry; absolute paths are normalized to {{CWD}}.

corepack pnpm turbo run conformance   # run the whole corpus against the current CLI

This corpus was the acceptance gate for the Rust rewrite, which has since landed: the shipping napl is a native Rust binary that executes these same fixtures byte-for-byte, and its own napl-core and napl-cli logic is itself generated from .napl prompts. The prompts are the source, the generated crates ship, and what stays hand-written is a small glue inventory (binary entry, error and clock seams, I/O shells, facades, the JSON-RPC server). See docs/architecture.md and docs/src-map.md.

Development

This is a pnpm + turborepo monorepo. Node >= 22 and corepack are required; the pnpm version is pinned via the root packageManager field.

corepack pnpm install     # install all workspace packages
corepack pnpm lint        # oxlint + the content checks
corepack pnpm format      # oxfmt --write .

The shipping napl toolchain is Rust, under rust/:

cd rust
cargo build                              # builds the napl binary (crates/napl-cli)
cargo test --workspace                   # napl-core, napl-cli, napl-lsp unit tests
cargo clippy --workspace --all-targets

The conformance corpus is the acceptance gate for toolchain behavior:

cd conformance && node --experimental-strip-types runner/cli.ts

The documentation site runs on its own vite server:

corepack pnpm --filter site dev          # local site
corepack pnpm --filter site build        # production build

Repo layout

src/            the toolchain's own `.napl` prompt sources, the source of truth (see below)
rust/crates     napl-core, napl-cli, napl-lsp: the shipping Rust toolchain, the `napl` binary and the language server
rust/napl-wasm  wasm-bindgen bindings over napl-core for the site playground
packages/napl-grammar   @napl-lang/grammar: TextMate grammars for .napl and .mapl
packages/napl-wasm      @napl-lang/wasm: workspace wrapper over the generated wasm pkg
packages/napl-editor    @napl-lang/editor: the CodeMirror playground and replay UI
packages/bundle-size    @napl-lang/bundle-size: the CI bundle-size gate
apps/site       the documentation site
apps/vscode     the VS Code extension (bundles the language server; unpublished)
npm/napl-lang   the published `napl` shim that resolves the platform binary
npm/binary-*    per-platform binary packages (@napl-lang/binary-<triple>)
examples/       greeting.πŸ§‘ + todo-app: real projects (emoji + canonical spellings), not workspace packages

The src/ directory

src/ is the toolchain's own source: a tree of .napl prompt files, not a conventional code directory. The prompts are the source of truth. The Rust implementation generated from them lives under src/.napl/ as a locked, committed artifact, and src/equivalence/ replays the behavioral test corpus against those generated crates.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages