valise-sh 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/variants.md CHANGED
@@ -1,63 +1,327 @@
1
- # Build Variants and Installed Releases
1
+ # The compile and build contract
2
2
 
3
- Two different mechanisms share the word "variant". They compose, but they
4
- are not the same command. This document describes both and says which
5
- repository each one needs.
3
+ This document describes how a consuming project turns metadata into an
4
+ installed release: what each input file contains, what `valise compile`
5
+ produces, what `valise build` packages, and how the two fail.
6
6
 
7
- ## Installed releases: `variant list` and `variant use`
7
+ Two different mechanisms share the word "variant". They compose, but they are
8
+ not the same command.
8
9
 
9
- These subcommands operate on the releases already installed on this
10
- machine. No source tree is required.
10
+ - **Build variants** are exclusion lists decided at compile time inside the
11
+ consuming project. They live in `shell/release/install-variants`.
12
+ - **Installed releases** are sealed directories on this machine. They are
13
+ listed, switched, and pruned with `valise release list|use|prune`.
14
+
15
+ Neither reads the other's file. `release use` never reads `install-variants`;
16
+ variant exclusion is decided when the plans are compiled.
17
+
18
+ ## The project layout
19
+
20
+ A consuming project that uses the CLI has this shape:
21
+
22
+ ```
23
+ shell/
24
+ module.d/ primary modules: <name>.meta
25
+ modules/ primary module bodies
26
+ layers/<provider>/ provider layers (optional)
27
+ module.d/ layer modules: <name>.meta
28
+ modules/ layer module bodies
29
+ release/
30
+ bundle-roots which modules seed each bundle, per shell
31
+ install-variants build variants and their exclusion lists
32
+ module-registry.tsv GENERATED from the metadata above
33
+ project.conf project defaults (optional)
34
+ ```
35
+
36
+ `shell/module.d/` and `shell/release/module-registry.tsv` are the two markers
37
+ that identify a directory as a valise project. A layer's `<provider>` is a
38
+ path component made of letters, digits, `_`, `-`, and `.`; it may not start
39
+ with `.` or contain `..`. The registry rejects any other provider name,
40
+ because the compiler emits module identities from it as shell source text. The CLI resolves the project
41
+ root from the working directory first, then from the engine's own location, so
42
+ running `valise compile` from the project root always compiles *that* project
43
+ rather than a tree that merely happens to contain the engine.
44
+
45
+ ## Metadata is the source of truth
46
+
47
+ Module metadata is data: `key=value` lines in a `.meta` file, parsed and
48
+ validated, never sourced. See [module-abi.md](module-abi.md) for the full
49
+ field list and the grammar each value must satisfy.
50
+
51
+ The registry is a **generated artifact**, not a second source of truth. The
52
+ generator is `valise registry`:
11
53
 
12
54
  ```sh
13
- valise variant list
55
+ valise registry # write shell/release/module-registry.tsv
56
+ valise registry --check # compare, fail on drift, write nothing
57
+ valise registry path/to/registry.tsv # write somewhere else
14
58
  ```
15
59
 
16
- This lists the installed sealed releases by directory name. Release
17
- directories are named by the content-derived SHA-256 of their manifest,
18
- and the active release is marked.
60
+ `valise build` regenerates the registry before it builds, so a project
61
+ that only has `.meta` files and bodies builds with no hand-written TSV. The
62
+ generator refuses to publish a registry when the metadata is inconsistent:
63
+ a `name` that does not match its filename, a missing body, a layer module
64
+ without the `contrib_` prefix, a load path that is not representable, or an
65
+ export name that is not a shell identifier.
66
+
67
+ The registry is a tab-separated file with one header row and one row per
68
+ module:
69
+
70
+ | # | Column | Meaning |
71
+ |---|---|---|
72
+ | 1 | `name` | Module identifier. |
73
+ | 2 | `version` | Optional version string; empty means unversioned. |
74
+ | 3 | `side_effect` | Startup side-effect class. |
75
+ | 4 | `requires` | Space-separated required dependencies. |
76
+ | 5 | `optional` | Space-separated optional dependencies. |
77
+ | 6 | `shells` | Space-separated shell identifiers. |
78
+ | 7 | `platforms` | Space-separated platform identifiers. |
79
+ | 8 | `exports` | Space-separated exported identifiers. |
80
+ | 9 | `load` | Repository-relative body path, or empty for a meta module. |
81
+ | 10 | `updates` | Space-separated update registration identifiers. |
82
+ | 11 | `source` | Directory that holds the `.meta`: `shell/module.d` or `shell/layers/<provider>/module.d`. |
83
+
84
+ Rows come out in a fixed order: the primary modules of `shell/module.d/`
85
+ first, then the layer modules of each `shell/layers/<provider>/module.d/`,
86
+ each group in C-locale name order. The order does not depend on the caller's
87
+ locale, so regenerating an unchanged project produces a byte-identical file.
88
+
89
+ ## Bundles: which modules seed each shell
90
+
91
+ A bundle names the root modules for each shell. The file is
92
+ `shell/release/bundle-roots`, tab-separated:
93
+
94
+ ```
95
+ # bundle shell roots
96
+ default bash profile_main
97
+ default zsh profile_main
98
+ default ksh profile_main
99
+ ```
100
+
101
+ - Column 1 is the bundle name.
102
+ - Column 2 is the shell the roots apply to.
103
+ - Column 3 is a space-separated list of root module names.
104
+
105
+ A root may be a bare name (`profile_main`), which resolves against
106
+ `shell/module.d/`, or a qualified name (`contrib_tool@shell/layers/vendor/module.d`)
107
+ that names its source directory explicitly. Use the qualified form when a
108
+ layer module and a primary module share a name.
109
+
110
+ The first row may be a header (`bundle`, `shell`, `roots`); it is not a bundle
111
+ definition.
112
+
113
+ A bundle names the **starting points**, not the final set. `valise compile`
114
+ walks each root's `requires` and `optional` edges, so the installed plan
115
+ contains the whole reachable closure. `valise explain-bundles` prints that
116
+ closure per shell, which is the fastest way to see what a bundle actually
117
+ loads:
118
+
119
+ ```sh
120
+ valise explain-bundles
121
+ valise explain-bundles --bundle=default
122
+ ```
123
+
124
+ It reads `bundle-roots` and the registry directly, and needs no Python. An
125
+ unknown `--bundle` name is an error, not an empty report.
126
+
127
+ ## Variants: what to leave out
128
+
129
+ A build variant is an exclusion list. The file is
130
+ `shell/release/install-variants`, tab-separated:
131
+
132
+ ```
133
+ variant_name description excluded_modules
134
+ default All project modules
135
+ workstation-lean No editor defaults contrib_editor
136
+ ```
137
+
138
+ - Column 1 is the variant name.
139
+ - Column 2 is a human-readable description.
140
+ - Column 3 is a space-separated list of excluded modules.
141
+
142
+ An exclusion is a bare module name (matching a `shell/module.d/` module) or a
143
+ qualified name (`<name>@<source>`) that names the source directory
144
+ explicitly. Every exclusion must match exactly one registry row; an unknown
145
+ or ambiguous exclusion fails the compile rather than silently excluding
146
+ nothing.
147
+
148
+ Exclusion is applied to the whole resolved graph, not just the roots. A module
149
+ excluded by name is not loaded, and neither is anything that depends on it.
150
+ `valise explain-variant <name>` shows a variant's description, its exclusion
151
+ list, and the per-shell impact.
152
+
153
+ The variant and the bundle compose: the bundle decides the starting points
154
+ and the layers they reach, and the variant then removes modules from the
155
+ resolved graph.
156
+
157
+ ## `valise compile`
19
158
 
20
159
  ```sh
21
- valise variant use <sha>
22
- ```
23
-
24
- This verifies the named release byte-for-byte against its manifest and
25
- switches the active pointer to it. The target must be an existing
26
- release directory under the releases root; anything else fails closed.
27
-
28
- ## Build variants: exclusion lists in the consuming project
29
-
30
- Separately, a consuming project can define named build variants as
31
- exclusion lists: a variant names modules to leave out of an installed
32
- plan so different machines share one release while loading different
33
- subsets. This mechanism lives in the consuming project, not in this
34
- repository:
35
-
36
- - The variant file (`shell/release/install-variants`, tab-separated
37
- `variant_name`, description, excluded modules) belongs to the
38
- consuming project.
39
- - `valise compile [--bundle=<name>] [--variant=<name>] <output-dir>`
40
- resolves the module graph from seeds, removes the active variant's
41
- exclusion set, and writes installed plans. It requires the consuming
42
- project's `shell/release/module-registry.tsv`,
43
- `shell/release/profile-release.json`,
44
- `hack/compile-shell-profile-release.py`, and `install-variants`
45
- files, and fails closed with "cannot find source tree" without them.
46
- - `valise explain-variant <name>` shows the root count and exclusion
47
- count per shell for a variant in `install-variants`. It requires the
48
- same consuming-project files.
49
-
50
- `valise variant list` / `valise variant use` never read
51
- `install-variants`. Variant exclusion is decided at compile time in
52
- the consuming project; release switching happens at install time with
53
- `variant use`.
54
-
55
- ## Bundles vs variants
56
-
57
- Bundles select which layer roots are active per shell (for example,
58
- core+private vs core+private+contrib). They are defined in the consuming
59
- project's `shell/release/bundles.json` and selected with `valise compile
60
- --bundle=<name>`. Variants select which modules within those layers to
61
- exclude. They compose: first a bundle determines the layer set, then a
62
- variant prunes specific modules from the resolved graph. Both files and
63
- both flags belong to the consuming-project compile path.
160
+ valise compile [--bundle=<name>] [--variant=<name>] <output-dir>
161
+ ```
162
+
163
+ Compile resolves the module graph and writes one plan per shell. It does not
164
+ copy module bodies; that is `build`'s job.
165
+
166
+ The steps, in order:
167
+
168
+ 1. Resolve the project root. Fail with `cannot find source tree` when the
169
+ project has no `shell/release/module-registry.tsv`.
170
+ 2. Validate the variant: the name must appear exactly once in
171
+ `install-variants`, and every exclusion must match exactly one registry
172
+ row.
173
+ 3. Validate the bundle when `--bundle` is given: the name must exist, and
174
+ its roots must resolve.
175
+ 4. For each of `bash`, `zsh`, and `ksh`, resolve the
176
+ bundle's roots for that shell, walk the dependency closure, drop the
177
+ variant's exclusions, select each module by `shells=`, emit its
178
+ `platforms=` gate, and emit a plan.
179
+ 5. Replace the output directory atomically.
180
+
181
+ The output directory must be empty or absent. Compile stages into a private
182
+ sibling directory and publishes only after every plan has compiled, so a
183
+ failed compile never leaves a half-written output tree.
184
+
185
+ ### What a plan contains
186
+
187
+ Each plan is a POSIX shell file at `generated/installed-modules.<shell>`:
188
+
189
+ ```
190
+ # shell-profile-installed-modules-v1
191
+ # Generated by valise compile; DO NOT EDIT.
192
+ # plan=bash
193
+ # roots=profile_main
194
+ # order=hello profile_main
195
+ ```
196
+
197
+ The header names the format, the generator, the shell, the roots, and the
198
+ resolved load order. The body then:
199
+
200
+ - validates `VALISE_MODE` (`strict` or `permissive`; default `strict`);
201
+ - probes the platform once and gates each module by `platforms=`;
202
+ - sources `vendor/shell-profile/lib/capabilities.sh`, so `sp_cap_*` helpers
203
+ stay available to module functions after the plan finishes;
204
+ - checks each module's declared exports before and after loading it;
205
+ - does not run a module whose required dependency is unavailable, whether that
206
+ dependency failed or was skipped for this platform, and reports it by name;
207
+ - in strict mode, returns nonzero at the first failure in the required closure:
208
+ the bundle's roots and everything they require;
209
+ - in permissive mode, reports each failure, continues with the unrelated
210
+ modules, and returns zero;
211
+ - treats an optional dependency as best effort: it loads first when it can, and
212
+ its absence never affects the module that declared it. A failure is reported;
213
+ an unsupported platform is expected and prints nothing;
214
+ - when `VALISE_RELOAD=1` is set, clears the plan's declared exports first, so
215
+ the plan can be sourced again.
216
+
217
+ The plan is the compiled equivalent of the loader: same dependency rules,
218
+ same export checks, same strict and permissive behavior. The plan is static and fast;
219
+ the loader parses metadata at runtime and is the fallback for releases that
220
+ ship an `entrypoints/` tree instead of `generated/` plans.
221
+
222
+ ### Failure behavior
223
+
224
+ Compile fails closed, before writing anything, when:
225
+
226
+ - the project root cannot be resolved;
227
+ - the variant is unknown or ambiguous;
228
+ - a variant exclusion matches no registry row, or more than one;
229
+ - the bundle is unknown, or its roots do not resolve;
230
+ - a module in the closure has a `side_effect` that is not safe to source at
231
+ startup (`generated-file`, `update-network`, `install-time`);
232
+ - the capabilities prelude is missing or defines no `sp_cap_*` functions;
233
+ - the registry names a module whose source, load path, or export name is not
234
+ representable.
235
+
236
+ Fish plans are not produced by the POSIX plan generator. `valise compile`
237
+ emits POSIX plans only; a Fish integration uses the native Fish loader.
238
+
239
+ ## `valise build`
240
+
241
+ ```sh
242
+ valise build [--bundle=<name>] [--variant=<name>] [out]
243
+ ```
244
+
245
+ Build a complete, installable release tree. It runs `valise
246
+ compile` internally, so everything compile validates is validated here too.
247
+
248
+ The built tree has this layout:
249
+
250
+ ```
251
+ generated/installed-modules.bash compiled plans
252
+ generated/installed-modules.zsh
253
+ generated/installed-modules.ksh
254
+ module.d/<name>.meta primary module metadata
255
+ modules/<path> primary module bodies
256
+ layers/<provider>/module.d/<name>.meta layer metadata
257
+ layers/<provider>/<path> layer bodies
258
+ vendor/shell-profile/lib/capabilities.sh the capability prelude
259
+ shell/release/ bundle-roots, install-variants, registry
260
+ shell/install/profile-release.manifest the release manifest
261
+ ```
262
+
263
+ Build:
264
+
265
+ 1. Runs from the project root — it needs `shell/module.d/`.
266
+ 2. Regenerates `shell/release/module-registry.tsv` from the metadata.
267
+ 3. Compiles the plans into a staging directory.
268
+ 4. Copies the capability prelude from the installed engine. The packaged
269
+ file is the same file the plans were built against.
270
+ 5. Copies each module's metadata and body to its release path. A primary
271
+ module lands at the release root; a layer module lands at
272
+ `layers/<provider>/`, matching the paths the plan references.
273
+ 6. Normalizes file modes to `0644` and writes the release manifest.
274
+ 7. Publishes the tree at the destination.
275
+
276
+ Like compile, build stages beside the destination and publishes only after
277
+ the complete tree and its manifest exist. A failed build never leaves a
278
+ partial release. The default destination is `build/release`; an existing
279
+ default tree is replaced only after the new tree has compiled and been
280
+ manifested. An explicit destination must be empty or absent.
281
+
282
+ Building the same project twice produces byte-identical output, including
283
+ the manifest.
284
+
285
+ ## `valise build` and `valise apply`
286
+
287
+ `valise build` produces the tree; `valise apply` adds install and shell
288
+ integration on top of it:
289
+
290
+ - `valise build [--variant=<name>] [--bundle=<name>] [out]` builds to
291
+ `build/release` by default, reading the variant and bundle defaults from
292
+ `shell/release/project.conf`.
293
+ - `valise apply [--variant=<name>] [--bundle=<name>] [--no-integration]`
294
+ builds, installs the release, writes the managed shell integration,
295
+ verifies the active release, and reports what changed.
296
+
297
+ Both are idempotent: running them twice leaves the same state.
298
+
299
+ `shell/release/project.conf` holds the defaults:
300
+
301
+ ```
302
+ variant=default
303
+ bundle=default
304
+ ```
305
+
306
+ ## `valise release list`, `use`, and `prune`
307
+
308
+ These operate on the releases installed on this machine. No source tree is
309
+ needed.
310
+
311
+ ```sh
312
+ valise release list
313
+ valise release use <release-id>
314
+ valise release prune --keep=3
315
+ ```
316
+
317
+ Release directories are named by the content-derived SHA-256 of their
318
+ manifest, and the active one is marked in the list. `release use` verifies
319
+ the named release byte-for-byte against its own manifest before switching the
320
+ active pointer.
321
+
322
+ `release prune --keep=N` removes old releases. It keeps the active release
323
+ and the `N` newest others, ordered by directory modification time. Before
324
+ removing any candidate it verifies the candidate against its own manifest; a
325
+ release that fails verification is **kept** and reported, because a release
326
+ that cannot be verified may be the only recoverable copy. The default is
327
+ `--keep=3`.
@@ -2,10 +2,21 @@
2
2
 
