Invisible link to canonical for Microformats

ADR-034 Path Resolution Anchors (moduleDir, cwd, projectRoot, libRoot)


Status

Accepted

Context

Slug programs may be executed from a directory other than the directory containing the entry script. This creates an important ambiguity around relative paths:

  • some paths should resolve relative to the current module file
  • some paths should resolve relative to the entry program / project
  • some paths should resolve relative to the user’s current working directory
  • some paths should resolve relative to the root of the current library namespace

Without explicit anchors, file access and import behaviour become surprising and brittle. A program launched from /tmp against /my-project/app.slug must still be able to:

  • access assets beside the current module
  • access project-wide resources
  • respect user-facing CLI path expectations

Slug favours explicitness over hidden behaviour. This ADR establishes a small, clear set of path anchors so that path intent is always visible in code.

Decision

These path anchors are not builtins. They are provided by the standard library module slug.path, which exposes runtime-aware functions.

Example:

var path = import("slug.path")

path.cwd()
path.projectRoot()
path.moduleDir()
path.libRoot()
path.join(...)
path.abs(...)

The runtime supplies the underlying values based on execution and module context.

Slug introduces four distinct path anchors and a small path utility surface.

1. path.cwd()

path.cwd() returns the current working directory of the running process — the directory from which the user invoked the command.

Rules:

  • It is execution-scoped.
  • It does not change during program execution.
  • It is the correct base for user-facing CLI paths and shell-oriented workflows.

Example:

cd /tmp
slug run /my-project/app.slug

Then:

path.cwd() == "/tmp"

Typical use:

val args = argv()
val input = readFile(args[0])

2. path.moduleDir()

path.moduleDir() returns the directory containing the current module file.

Rules:

  • It is module-scoped.
  • Its value may differ between imported modules.
  • It is the correct base for module-local assets and relative imports.

Example:

// /my-project/lib/db/query.slug
path.moduleDir() == "/my-project/lib/db"

Typical use:

val sql = readFile(path.join(path.moduleDir(), "query.sql"))

3. path.projectRoot()

path.projectRoot() returns the directory of the entry module — the script passed to program execution.

Rules:

  • It is execution-scoped.
  • It is determined once at startup.
  • It does not change as modules are imported.
  • It is the correct base for project-wide resources and project-local module resolution.

Example:

cd /tmp
slug run /my-project/app.slug

Then:

path.projectRoot() == "/my-project"

Typical use:

val tpl = readFile(path.join(path.projectRoot(), "templates", "email.mustache"))

4. path.libRoot()

path.libRoot() returns the root of the library / module namespace from which the current module was loaded.

Rules:

  • It is module-scoped.
  • Different modules may have different library roots.
  • It is intended for library-relative resources and tooling-aware library layout.

Example:

// /my-project/lib/db/query.slug, loaded as "lib.db.query"
path.libRoot() == "/my-project"

This differs from path.moduleDir():

path.moduleDir() == "/my-project/lib/db"
path.libRoot()   == "/my-project"

5. Path utility functions (via slug.path)

Slug provides a small path utility surface:

path.join(...)

Joins path elements using the system-specific separator.

Example:

join(path.projectRoot(), "templates", "email.mustache")

`path.abs(…)

Returns a normalized absolute path.

Example:

abs("./tmp/out.txt")

6. Responsibility of each anchor

The intended usage model is:

  • path.moduleDir() → file-local assets and relative imports
  • path.cwd() → user interaction and CLI path semantics
  • path.projectRoot() → project structure and entry-rooted resources
  • path.libRoot() → library-relative resources and namespace-rooted tooling

7. Worked example

Given:

/my-project/
├── app.slug
├── templates/
│   └── email.mustache
└── lib/
    └── db/
        ├── query.slug
        └── query.sql

Run as:

cd /tmp
slug run /my-project/app.slug

Then in app.slug:

path.cwd()         == "/tmp"
path.projectRoot() == "/my-project"
path.moduleDir()   == "/my-project"

And in lib/db/query.slug:

path.cwd()         == "/tmp"
path.projectRoot() == "/my-project"
path.moduleDir()   == "/my-project/lib/db"
path.libRoot()     == "/my-project"

Consequences

Positive

  • Makes path intent explicit and predictable.
  • Prevents accidental coupling between shell location and module resolution.
  • Supports both module-local and project-wide resource access cleanly.
  • Preserves user-friendly CLI behaviour for relative input/output paths.
  • Scales to tooling, manifests, and future package/library systems.
  • Aligns with Slug’s preference for explicitness over hidden behaviour.

Negative

  • Introduces multiple path concepts that users must learn.
  • Requires runtime bookkeeping for both execution-scoped and module-scoped anchors.
  • path.moduleDir() and path.libRoot() may look similar in simple projects, which can initially confuse users.

Neutral

  • In trivial executions, path.cwd(), path.projectRoot(), and path.moduleDir() may all be the same path.
  • Future project-root discovery via marker files (for example slug.toml or .git) may refine how path.projectRoot() is determined without changing the anchor model itself.
  • Additional path helpers may be added later, but this ADR keeps the initial surface intentionally small.