Cuts
.d6 is d6's default and preferred source extension, including for new
cuts. Backward compatibility with .di source files and published cuts is
retained. The loader prefers lib/<name>.d6, then
<name>.d6, then the corresponding legacy .di paths. Facet accepts and
audits both source suffixes so immutable releases remain installable.
For the publishable package contract and registry design history, see cut-contract.md and package-registry-plan.md. This page documents the currently implemented behavior.
facet check <cut-directory> [--files] validates the first publishable layout,
manifest, and runtime file inventory without installing dependencies or
creating an archive. --files prints the sorted candidate artifact file list.
facet pack <cut-directory> <output.tar> packages that exact list as a
deterministic ustar archive and prints its SHA-256 digest. Neither command
publishes to a registry.
facet verify <archive.tar> [--sha256 <digest>] checks a packed artifact
without extracting it. The expected digest is required when verifying bytes
against a trusted lockfile or registry record.
See cut-contract.md for its current scope.
facet publish <cut-directory> --registry <https-url> --token <token> runs the
same local checks, then sends the verified archive to the registry's publish
endpoint. The token is used only for that request and is not stored in the
project.
For a local checkout without a registry service,
tools/install_local_cuts.sh <project-directory> installs all bundled cuts as
verified artifacts from packages/ into that project's cuts/ directory.
This development bootstrap does not
resolve registry versions or write a registry lockfile.
An existing Cutfile.lock may pin a registry artifact with source, registry,
version, sha256, and size. facet install uses curl to fetch only that
digest-addressed HTTPS archive, validates it with the same checks as
facet verify, and stages every cut before replacing installed copies. It does
not query an index or resolve registry versions yet.
This document covers d6's package resolution — packages are called
cuts, resolved from a cuts/ directory via require_cut, a require
form separate from and never competing with ordinary require — plus the
optional manifest a cut can declare its identity with, and facet, the
standalone tool that fetches and installs cuts. facet is a separate
program (its own binary, build/facet, built from tools/facet*.c), not
part of the d6 runtime or its release — the same relationship
RubyGems' gem has to ruby.
require_cut "name"
require_cut "greeter"
greet("world")
require_cut is a require form of its own, distinct from plain require
— parsed the same textual way (src/loader.c's expand()), but resolved
completely differently: require_cut "name" always resolves to
cuts/name/lib/name.d6, anchored to the process's own current working
directory (not the requiring file's own directory — cuts are
project-level, so they're anchored the same way the top-level entry file
itself is). If cuts/name/lib/name.d6 doesn't exist, the loader falls
back to the flat cuts/name/name.d6 location, then the corresponding legacy
.di paths. If none
resolves, require_cut fails with a clear "cannot require_cut" error
rather than silently doing nothing.
name must be a bare cut name — a require_cut "sub/greeter" containing
a / is rejected outright, before any filesystem lookup, with a clear
error. There is no path-like form of require_cut; a cut is always
addressed purely by its own name.
require and require_cut never compete. Plain require "name"
means exactly what it always has — resolve name relative to the
requiring file (or as an absolute path) — full stop, with no fallback of
any kind. It never looks inside cuts/, even for a bare name with no
/. A cuts/greeter/ directory sitting next to a file that does
require "greeter" changes nothing about what that require resolves
to; the two are simply unrelated. This is deliberate: earlier, a bare
require "name" first tried a relative file and silently fell back to a
package lookup if that failed — that ambiguity (was this a local file or
a dependency?) is exactly what require_cut as a separate, explicit
form removes. Every existing require "name" in every existing program
keeps meaning exactly what it always meant.
Layout convention
cuts/
greeter/
Cutfile # manifest (optional)
lib/
greeter.d6 # public entry point, same name as the cut
helpers.d6 # implementation components
The public entry point is resolved from lib/<name>.d6. A cut's own code
can require sibling components with ordinary relative paths from that
lib/ directory. The loader retains the flat <name>.d6 location (no
lib/ subdirectory) as a compatibility fallback for cuts that have not
migrated yet.
Manifests
# cuts/greeter/Cutfile
{"name": "greeter", "version": "0.1.0"}
A cut may optionally include cuts/<name>/Cutfile — a fixed filename,
not parameterized by the cut's own name, since the same file also has to
work unmodified as the manifest sitting at a dependency's own repository
root before facet ever installs it anywhere (see "facet" below). If
present, it must contain a data-only Hash with a String name key matching
the cut's own directory exactly; a cut whose
manifest disagrees with its own directory name fails the whole
require_cut with a clear error rather than silently loading anyway
(this is the manifest's actual payoff today: catching a cut that was
copied or renamed incorrectly during a manual install). An optional
version key, if present, must be a String — validated for type only;
nothing reads its value (see "What's deliberately out of scope so far"
for why).
A manifest must contain a single data-only Hash literal. The loader and
facet parse it without compiling or running d6 code. Expressions,
calls, interpolation, duplicate keys, and trailing code are rejected.
require and require_cut are not supported inside metadata.
If no Cutfile exists at all, none of this applies — the cut resolves
exactly as it would with no manifest support at all.
A manifest can be pretty-printed across multiple lines. Whitespace is allowed
around keys, colons, values, commas, and braces. A trailing comma is accepted
for compatibility with existing Cutfile.lock files.
Dependencies
# Cutfile, at your project's own root
{"name": "myapp", "version": "0.1.0", "dependencies": {"greeter": {"git": "https://example.com/user/greeter", "tag": "v1.0.0"}}}
An optional dependencies key, read only by facet (see below) — the
runtime's own manifest validation ignores it entirely, so adding it to
an existing manifest changes nothing about how require_cut behaves. It's
a Hash from cut name to a spec Hash with a required String
git key and exactly one of tag, branch, commit, or version
(also String), or a registry HTTPS URL with exactly one version constraint.
Git and registry fields cannot be mixed. Ambiguous or missing source/ref/version
keys are rejected before anything is fetched. facet update resolves registry
version indexes and transitive release metadata into a complete lock before it
downloads any package archive. An existing registry lock installs without
consulting the mutable version index.
version is a semver range/constraint instead of a pinned ref —
^1.2.3 (compatible-with, semver's own "don't change the left-most
non-zero digit" rule), ~1.2.3 (patch-level only), >=1.0.0 <2.0.0
(one or two whitespace-separated comparator terms), or an exact
1.2.3. There's still no registry: a dependency's available versions
come from git ls-remote --tags --refs against its own repository,
filtered to tags that parse as semver (with or without a leading v).
When two different requesters in the dependency graph constrain the
same cut, facet intersects both ranges and picks the highest
available tag satisfying the result, deterministically. If a requester
imposing a tighter range is only discovered after the cut already
resolved to a tag that range excludes, facet retries the whole
resolution rather than treating that as unrecoverable — see "real
backtracking" below. An empty intersection (no version satisfies both,
independent of resolution order) is still an immediate hard error
naming both requesters and their own ranges; mixing an exact ref and a
version constraint for the same cut is also a hard error, except when
whichever side resolved first happens to already satisfy the other's
own constraint too — that one is never resolved by backtracking (see
below). See src/semver.h for the exact range grammar and
tools/facet_resolve.c for the resolver implementation.
Declaring a dependency here does not implicitly require_cut it —
require_cut and dependencies are separate mechanisms. A project that
uses a dependency's functions still needs its own explicit
require_cut "greeter", exactly as if that cut were installed by hand;
dependencies only tells facet what to fetch and where to put it so
that require_cut can find it.
Development dependencies
Cuts a project needs only for tests or development go in Cutfile.dev,
next to Cutfile. Its only key is dependencies, in the format above:
# Cutfile.dev
{"dependencies": {"checker": {"registry": "https://cuts.dilang.tech", "version": "^0.1.0"}}}
facet install and facet update read it only in the project they run in. A
dependency's own Cutfile.dev is never read, so the cuts it tests itself
with are not installed into projects that use it. Development dependencies
resolve in the same graph as Cutfile's (a cut both name, directly or
transitively, gets one version) and are recorded in the same Cutfile.lock. A name
may be in only one of the two files.
Cutfile.dev is never part of a published cut: facet pack takes only
Cutfile, README.md, LICENSE, lib/, bin/ and assets/. It is a
separate file, not a key in Cutfile, because every released facet rejects
an unknown key in a cut archive's Cutfile.
facet
facet install reads Cutfile in the current directory, resolves
every dependency (recursively — a dependency's own dependencies are
followed too), fetches each one via git clone/checkout (never a
shell — arguments go straight to execvp, so a URL or ref pulled from
a manifest can never be interpreted as shell syntax), and installs the
result into cuts/<name>/ with .git/ stripped (the lockfile below is
the source of truth for "what commit," not a live repository sitting
inside cuts/). Because a resolved dependency's own cloned checkout is
moved into place as-is (rename, not a re-copy that could rewrite
anything), its manifest keeps the same fixed Cutfile filename it had at
its own repository root — this is exactly why that filename isn't
parameterized by the cut's own name. It writes Cutfile.lock alongside
Cutfile — a d6 Hash literal, same as a manifest. facet itself
always writes it compact/single-line (it's machine-generated, not
something you're meant to hand-edit), but reads one back the same way it
reads any manifest, so a hand-edited lockfile can be pretty-printed too
if you ever want to:
# Cutfile.lock
{"greeter": {"source": "git", "git": "https://example.com/user/greeter", "commit": "a1b2c3d..."}}
a flat map of every resolved cut (the whole transitive set, already
flattened) to its exact commit. Once Cutfile.lock exists, facet install installs exactly what it says without re-resolving anything —
the fast, reproducible path, unaffected by a tag or branch moving in
the meantime — as long as the lock still covers Cutfile and
Cutfile.dev. When a direct dependency is missing from the lock, comes
from a different source, is locked outside its version constraint, or names a
different commit, facet install says so and resolves again, keeping each
locked registry version that still fits. A changed tag or branch is not
visible in the lock, which records only the commit; facet update picks it up.
facet update always re-resolves from Cutfile
(picking up anything a tracked branch has moved to) and rewrites the
lock.
New Git locks mark each entry with "source": "git". Older Git locks without
that key remain readable. Registry entries use "source": "registry" plus
their pinned source, version, digest, and archive size. Unknown source types
and lockfile fields fail explicitly.
For Git dependencies, resolving is fetching: discovering a dependency's own
transitive dependencies requires a clone of it to read its Cutfile.
Registry dependencies use their metadata API during facet update, so their
complete graph is resolved before any archive is downloaded.
Exact-ref conflicts are hard errors, never resolved. If two
different requesters in the dependency graph want a different git/
exact ref (tag/branch/commit) for the same cut name, facet
reports both requesters by name and stops — it can never pick one over
the other, because of the same flat-namespace constraint noted above:
two versions of one cut name could never coexist in a single compiled
d6 program anyway, so there is no "resolve to whichever" fallback
to fall back to. Two version-constrained requesters, by contrast,
intersect (see "Dependencies" above) rather than conflicting outright,
as long as some version satisfies both ranges. A cycle in the
dependency graph (A depends on B depends on A) terminates safely with
no special handling — the second time the walk reaches an
already-resolved (or already-pending) name it just stops, the same
check that would catch a genuine conflict.
Because a version-constrained dependency's own transitive dependencies
can't be discovered without first picking (and cloning) one concrete
version of it, and that choice depends on every requester's constraint
— some of which may not be discovered until later in the walk — facet
resolves exact-ref dependencies immediately (as always) but defers each
version-constrained name until the exact-ref-reachable part of the
graph runs dry, then resolves one pending name at a time, folding
whatever new work that uncovers back in, until nothing pending remains.
Real backtracking: it's still possible for a name to resolve (clone
a concrete tag) before every requester's own constraint on it is known
— a requester reached only through a different pending name, resolved
later, can turn out to want a tighter range the already-chosen tag
doesn't satisfy. Rather than treating that as unrecoverable, facet
wipes its ephemeral scratch clones and re-resolves the whole graph from
scratch, carrying forward the full intersected constraint history for
every version-constrained name across attempts — so a later attempt
already knows everything an earlier one discovered about that name,
however late, and resolves it correctly the first time that attempt
reaches it. This converges quickly in practice (at most one retry per
name that's ever affected) and is bounded (a resolver-internal error,
not a manifest problem, if it somehow doesn't). A genuinely disjoint
pair of constraints is unaffected by any of this — that's still an
immediate hard error, not a reason to retry.
A cloned dependency's own Cutfile must declare a name matching
the dependency key it was fetched as — the same rule require_cut already
enforces for hand-installed cuts (see above) — so a misconfigured
or renamed dependency fails clearly at facet install time rather than
surprising require_cut later.
facet new
facet new <kind> <path>
facet new creates a project directory at <path> with code, tests and a
README that work as generated, once facet install has fetched the cuts it
names. The project name is the path's last component.
It follows the cut-name rules (lowercase ASCII letters, digits and underscores,
starting with a letter). The parent directory must already exist, and <path>
must not.
| Kind | What it creates |
|---|---|
cli | A command-line program: main.d6 passes ARGV to the project's main and exits with the status it returns. |
web | A web application served by gremlin, with rack middleware, both runtime dependencies from the registry. |
tcp | A line-based TCP server that uses IO.poll to serve every client from one thread. |
udp | A UDP server that answers each datagram. |
lib | A library cut ready for facet check: a full manifest (version 0.1.0, MIT LICENSE), with the maintainer taken from git config user.name/user.email, or placeholders when those are unset or unusable. |
Every kind shares one layout:
<name>/
Cutfile
Cutfile.dev minitest, a development dependency
README.md
.gitignore
main.d6 (not for lib) the entry point
lib/<name>.d6 the code, in a module or class named after the project
test/<name>_test.d6 tests for it
test.sh runs every test/*_test.d6
The tests load the minitest cut with require_cut "minitest". It is listed in
Cutfile.dev, so facet install fetches it
for the project's own tests, but a lib project's users never install it.
test.sh stops with a reminder when cuts/minitest is missing.
The project's module is the name in CamelCase (my_app becomes MyApp), so
facet new refuses a name whose module would hide a built-in such as File,
Time or StandardError, collide with minitest's classes, or, for web,
collide with a dependency cut or one of its classes. tests/facet_new_test.sh
builds every kind, runs its tests and entry point, and checks that the
reserved list still matches the compiler and library sources.
facet init and facet add
facet init [name] # writes a fresh Cutfile; defaults name to the
# current directory's own basename if omitted
facet add <name> [--dev] [--registry <https-url>] [--version <constraint>]
facet add <name> [--dev] --git <url> (--tag <ref> | --branch <ref> | --commit <ref> | --version <constraint>)
Without --git, the dependency comes from a registry: --registry, or the
public registry at https://cuts.dilang.tech. Without --version, facet add
asks the registry for the newest release that is not yanked and writes ^ plus
that version, so facet update takes compatible releases only.
--dev adds the dependency to Cutfile.dev instead, creating it if needed
(see "Development dependencies" above); the name must not already be in either
file.
facet init refuses to run if Cutfile already exists — it never
overwrites one. facet add requires one to already exist (facet init
first, or hand-write one), parses it, appends the new dependency (a hard
error if that name is already present — edit the existing entry by hand,
or remove it first, rather than guessing you meant to replace it), and
rewrites the file. Both validate what they write the same way facet install would reject it later: for a git dependency exactly one of --tag/--branch/
--commit/--version, and a --version value that actually parses as a
constraint (src/semver.h's own grammar) — so a mistake is caught
immediately, not at the next facet install.
facet add rewrites the entire Cutfile, not just the one entry
being added — there's no Hash-literal-aware text editor here, only a
parse-the-whole-thing-and-regenerate-it round trip (mirroring how facet install/update already regenerate Cutfile.lock from scratch every
time). Any hand-added comment or unusual formatting in an existing
Cutfile does not survive a facet add; the regenerated file uses
the same pretty-printed, multi-line style described above. It does
round-trip every existing dependency's own ref kind exactly (a branch
dependency stays a branch dependency after adding an unrelated one) —
this is the one thing worth getting right in a full-file rewrite, since
guessing wrong here would silently change what a dependency actually
tracks, not just how the file looks.
A real example: packages/http
HTTP support is an ordinary cut rather than part of the runtime. The bundled
example at packages/http has its own Cutfile, README, and tests, and is
loaded with require_cut rather than discovered by the core require path.
To use it from another project the same way any facet dependency
works, either copy packages/http/ into that project's
cuts/http/ directly, or give it its own git remote and point a
dependencies entry at that:
{"name": "myapp", "dependencies": {"http": {"git": "<url-of-a-remote-for-packages/http>", "tag": "v0.1.0"}}}
facet has no notion of "a subdirectory of a larger repo is the
cut root" — a git dependency's whole repo becomes the installed
cut, so the above only works against a remote whose root actually
is packages/http's own contents, not this repo's.
require and require_cut never shadow each other
A relative file and a same-named cut can sit side by side without either
one affecting the other — require "greeter" only ever considers a
relative file (or absolute path), and require_cut "greeter" only ever
considers cuts/greeter/. There is no precedence question to resolve
between them, because there is no path along which they could compete in
the first place.
Packages in this repository
Each lives in packages/NAME with its own Cutfile, README and tests, and installs
with facet like any other cut.
| Package | What it is |
|---|---|
active_auth | Account, session, password reset, and recovery-code models |
active_discussion | Threaded comments, karma votes, and moderation signals |
active_karma | Project trust signals into persona state |
active_record | SQL repositories, models, validation, and migrations |
active_social | Follower relationships backed by ActiveRecord |
active_tagging | Normalized tags and tagging relationships |
arel | Immutable SQL query builder with dialect visitors |
cancellation | Cooperative cancellation tokens, deadlines, and task scopes |
cookies | Cookie parsing, signing, encryption, and session helpers |
database_config | Load JSON database configuration and open a connection |
dials | HTTP routing and controller responses |
div | Compile view templates into d6 source |
graphql | GraphQL schema and query execution |
graphsql | GraphQL-aware SQL selection and association preloading |
gremlin | Concurrent fiber-based HTTP server |
http | HTTP server and client for d6 |
jobs | Durable database-backed background jobs |
log_viewer | Format structured JSON log streams |
logger | Leveled text and structured JSON logging |
minitest | Assertions, skips, setup/teardown hooks and a test runner (a copy of lib/minitest.d6); facet new projects list it in Cutfile.dev |
mjolnir | Stateless data mapper on Arel: schemas, immutable queries, changesets, and a Repo |
multipart | Parse multipart form data and file uploads |
network_safety | Guard outbound HTTP URLs against local names and private IPv4 literals |
rack | Compose HTTP request middleware |
redis | Redis client using RESP2 and RESP3 |
registry | Cut registry storage primitives |
websocket | WebSocket handshake and frame handling |
What's deliberately out of scope so far
- A manifest's own top-level
versionkey ({"name": "greeter", "version": "0.1.0"}— a cut declaring its own version) is still validated to be a String and nothing more; nothing compares, selects, or otherwise consumes it. Don't confuse this with adependenciesentry's ownversionkey (a requester's constraint on some other cut, "Dependencies" above) — that one is real now. A cut's own top-levelversionstaying inert isn't an oversight sharing the same gap:facetlearns what versions a dependency actually has from its git tags, never from aCutfilestring the requester would have to trust regardless. - Two versions of the same cut coexisting is architecturally
constrained, not just unbuilt: d6 compiles every
require/require_cutd file into one flat namespace with small, global, shared tables (DIAMOND_MAX_FUNCTIONS,DIAMOND_MAX_CLASSES,DIAMOND_MAX_MODULESinsrc/vm.h), so two versions of the same cut could never coexist in one compiled program — same function/class names would collide. This is why an exact-ref conflict is still a hard error rather than something to resolve (two version-constrained requesters intersect instead, since a range genuinely can pick one version satisfying both — see "Dependencies" above): there is no such thing as "both," so nothing could ever be resolved to when the refs themselves disagree. - A default registry and search in
facet:facet install greeterby short name, or a search command, with no explicit registry. The public registry at cuts.dilang.tech is live, but a registry dependency resolves only against the explicit HTTPS source in a manifest, andfacethas no built-in default and no search command.
Each of these is a plausible next slice, sized independently rather than attempted together.