3
3
  - `local/` is the smallest example: one metadata file and one POSIX shell
4
4
  module. It demonstrates sourcing a module without installing a release.
5
+ - `profile-project/` is a complete consuming project: metadata-driven
6
+ modules, a provider layer, a bundle, a variant, and project defaults.
7
+ It is the shortest path from a clone to a loaded profile.
5
8
  - `e2e/` is a sample consumer profile. Its twelve modules cover dependency
6
9
  resolution, platform and shell selection, exports, optional dependencies,
7
10
  release installation, and shared-module composition.
8
11
 
12
+ ## Complete project
13
+
14
+ `profile-project/` is the one to read if you want to see what a profile
15
+ project looks like end to end. Its
16
+ [README](profile-project/README.md) walks from a fresh checkout through
17
+ `valise build` and `valise apply` to a `greet` function that works in a new
18
+ login shell.
19
+
9
20
  ## Minimal module
10
21
 
11
22
  From the repository root, load the example with:
@@ -42,14 +42,14 @@ export XDG_DATA_HOME="$_demo/data"
42
42
  export XDG_CONFIG_HOME="$_demo/config"
43
43
  export XDG_STATE_HOME="$_demo/state"
44
44
 
45
- (cd examples/e2e && valise install)
46
- valise verify-active
45
+ (cd examples/e2e/build/release && valise install)
46
+ valise release verify
47
47
  ```
48
48
 
49
- The build script copies the engine runtime and shared example into the e2e
50
- tree, then writes `shell/install/profile-release.manifest` with each file's
51
- size and SHA-256 digest. Generated release files are ignored by Git and can
52
- be rebuilt at any time.
49
+ The build script copies the engine runtime and shared example into
50
+ `examples/e2e/build/release`, then writes `shell/install/profile-release.manifest`
51
+ with each file's size and SHA-256 digest using `valise make-manifest`. Generated
52
+ release files are isolated in `build/` and can be rebuilt at any time.
53
53
 
54
54
  Load sample modules in a POSIX shell:
55
55
 
@@ -71,7 +71,7 @@ environment before starting Fish:
71
71
 
72
72
  ```sh
73
73
  VALISE_MODULES=e2e_fish_features VALISE_MODE=strict fish -c \
74
- 'functions -q e2e_fish_loaded; and valise verify-active'
74
+ 'functions -q e2e_fish_loaded; and valise release verify'
75
75
  ```
76
76
 
77
77
  When finished, stop using the functions and remove the temporary installation: