Maintainer order (2026-07-27)
- Generated source must be exposed OUTSIDE the
.napl/ state directory, directly
next to the prompt that generated it. Where the prompt-to-output relationship is
one-to-many or many-to-many, symlinks may express the relationship.
- Generated output must preserve the directory structure of the
.napl sources.
Standing principle for this feature, verbatim from the maintainer: do this right and
do not hide anything ugly. The generated code is a first-class product of the
language, not a build byproduct, and the repo layout must say so.
Why (the usability failure today)
Today src/store/store_hash.napl generates src/.napl/src/rust/store_hash/src/lib.rs.
Three problems:
- The output is buried in a dot-directory two levels deep; a reader browsing the
sources cannot see the code a prompt produced without knowing toolchain internals.
- The generated tree is FLAT (
rust/<module>/), discarding the authored hierarchy
(store/, cli/, core/, ...). The authored structure carries meaning; the
output throws it away.
- The mapping prompt->output lives only in
map.json, i.e. in metadata instead of
in the filesystem where humans and tools look first.
Requirements
- R1: For every module, its generated output is reachable at a stable path adjacent
to its authoring .napl file, visible in a normal file tree (no dot-prefix).
- R2: The generated tree mirrors the authored tree:
src/store/store_hash.napl ->
a generated unit under src/store/ (exact naming below), recursively for any
authored nesting depth.
- R3: Multi-target modules (one prompt -> rust and ts outputs) and any future
many-to-many prompt/output relationships must be expressible; symlinks are the
sanctioned mechanism where a single physical location must appear in more than
one place.
- R4: Nothing about the store changes: history, blame, and drift stay
content-addressed in .napl/store/. This feature is about where LIVE outputs
sit, not where history sits.
- R5: No hidden mirrors. If a path exists in two places, one is a symlink and
ls -l says so. No copies, no generated-then-synced trees.
Candidate designs (to be settled in the spec)
A. Physical adjacency (preferred starting point)
Generated code physically lives next to its prompt:
src/
store/
store_hash.napl
store_hash.rust/ <- the generated crate (Cargo.toml, src/lib.rs), 0444
cli/
blame_render.napl
blame_render.rust/
- The cargo workspace enumerates members across the authored tree (workspace
member globs or a generated members list in the workspace manifest).
<module>.<target>/ naming makes target multiplicity trivial (.rust/, .ts/).
- Symlinks only appear for genuine m:n cases (R3), not as the primary mechanism.
- Ugliness surfaced honestly: generated dirs sit in your face in the tree. That is
the point; editors can fold them, and their read-only bit marks them.
B. Symlink views over the current physical layout
Keep physical outputs under .napl/src/, place src/store/store_hash.rust -> ../.napl/src/rust/store_hash symlinks next to prompts.
- Cheaper migration, but violates the spirit of "not hidden": the real files stay
buried, and the visible tree is a facade of links. Also symlink-in-git has
platform caveats (Windows checkout).
- Retained as a fallback if design A hits a hard blocker (e.g. cargo workspace
constraints that cannot express the mirrored layout).
Blast radius (honest list; every consumer of the flat layout)
- paths_core (all generated-path derivation), discovery, map schema/map.json
- cmd_gen_io (write paths, scope gate = "module writes only its own dir"),
statusclass_io (observed generated tree), driftdetect_*, healing_io (moved-file
heal must understand the mirrored layout - it may get SIMPLER, since authored
moves and generated moves become the same move)
- store tree paths for live-tree derivation (store objects record relative paths;
decide whether stored paths are authored-relative, which is the natural choice
post-migration)
- migrate/one-shot relayout of the existing tree + git mv-equivalent handling
- cargo workspace manifest, CI --locked builds, conformance runner paths,
equivalence suite paths, LSP file resolution, VS Code extension watcher globs
- prek/lint ignores, tsdown/vitest globs on the ts side
Sequencing
Strictly AFTER: C4 store activation, morning retirement pass, and the
metadata-fold slice (ir/ + mapl/ + attribution/ + map.json into the store).
Rationale: every artifact retired before this lands is one fewer consumer of paths
to relayout; the store's authored-relative path decision (R4/blast list) is the
natural foundation. This is the "make the language usable" slice that follows the
"make the language sound" slices.
Open questions for the spec
- Naming:
<module>.rust/ vs <module>.gen/rust/ vs bare <module>/ when
single-target. Leaning <module>.<target>/ for greppability and multiplicity.
- Workspace mechanics: cargo member globs across arbitrary depth vs a
toolchain-maintained members list in the workspace Cargo.toml (generated,
reviewed, committed).
- Read-only enforcement stays 0444 per file; does the dir itself get marked?
- Does the ts target mirror identically under the same rule at the same time, or
in a follow-up?
- m:n semantics: what authored construct produces many-to-many today or
near-term? (If none concrete, R3 stays a design constraint, not built
machinery - no speculative symlink framework.)
The full design spec (Fable-authored, per campaign discipline) lands in
docs/specs/ once C4 + retirement close and the store path decisions above are
observable facts rather than predictions. This issue is the tracking anchor.
Maintainer order (2026-07-27)
.napl/state directory, directlynext to the prompt that generated it. Where the prompt-to-output relationship is
one-to-many or many-to-many, symlinks may express the relationship.
.naplsources.Standing principle for this feature, verbatim from the maintainer: do this right and
do not hide anything ugly. The generated code is a first-class product of the
language, not a build byproduct, and the repo layout must say so.
Why (the usability failure today)
Today
src/store/store_hash.naplgeneratessrc/.napl/src/rust/store_hash/src/lib.rs.Three problems:
sources cannot see the code a prompt produced without knowing toolchain internals.
rust/<module>/), discarding the authored hierarchy(
store/,cli/,core/, ...). The authored structure carries meaning; theoutput throws it away.
map.json, i.e. in metadata instead ofin the filesystem where humans and tools look first.
Requirements
to its authoring
.naplfile, visible in a normal file tree (no dot-prefix).src/store/store_hash.napl->a generated unit under
src/store/(exact naming below), recursively for anyauthored nesting depth.
many-to-many prompt/output relationships must be expressible; symlinks are the
sanctioned mechanism where a single physical location must appear in more than
one place.
content-addressed in
.napl/store/. This feature is about where LIVE outputssit, not where history sits.
ls -lsays so. No copies, no generated-then-synced trees.Candidate designs (to be settled in the spec)
A. Physical adjacency (preferred starting point)
Generated code physically lives next to its prompt:
member globs or a generated members list in the workspace manifest).
<module>.<target>/naming makes target multiplicity trivial (.rust/,.ts/).the point; editors can fold them, and their read-only bit marks them.
B. Symlink views over the current physical layout
Keep physical outputs under
.napl/src/, placesrc/store/store_hash.rust -> ../.napl/src/rust/store_hashsymlinks next to prompts.buried, and the visible tree is a facade of links. Also symlink-in-git has
platform caveats (Windows checkout).
constraints that cannot express the mirrored layout).
Blast radius (honest list; every consumer of the flat layout)
statusclass_io (observed generated tree), driftdetect_*, healing_io (moved-file
heal must understand the mirrored layout - it may get SIMPLER, since authored
moves and generated moves become the same move)
decide whether stored paths are authored-relative, which is the natural choice
post-migration)
equivalence suite paths, LSP file resolution, VS Code extension watcher globs
Sequencing
Strictly AFTER: C4 store activation, morning retirement pass, and the
metadata-fold slice (ir/ + mapl/ + attribution/ + map.json into the store).
Rationale: every artifact retired before this lands is one fewer consumer of paths
to relayout; the store's authored-relative path decision (R4/blast list) is the
natural foundation. This is the "make the language usable" slice that follows the
"make the language sound" slices.
Open questions for the spec
<module>.rust/vs<module>.gen/rust/vs bare<module>/whensingle-target. Leaning
<module>.<target>/for greppability and multiplicity.toolchain-maintained members list in the workspace Cargo.toml (generated,
reviewed, committed).
in a follow-up?
near-term? (If none concrete, R3 stays a design constraint, not built
machinery - no speculative symlink framework.)
The full design spec (Fable-authored, per campaign discipline) lands in
docs/specs/ once C4 + retirement close and the store path decisions above are
observable facts rather than predictions. This issue is the tracking anchor.