Skip to content

Source-adjacent generated code + structure-preserving output layout #41

Description

@omridevk

Maintainer order (2026-07-27)

  1. 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.
  2. 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

  1. Naming: <module>.rust/ vs <module>.gen/rust/ vs bare <module>/ when
    single-target. Leaning <module>.<target>/ for greppability and multiplicity.
  2. Workspace mechanics: cargo member globs across arbitrary depth vs a
    toolchain-maintained members list in the workspace Cargo.toml (generated,
    reviewed, committed).
  3. Read-only enforcement stays 0444 per file; does the dir itself get marked?
  4. Does the ts target mirror identically under the same rule at the same time, or
    in a follow-up?
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:p2Planned product or architecture work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions