valise-sh 0.1.0 → 0.3.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +320 -9
  2. package/README.md +104 -127
  3. package/bin/valise +67 -3266
  4. package/docs/architecture.md +76 -51
  5. package/docs/contrib-modules.md +180 -173
  6. package/docs/installation.md +210 -69
  7. package/docs/module-abi.md +211 -148
  8. package/docs/projects.md +249 -0
  9. package/docs/release-policy.md +90 -11
  10. package/docs/shared-modules.md +149 -26
  11. package/examples/README.md +20 -8
  12. package/examples/e2e/README.md +61 -68
  13. package/examples/e2e/bundles.tsv +6 -0
  14. package/examples/e2e/module.d/e2e_aliases.meta +1 -2
  15. package/examples/e2e/module.d/e2e_completions_bash.meta +0 -1
  16. package/examples/e2e/module.d/e2e_completions_zsh.meta +0 -1
  17. package/examples/e2e/module.d/e2e_env_setup.meta +2 -3
  18. package/examples/e2e/module.d/e2e_fish_features.meta +0 -1
  19. package/examples/e2e/module.d/e2e_generated_config.meta +1 -2
  20. package/examples/e2e/module.d/e2e_ksh_features.meta +0 -1
  21. package/examples/e2e/module.d/e2e_layer_probe.meta +1 -2
  22. package/examples/e2e/module.d/e2e_optional_network.meta +1 -2
  23. package/examples/e2e/module.d/e2e_platform_tools.meta +8 -0
  24. package/examples/e2e/module.d/e2e_platform_tools_bsd.meta +2 -3
  25. package/examples/e2e/module.d/e2e_platform_tools_darwin.meta +2 -3
  26. package/examples/e2e/module.d/e2e_platform_tools_linux.meta +2 -3
  27. package/examples/e2e/valise.conf +4 -0
  28. package/examples/e2e/variants.tsv +3 -0
  29. package/examples/local/module.d/hello_valise.meta +1 -2
  30. package/examples/profile-project/README.md +151 -0
  31. package/examples/profile-project/bundles.tsv +8 -0
  32. package/examples/profile-project/layers/demo/module.d/contrib_demo_greeting.meta +9 -0
  33. package/examples/profile-project/layers/demo/modules/shared/contrib_demo_greeting.sh +4 -0
  34. package/examples/profile-project/module.d/greeting.meta +9 -0
  35. package/examples/profile-project/module.d/profile_main.meta +8 -0
  36. package/examples/profile-project/modules/greeting.sh +5 -0
  37. package/examples/profile-project/valise.conf +4 -0
  38. package/examples/profile-project/variants.tsv +3 -0
  39. package/examples/shared/README.md +13 -34
  40. package/examples/shared/module.d/contrib_shared_paths.meta +2 -3
  41. package/examples/shared/module.d/contrib_shared_umask.meta +1 -2
  42. package/examples/shared/modules/shared/contrib_shared_paths.sh +2 -2
  43. package/lib/capabilities.sh +40 -13
  44. package/lib/cli/commands.sh +278 -0
  45. package/lib/cli/common.sh +326 -0
  46. package/lib/cli/devtools.sh +539 -0
  47. package/lib/cli/fish-hook.sh +986 -0
  48. package/lib/cli/integration.sh +337 -0
  49. package/lib/cli/lifecycle.sh +578 -0
  50. package/lib/cli/project.sh +1807 -0
  51. package/lib/cli/store.sh +1023 -0
  52. package/lib/cli/usage.sh +308 -0
  53. package/lib/compiler.awk +352 -24
  54. package/lib/fish/loader.fish +163 -28
  55. package/lib/fish/metadata.fish +62 -19
  56. package/lib/fish/profile.fish +170 -0
  57. package/lib/loader.sh +736 -158
  58. package/lib/metadata.sh +137 -55
  59. package/manifests/parser-support.tsv +2 -2
  60. package/module.d/valise_editor.meta +9 -0
  61. package/module.d/valise_history.meta +9 -0
  62. package/module.d/valise_pager.meta +9 -0
  63. package/module.d/valise_path.meta +9 -0
  64. package/module.d/{platform_bsd.meta → valise_platform_bsd.meta} +3 -4
  65. package/module.d/{platform_linux.meta → valise_platform_linux.meta} +3 -4
  66. package/module.d/{platform_macos.meta → valise_platform_macos.meta} +3 -4
  67. package/module.d/{platform_posix.meta → valise_platform_posix.meta} +2 -3
  68. package/module.d/valise_xdg.meta +9 -0
  69. package/modules/system/{contrib_editor.sh → valise_editor.sh} +2 -2
  70. package/modules/system/{contrib_history.sh → valise_history.sh} +2 -2
  71. package/modules/system/{contrib_pager.sh → valise_pager.sh} +2 -2
  72. package/modules/system/{contrib_path.sh → valise_path.sh} +3 -3
  73. package/modules/system/{contrib_xdg.sh → valise_xdg.sh} +2 -2
  74. package/package.json +91 -9
  75. package/docs/variants.md +0 -63
  76. package/entrypoints/.gitkeep +0 -0
  77. package/entrypoints/fish/profile.fish +0 -99
  78. package/examples/e2e/build-manifest.sh +0 -250
  79. package/examples/local/.gitkeep +0 -0
  80. package/lib/.gitkeep +0 -0
  81. package/manifests/.gitkeep +0 -0
  82. package/module.d/.gitkeep +0 -0
  83. package/module.d/contrib_editor.meta +0 -10
  84. package/module.d/contrib_history.meta +0 -10
  85. package/module.d/contrib_pager.meta +0 -10
  86. package/module.d/contrib_path.meta +0 -10
  87. package/module.d/contrib_xdg.meta +0 -10
  88. package/modules/.gitkeep +0 -0
@@ -1,15 +1,17 @@
1
- # Shell-profile module ABI
1
+ # Module ABI
2
2
 
3
- This document defines the metadata consumed by the loader before it sources
4
- module bodies. Metadata is data: discovery does not execute module
5
- implementations to learn their names, dependencies, or supported runtimes.
3
+ This document defines module metadata and the rules the compiled plans and
4
+ the loaders apply to it. Metadata is data: valise never executes a module to
5
+ learn its name, dependencies, or supported shells.
6
6
 
7
7
  ## Metadata file format
8
8
 
9
9
  Module metadata lives in `module.d/*.meta` as UTF-8 text with one `key=value`
10
10
  field per line. Blank lines and `#` comments are ignored. Values are plain text;
11
11
  there is no shell quoting, interpolation, command substitution, glob expansion,
12
- or `eval` phase.
12
+ or `eval` phase. Each field may appear once: a repeated key is rejected rather
13
+ than letting the last value win. An unknown field is rejected, and so is
14
+ `updates`, which was removed in 0.3.0.
13
15
 
14
16
  Required fields:
15
17
 
@@ -19,7 +21,8 @@ Required fields:
19
21
  | `shells` | Space-separated supported shell identifiers. |
20
22
  | `platforms` | Space-separated supported platform identifiers. |
21
23
  | `side_effect` | Startup/install/update side-effect class. |
22
- | `load` or `load_hook` | Exactly one load target: a relative module body path or a validated shell function hook. |
24
+ | `load` | Path to the module body, relative to the module's source directory. Absent for a **meta module** (see below). |
25
+ | `requires` | Required for a meta module. |
23
26
 
24
27
  Optional fields:
25
28
 
@@ -28,9 +31,41 @@ Optional fields:
28
31
  | `requires` | Space-separated required dependency module identifiers. |
29
32
  | `optional` | Space-separated optional dependency module identifiers. |
30
33
  | `exports` | Space-separated exported command/function identifiers. |
31
- | `updates` | Space-separated update identifiers registered by the consuming project. |
34
+ | `commands` | Space-separated exports a built release publishes as commands on PATH. |
35
+ | `executables` | Space-separated module-relative paths of scripts published as commands. |
36
+ | `tests` | Space-separated module-relative test scripts that `valise test` runs. |
32
37
  | `version` | Optional module version used to reject version skew across roots. |
33
38
 
39
+ ## Meta modules
40
+
41
+ A **meta module** declares dependencies and has no body. It is the natural
42
+ way to write an aggregator: instead of an empty shell file whose only job is
43
+ to name a dependency, the aggregator is metadata alone.
44
+
45
+ A meta module has `requires=` with at least one entry and **no** `load=`.
46
+ The loader and the compiler treat it as a dependency-only
47
+ record: the module itself has nothing to source, but its required closure
48
+ loads normally and in the right order. A meta module must leave `exports=`
49
+ empty; it has no body to define an exported function. Metadata that declares
50
+ an export on a meta module is rejected.
51
+
52
+ ```text
53
+ name=profile_main
54
+ version=1.0.0
55
+ side_effect=read-only
56
+ requires=valise_path valise_editor
57
+ optional=
58
+ shells=posix
59
+ platforms=any
60
+ exports=
61
+ ```
62
+
63
+ At least one of `load` or `requires` is required: a module with neither a
64
+ body nor dependencies is malformed.
65
+
66
+ `load` and `requires` are **not** mutually exclusive. A module that declares
67
+ both loads its own body *and* its dependencies, in dependency-first order.
68
+
34
69
  ## Identifiers and allowed values
35
70
 
36
71
  Module names, dependency names, and exported shell functions must match
@@ -41,60 +76,76 @@ non-empty list containing only whitespace is malformed. Required `shells` and
41
76
  `platforms` lists must contain at least one parsed value. Empty identifiers,
42
77
  whitespace inside one identifier, path separators, quotes, shell metacharacters,
43
78
  glob characters, and control characters are invalid. Invalid identifiers are
44
- rejected before the loader can build function names or compatibility
45
- bridge hooks.
46
-
47
- Accepted `shells` values are `sh`, `bash`, `zsh`, `ksh`, and `fish`.
48
- `shells=sh` means the module body is POSIX-shell syntax and may be loaded by
49
- the POSIX core from `sh`, `bash`, `zsh`, or `ksh` adapters. `fish` selects the
50
- native Fish surface under `lib/fish/`; it never sources POSIX shell bodies.
51
- The engine ships a generic Fish adapter at `entrypoints/fish/profile.fish` as
52
- the reference implementation of this handshake: it sets
53
- `_sp_fish_loader_bootstrap_mode` and `_sp_fish_loader_bootstrap_seeds`, then sources `loader.fish`. The loader
54
- consumes and clears that request and publishes a fresh
55
- `_sp_fish_loader_bootstrap_status`; callers must fail closed when the marker is
56
- absent. Fish provider precedence is project-native, engine-native, engine
57
- generic, and project generic. Duplicate providers are valid only when their
58
- metadata and native bodies are byte-identical; conflicting metadata or bodies
59
- is rejected before any body is sourced. Public adapters must not execute the
60
- generated Fish plan directly.
79
+ rejected before the loader can build function names or source a body.
80
+
81
+ Accepted `shells` values are `posix`, `sh`, `bash`, `zsh`, `ksh`, and
82
+ `fish`. Each running POSIX shell belongs to one class: `bash`, `zsh`, or
83
+ `ksh` when the shell can be proven to be that shell, and `sh` for every
84
+ other POSIX shell (dash, BusyBox ash, and so on). The values match classes
85
+ as follows:
86
+
87
+ - `posix` matches every POSIX class: `sh`, `bash`, `zsh`, and `ksh`. Use it
88
+ for a body written in portable shell.
89
+ - `sh` matches only the `sh` class. Combine it with the shells the body also
90
+ supports: `shells=sh bash ksh` loads in dash, bash, and ksh, and is skipped
91
+ in zsh, for a body that needs word splitting zsh does not do.
92
+ - `bash`, `zsh`, and `ksh` match only that class.
93
+ - `fish` selects the native Fish runtime. Fish never sources a POSIX body,
94
+ and no POSIX value matches Fish.
95
+
96
+ A module whose shells do not include the running class is skipped; a bundle
97
+ root, or a module a root requires, that does not support its shell is an
98
+ error.
99
+
100
+ In a built release, the native Fish loader resolves the release's Fish roots
101
+ (`roots_fish=` in `valise-release.conf`) against the release's `module.d/`,
102
+ each `layers/<provider>/module.d/`, and `valise/module.d/`, in that order.
103
+ `runtime/fish/profile.fish` is the adapter the Fish hook sources: it sets
104
+ `_sp_fish_loader_bootstrap_mode` and `_sp_fish_loader_bootstrap_seeds`,
105
+ sources `runtime/fish/loader.fish`, and requires the
106
+ `_sp_fish_loader_bootstrap_status` marker the loader publishes, failing
107
+ closed when it is absent. Duplicate providers of one name are valid only when
108
+ their metadata and Fish bodies are byte-identical.
61
109
 
62
110
  Supported `platforms` values are `any`, `darwin`, `linux`, `bsd`, `freebsd`,
63
111
  `openbsd`, and `netbsd`. `platforms=any` matches every supported platform.
64
112
  Specific platform values are ORed: `platforms=darwin linux` matches macOS or
65
113
  Linux, not BSD.
66
114
 
67
- ## Multi-root layer search path
68
-
69
- The loader resolves modules across a priority-ordered root list. The primary
70
- root is the `sp_loader_load_modules <root> ...` argument and always wins for
71
- metadata it provides. `VALISE_MODULE_PATH` lists additional layer roots as a
72
- colon-separated path, searched in priority order after the primary root.
73
- With it unset the behavior is exactly the historical single-root
74
- behavior.
75
-
76
- Semantics, enforced by `tests/loader/multi-root.test.sh`:
77
-
78
- - Every search-path element must be a non-empty readable, executable directory; empty
79
- elements, missing, unreadable, or unsearchable directories are fail-closed
80
- data errors (element validation runs with globbing disabled, so
81
- glob-metacharacter elements are not expanded and must name a real readable,
82
- executable directory). Identical duplicate elements are deduplicated
83
- at validation; priority belongs to the first occurrence.
84
- - A module name present in multiple roots must have identical metadata and
85
- body bytes. Identical copies resolve to the highest-priority root; any
86
- difference is an error naming both paths. A layer cannot silently replace
87
- a module from the primary root or an earlier layer.
88
- - A module provided only by a layer root must carry the `contrib_` name
89
- prefix. Non-prefixed layer-only modules fail closed with a diagnostic
90
- naming the layer.
91
- - Module bodies load from the root that provided the metadata; cross-root
92
- dependency discovery works in both directions (layer modules may require
93
- primary modules), and deterministic `sort -u` + topological ordering is
94
- preserved across roots.
95
- - The duplicate-export fail-closed check is global across roots: a layer
96
- module may not re-export an identifier already declared by a loaded module.
97
- - No loader-private state leaks after multi-root loads.
115
+ ## Layers
116
+
117
+ A project composes provider modules by vendoring the provider's tree as
118
+ `layers/<provider>/` (its `module.d/` and bodies). Every module in a layer
119
+ uses the `contrib_` prefix, and the `valise_` prefix belongs to the engine.
120
+ Dependencies resolve by name: the same source first, then the only module
121
+ with that name; a name defined in several sources must be written as
122
+ `name@source` where it is used as a root or an exclusion. A built release
123
+ copies the layer modules it loads to `layers/<provider>/`, and one manifest
124
+ covers the project, layer, and engine files together.
125
+
126
+ To update a provider, copy a reviewed revision into `layers/<provider>/`,
127
+ review the diff, and rebuild; record the provider revision beside it if you
128
+ need to know later which one you have. The manifest proves that the
129
+ installed files match the release, not who wrote them.
130
+
131
+ ### The development loader's search path
132
+
133
+ The POSIX loader in `lib/loader.sh`, used by `entrypoints/profile.sh` from an
134
+ engine checkout, resolves modules across a priority-ordered root list
135
+ instead: the primary root first, then the colon-separated
136
+ `VALISE_MODULE_PATH`. Its rules, enforced by
137
+ `tests/loader/multi-root.test.sh`:
138
+
139
+ - Every element must be a non-empty, readable, searchable directory. An
140
+ empty element, a missing directory, a newline, or any of the glob
141
+ characters `[`, `?`, and `*` is rejected. Duplicate elements are
142
+ deduplicated; the first occurrence keeps its priority.
143
+ - A module present in several roots must have identical metadata and body
144
+ bytes. Identical copies resolve to the highest-priority root; any
145
+ difference is an error naming both paths.
146
+ - A module provided only by a layer root must carry the `contrib_` prefix.
147
+ - Bodies load from the root that provided the metadata. Dependencies resolve
148
+ across roots in both directions, and the duplicate-export check is global.
98
149
 
99
150
  ## Versioning
100
151
 
@@ -102,10 +153,11 @@ Shared modules carry an optional `version` field so independent releases
102
153
  of the same name can be told apart. The value must start with a digit
103
154
  and contain only digits, letters, `.`, `_`, `-`; an empty value means
104
155
  unversioned. Providers should bump the version for a breaking change.
105
- Consumers vendor a specific provider snapshot; the loader does not resolve
106
- version ranges. See [Cross-repo composition](#cross-repo-composition).
156
+ Consumers vendor a specific provider snapshot; nothing resolves version
157
+ ranges. See [Layers](#layers).
107
158
 
108
- Enforcement, in both the POSIX loader and the native Fish loader:
159
+ Enforcement, where one name has several providers (the development loader's
160
+ roots, or the Fish loader's providers):
109
161
 
110
162
  - The same name with different valid versions across roots fails closed
111
163
  with a version-skew diagnostic naming both versions and both paths,
@@ -119,32 +171,6 @@ A versioned record that disagrees with itself is not a version at all:
119
171
  malformed versions are rejected by `sp_metadata_read` before any body
120
172
  is sourced.
121
173
 
122
- ## Cross-repo composition
123
-
124
- The supported composition is a primary consumer root plus one
125
- version-pinned layer root per provider.
126
- The provider tree is vendored under `layer/<name>/` of the release
127
- (preserving its internal `module.d/` + `modules/` layout), and a single
128
- manifest covers the whole closure, so `verify-active` checks consumer
129
- and provider bytes together before anything is sourced.
130
-
131
- - Pinning: record the provider source and revision beside the vendored
132
- bytes (the e2e example writes `layer/shared/layer-pin.txt` from
133
- `git rev-parse HEAD` at vendor time). Re-pinning means copying the
134
- provider at a new revision, reviewing the diff, and rebuilding the
135
- manifest. Pin by git SHA and review the diff; the seal proves the
136
- release matches its manifest, not who authored it.
137
- - Loading: the consumer sets `VALISE_MODULE_ROOT` to the release and
138
- `VALISE_MODULE_PATH` to the layer directory inside it. Deployed login
139
- snippets carry only the primary root; the layer path is a consumer
140
- setting applied when sourcing the entrypoint.
141
- - Naming: layer-only modules must carry the `contrib_` prefix, and
142
- same-name modules across roots must be byte-identical or the load
143
- fails closed naming both paths. Dependencies resolve across the
144
- boundary in both directions: layer modules may require primary
145
- modules, and primary modules may follow optional dependencies into
146
- the layer.
147
-
148
174
  ## Runtime detection contract
149
175
 
150
176
  The loader classifies the running shell and platform before matching module
@@ -181,6 +207,7 @@ Supported `side_effect` values are:
181
207
  - `update-network` — performs explicit update/check network behavior.
182
208
  - `install-time` — belongs only in installer flows.
183
209
  - `local-secret` — reads local secret references at runtime only.
210
+ - `local-state` — creates or caches state under the XDG state directory.
184
211
  - `work-only` — work/project-specific behavior that must be opt-in.
185
212
 
186
213
  `requires` dependencies must exist and load before the declaring module. Missing
@@ -211,50 +238,100 @@ and `tests/fish-runtime.test.sh` respectively:
211
238
  quarantines them and continues. Unlike the POSIX fallback, quarantined
212
239
  members are not loaded.
213
240
 
214
- `exports` lists public commands or shell functions the module declares. The loader
215
- uses this list for post-load export checks and failure cleanup; project catalogs
216
- and wrapper manifests use it for documentation and registration. An exported name
217
- must be defined by the load hook/body before the module is considered
218
- successfully loaded.
219
-
220
- `updates` lists update registrations declared by the module. Each value is an ABI
221
- identifier that maps one-to-one to a row in the project update manifest. The
222
- project validator rejects missing, duplicate, or unowned registrations. URL,
223
- destination, digest, trust policy, and rollback state belong in that manifest;
224
- module metadata states registration only and does not execute update lifecycle work.
225
-
226
- ## Update channel for shared modules
227
-
228
- Shared modules are vendored snapshots. `valise` does not fetch provider
229
- updates. To update a provider, vendor a reviewed revision and rebuild the
230
- release manifest. Bump `version=` when making an incompatible module change;
231
- the loader rejects conflicting versions of the same module across roots.
232
- The manifest detects changes to installed files but does not authenticate
233
- their author; see [installation.md](installation.md). The `updates=` field
234
- is registration for a consuming project's separate update manifest, not an
235
- engine update mechanism.
241
+ ### Writing a cross-platform profile
242
+
243
+ `requires` means the module cannot work without the dependency, so a
244
+ dependency that does not support the host blocks the module and fails strict
245
+ mode. Use `optional` for platform-specific modules, so one profile works on
246
+ every host:
247
+
248
+ ```text
249
+ name=profile_main
250
+ requires=common_tools
251
+ optional=mac_tools linux_tools
252
+ ```
253
+
254
+ Only the module for the running platform loads. The other is skipped without a
255
+ message, because its absence is expected. The POSIX loader and compiled plans
256
+ follow the same rules.
257
+
258
+ `exports` lists the public functions the module defines. Plans and loaders use
259
+ it for the pre-load conflict check, the post-load check, and failure cleanup.
260
+ An exported name
261
+ must be defined by the module body before the module is considered
262
+ successfully loaded. An export must name a function, so shell reserved words
263
+ (`case`, `do`, `done`, `elif`, `else`, `esac`, `fi`, `for`, `if`, `in`,
264
+ `then`, `until`, `while`, and `time`) are rejected.
265
+
266
+ ### Published commands
267
+
268
+ `commands` names exports that should also run as ordinary commands, from any
269
+ program and not only from an interactive shell. Every entry must also appear in
270
+ `exports`. `executables` names standalone scripts in the module's directory;
271
+ each is published under its file name, which must start with a letter, digit,
272
+ or underscore and contain only letters, digits, `.`, `_`, and `-`. The
273
+ metadata field `updates` was removed in 0.3.0; a metadata file that still sets
274
+ it is rejected.
275
+
276
+ `valise build` turns both into entries under the release's `bin/`:
277
+
278
+ - For a module with `commands`, it compiles `plans/commands/<module>.sh`, a
279
+ plan whose only root is that module, for the module's command shell: `sh`
280
+ when the module supports `posix` or `sh`, otherwise the first of `bash`,
281
+ `zsh`, and `ksh` it lists. Each command gets a launcher `bin/<command>` that
282
+ runs under that shell, loads the plan in strict mode from the release that
283
+ contains it, restores the caller's `VALISE_MODE`, and calls the function
284
+ with the command's arguments. The function's exit status is the command's.
285
+ - Each `executables` path is copied to `bin/<file name>` and keeps its own
286
+ interpreter line.
287
+
288
+ The plan's closure ships in the release even when no shell bundle loads it.
289
+ A Fish-only module cannot declare `commands`; it may declare `executables`.
290
+ Two modules that publish the same command name, from either field, fail the
291
+ build. `valise-release.conf` lists the published names in `commands=`.
292
+ Installation puts a stub for each name on PATH; see
293
+ [installation.md](installation.md#published-commands).
236
294
 
237
295
  ## Side-effect phases
238
296
 
239
297
  The ordinary source phase rejects `generated-file`, `update-network`, and
240
298
  `install-time` modules before sourcing their bodies. `read-only`, `path-env`,
241
- `local-secret`, and `work-only` are permitted by the engine; choosing whether
242
- to load a `work-only` module is the consuming profile's responsibility.
299
+ `local-secret`, `local-state`, and `work-only` are permitted by the engine;
300
+ choosing whether to load a `work-only` module is the consuming profile's
301
+ responsibility.
302
+
303
+ `local-state` is the permitted class for a module that creates a cache or
304
+ directory under the XDG state directory at source time (`mkdir -p` under
305
+ `$XDG_STATE_HOME`). It is the only write class the ordinary source phase
306
+ allows. A module that writes anywhere else must declare `generated-file`,
307
+ which the source phase rejects.
243
308
 
244
309
  The enforcement hook is
245
310
  `sp_metadata_validate_source_side_effect <side_effect>`. The loader calls it
246
- before ordinary source invokes a load path or compatibility load hook.
311
+ before ordinary source invokes a load path.
247
312
 
248
313
  ## Capability layer available to modules
249
314
 
250
- The POSIX entrypoint sources `lib/capabilities.sh` before it calls the loader.
251
- Portable module bodies may therefore depend on `sp_cap_*` helpers without
252
- sourcing OS-specific files themselves. This is the ABI boundary for behavior that
253
- varied in the flat monolith: CPU count, file mtime, temp directories/files, command
254
- lookup, OS detection, canonical path containment, symlink-component rejection,
255
- Homebrew path discovery, secure local-bin permission checks, and background
315
+ Every plan sources the capability prelude (`runtime/capabilities.sh` in a
316
+ release, `lib/capabilities.sh` in the engine) before any module, and the
317
+ development entrypoint does the same before the loader. Module bodies may
318
+ therefore call the `sp_cap_*` helpers without sourcing platform files
319
+ themselves: CPU count, file mtime, temporary directories and files, command
320
+ lookup, OS detection, canonical path containment, symlink-component
321
+ rejection, Homebrew discovery, secure local-bin checks, and background
256
322
  process launch.
257
323
 
324
+ **The helpers stay available after the profile loads.** A compiled plan
325
+ sources the capability prelude and leaves it in place, so a function defined
326
+ by a module can call `sp_cap_os` or any other `sp_cap_*` helper long after
327
+ the plan finished loading. The compiled plan and the loader agree on this;
328
+ neither cleans the helpers up at the end of a load.
329
+
330
+ A module that needs a portable operation should call the `sp_cap_*` helper
331
+ rather than reaching for a platform-specific command directly. That keeps the
332
+ module working on every platform the engine supports, and keeps the
333
+ platform-specific code in one reviewed library.
334
+
258
335
  Mutation paths must fail closed when a capability cannot prove safety. Update
259
336
  destinations must combine canonical containment with symlink-component rejection
260
337
  before writing, cooldown logic must use the mtime helper instead of raw
@@ -265,24 +342,22 @@ Read-only/status paths may degrade only by quietly skipping unsupported details
265
342
  or by returning a clear nonzero status with a diagnostic from the caller.
266
343
 
267
344
  Platform modules under `modules/platform/` are thin wrappers around the library.
268
- They expose stable module dependencies (`platform_posix`, `platform_macos`,
269
- `platform_linux`, and `platform_bsd`) while keeping compatibility logic in one
345
+ They expose stable module dependencies (`valise_platform_posix`, `valise_platform_macos`,
346
+ `valise_platform_linux`, and `valise_platform_bsd`) while keeping compatibility logic in one
270
347
  library.
271
348
 
272
349
  ## Load target and loader mode
273
350
 
274
- For file-backed modules, `load` is the repository-relative shell file sourced by
275
- the loader after metadata validation, dependency sorting, shell/platform matching,
276
- and phase side-effect checks have passed. The ABI has no unload hook: the loader
351
+ For file-backed modules, `load` is the shell file sourced after metadata
352
+ validation, dependency ordering, shell and platform matching, and the
353
+ side-effect checks have passed. The ABI has no unload hook: the loader
277
354
  does not implement module rollback or unload semantics.
278
355
 
279
- Bridge-backed records include `load_hook=_shell_module_<name>` instead of a
280
- `load` path. `load` and `load_hook` are mutually exclusive; at least one is
281
- required. A `load_hook` value must match the same identifier grammar as exported
282
- functions. The bridge hook is invoked only after the same identifier, dependency,
283
- shell/platform, and phase checks that protect file-backed modules.
356
+ A meta module has no `load`; it has `requires` and no body. Nothing is
357
+ sourced for it, but its dependency closure loads and its dependencies'
358
+ export checks still apply.
284
359
 
285
- Module bodies and bridge hooks must treat `_spl_*`, `_sp_loader_*`, and
360
+ Module bodies must treat `_spl_*`, `_sp_loader_*`, and
286
361
  `_SP_LOADER_*` names as loader-private reserved namespaces. Native Fish
287
362
  transactions additionally reserve `_sp_fish_loader_scope_before_*`,
288
363
  `__sp_fish_loader_scope_before_*`, `_sp_fish_loader_scope_after_*`, and
@@ -295,8 +370,14 @@ module and must never use module-controlled private state for recursive cleanup.
295
370
  Modules must also not define shell functions that shadow critical shell control
296
371
  utilities (`command`, `unset`, `readonly`, `return`, or `eval`) or mark those
297
372
  functions readonly. Those names are reserved because the loader must recover
298
- control after sourcing a module; direct definitions in file-backed modules and
299
- inspectable bridge hooks are rejected before the module body or hook is invoked.
373
+ control after sourcing a module; direct definitions in module bodies are
374
+ rejected before the body is invoked.
375
+
376
+ After a body runs, the loader restores the caller's physical directory, logical
377
+ `PWD` and `OLDPWD`, `IFS`, and the shell options it snapshotted. A body that
378
+ makes any of those variables readonly is rejected as caller-state modification:
379
+ the loader fails the module without aborting the caller. Cleanup resolves `rm`
380
+ from a fixed absolute path, so a module that changes `PATH` cannot prevent it.
300
381
 
301
382
  Failure handling is selected once by the loader caller with
302
383
  `VALISE_MODE=strict|permissive`; it is not a per-module metadata promise.
@@ -305,25 +386,11 @@ Permissive mode reports the affected module and continues with unrelated modules
305
386
 
306
387
  ## Path fields
307
388
 
308
- `load` paths are repository-relative implementation paths. Absolute
389
+ `load`, `executables`, and `tests` paths are relative to the module's source
390
+ directory (the project root, `layers/<provider>/`, or the engine). Absolute
309
391
  paths, `..` path segments, spaces, quotes, shell metacharacters, and glob
310
392
  characters are invalid. Reading metadata must not source these paths.
311
393
 
312
- ## Compatibility bridge
313
-
314
- The legacy flat-gist modules expose dependencies by executing `_shell_module_*`
315
- functions with `_SHMOD_GET_DEPS=1`. That behavior is compatibility evidence, not
316
- the target discovery model. The bridge serializes each legacy module into the
317
- same metadata shape with:
318
-
319
- - `name=<legacy-name>`
320
- - `requires=<legacy _SHMOD_DEPS>`
321
- - `load_hook=_shell_module_<legacy-name>`
322
-
323
- The bridge exists to preserve current dependency edges and permissive warning
324
- behavior while the loader migrates to metadata. It must not be used to justify
325
- executing arbitrary new module bodies during discovery.
326
-
327
394
  ## Validation implementation
328
395
 
329
396
  `lib/metadata.sh` provides the current metadata parser and validator:
@@ -332,9 +399,5 @@ executing arbitrary new module bodies during discovery.
332
399
  - `sp_metadata_validate_identifier <identifier>` validates ABI identifiers.
333
400
  - `sp_metadata_validate_source_side_effect <side_effect>` rejects phase-unsafe
334
401
  classes before ordinary startup can source a module body.
335
- - `sp_metadata_legacy_bridge <name> <requires> <side-effect> <exports>` emits a
336
- normalized compatibility record for an existing `_shell_module_*` module. Bridge
337
- records include empty `updates=` unless a later migration task explicitly maps
338
- legacy update registration.
339
402
 
340
403
  The executable contract is `tests/loader/metadata-abi.test.sh`.