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.
@@ -19,7 +19,8 @@ Required fields:
19
19
  | `shells` | Space-separated supported shell identifiers. |
20
20
  | `platforms` | Space-separated supported platform identifiers. |
21
21
  | `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. |
22
+ | `load` | Repository-relative path to the module body. |
23
+ | `requires` | Required when the module is a **meta module** (see below). |
23
24
 
24
25
  Optional fields:
25
26
 
@@ -31,6 +32,37 @@ Optional fields:
31
32
  | `updates` | Space-separated update identifiers registered by the consuming project. |
32
33
  | `version` | Optional module version used to reject version skew across roots. |
33
34
 
35
+ ## Meta modules
36
+
37
+ A **meta module** declares dependencies and has no body. It is the natural
38
+ way to write an aggregator: instead of an empty shell file whose only job is
39
+ to name a dependency, the aggregator is metadata alone.
40
+
41
+ A meta module has `requires=` with at least one entry and **no** `load=`.
42
+ The loader and the compiler treat it as a dependency-only
43
+ record: the module itself has nothing to source, but its required closure
44
+ loads normally and in the right order. A meta module must leave `exports=`
45
+ empty; it has no body to define an exported function. Metadata that declares
46
+ an export on a meta module is rejected.
47
+
48
+ ```text
49
+ name=profile_main
50
+ version=1.0.0
51
+ side_effect=read-only
52
+ requires=contrib_path contrib_editor
53
+ optional=
54
+ shells=bash zsh ksh
55
+ platforms=any
56
+ exports=
57
+ updates=
58
+ ```
59
+
60
+ At least one of `load` or `requires` is required: a module with neither a
61
+ body nor dependencies is malformed.
62
+
63
+ `load` and `requires` are **not** mutually exclusive. A module that declares
64
+ both loads its own body *and* its dependencies, in dependency-first order.
65
+
34
66
  ## Identifiers and allowed values
35
67
 
36
68
  Module names, dependency names, and exported shell functions must match
@@ -41,13 +73,22 @@ non-empty list containing only whitespace is malformed. Required `shells` and
41
73
  `platforms` lists must contain at least one parsed value. Empty identifiers,
42
74
  whitespace inside one identifier, path separators, quotes, shell metacharacters,
43
75
  glob characters, and control characters are invalid. Invalid identifiers are
44
- rejected before the loader can build function names or compatibility
45
- bridge hooks.
76
+ rejected before the loader can build function names or source a body.
46
77
 
47
78
  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.
79
+ `shells=sh` means the module body is POSIX-shell syntax, and it is loaded by
80
+ **every POSIX shell** — `sh`, `bash`, `zsh`, `ksh`, and dash — because those
81
+ shells all run POSIX syntax. The engine classifies a running shell as `bash`,
82
+ `zsh`, or `ksh` when it can prove it, and as `sh` otherwise (dash, BusyBox
83
+ ash, and any other POSIX shell); `shells=sh` matches both the `sh` class and
84
+ the specific classes, so a `shells=sh` module loads in all of them. Write
85
+ portable bodies for `shells=sh` and you cover every POSIX shell at once.
86
+
87
+ `fish` is the exception: it selects the native Fish surface under
88
+ `lib/fish/`, and the Fish loader never sources POSIX shell bodies. A module
89
+ whose `shells=` list does not contain `fish` is skipped under Fish rather
90
+ than failed — a directly requested module fails closed in strict mode.
91
+
51
92
  The engine ships a generic Fish adapter at `entrypoints/fish/profile.fish` as
52
93
  the reference implementation of this handshake: it sets
53
94
  `_sp_fish_loader_bootstrap_mode` and `_sp_fish_loader_bootstrap_seeds`, then sources `loader.fish`. The loader
@@ -125,7 +166,7 @@ The supported composition is a primary consumer root plus one
125
166
  version-pinned layer root per provider.
126
167
  The provider tree is vendored under `layer/<name>/` of the release
127
168
  (preserving its internal `module.d/` + `modules/` layout), and a single
128
- manifest covers the whole closure, so `verify-active` checks consumer
169
+ manifest covers the whole closure, so `release verify` checks consumer
129
170
  and provider bytes together before anything is sourced.
130
171
 
131
172
  - Pinning: record the provider source and revision beside the vendored
@@ -181,6 +222,7 @@ Supported `side_effect` values are:
181
222
  - `update-network` — performs explicit update/check network behavior.
182
223
  - `install-time` — belongs only in installer flows.
183
224
  - `local-secret` — reads local secret references at runtime only.
225
+ - `local-state` — creates or caches state under the XDG state directory.
184
226
  - `work-only` — work/project-specific behavior that must be opt-in.
185
227
 
186
228
  `requires` dependencies must exist and load before the declaring module. Missing
@@ -211,11 +253,30 @@ and `tests/fish-runtime.test.sh` respectively:
211
253
  quarantines them and continues. Unlike the POSIX fallback, quarantined
212
254
  members are not loaded.
213
255
 
256
+ ### Writing a cross-platform profile
257
+
258
+ `requires` means the module cannot work without the dependency, so a
259
+ dependency that does not support the host blocks the module and fails strict
260
+ mode. Use `optional` for platform-specific modules, so one profile works on
261
+ every host:
262
+
263
+ ```text
264
+ name=profile_main
265
+ requires=common_tools
266
+ optional=mac_tools linux_tools
267
+ ```
268
+
269
+ Only the module for the running platform loads. The other is skipped without a
270
+ message, because its absence is expected. The POSIX loader and compiled plans
271
+ follow the same rules.
272
+
214
273
  `exports` lists public commands or shell functions the module declares. The loader
215
274
  uses this list for post-load export checks and failure cleanup; project catalogs
216
275
  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.
276
+ must be defined by the module body before the module is considered
277
+ successfully loaded. An export must name a function, so shell reserved words
278
+ (`case`, `do`, `done`, `elif`, `else`, `esac`, `fi`, `for`, `if`, `in`,
279
+ `then`, `until`, `while`, and `time`) are rejected.
219
280
 
220
281
  `updates` lists update registrations declared by the module. Each value is an ABI
221
282
  identifier that maps one-to-one to a row in the project update manifest. The
@@ -238,12 +299,19 @@ engine update mechanism.
238
299
 
239
300
  The ordinary source phase rejects `generated-file`, `update-network`, and
240
301
  `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.
302
+ `local-secret`, `local-state`, and `work-only` are permitted by the engine;
303
+ choosing whether to load a `work-only` module is the consuming profile's
304
+ responsibility.
305
+
306
+ `local-state` is the permitted class for a module that creates a cache or
307
+ directory under the XDG state directory at source time (`mkdir -p` under
308
+ `$XDG_STATE_HOME`). It is the only write class the ordinary source phase
309
+ allows. A module that writes anywhere else must declare `generated-file`,
310
+ which the source phase rejects.
243
311
 
244
312
  The enforcement hook is
245
313
  `sp_metadata_validate_source_side_effect <side_effect>`. The loader calls it
246
- before ordinary source invokes a load path or compatibility load hook.
314
+ before ordinary source invokes a load path.
247
315
 
248
316
  ## Capability layer available to modules
249
317
 
@@ -255,6 +323,17 @@ lookup, OS detection, canonical path containment, symlink-component rejection,
255
323
  Homebrew path discovery, secure local-bin permission checks, and background
256
324
  process launch.
257
325
 
326
+ **The helpers stay available after the profile loads.** A compiled plan
327
+ sources the capability prelude and leaves it in place, so a function defined
328
+ by a module can call `sp_cap_os` or any other `sp_cap_*` helper long after
329
+ the plan finished loading. The compiled plan and the loader agree on this;
330
+ neither cleans the helpers up at the end of a load.
331
+
332
+ A module that needs a portable operation should call the `sp_cap_*` helper
333
+ rather than reaching for a platform-specific command directly. That keeps the
334
+ module working on every platform the engine supports, and keeps the
335
+ platform-specific code in one reviewed library.
336
+
258
337
  Mutation paths must fail closed when a capability cannot prove safety. Update
259
338
  destinations must combine canonical containment with symlink-component rejection
260
339
  before writing, cooldown logic must use the mtime helper instead of raw
@@ -276,13 +355,11 @@ the loader after metadata validation, dependency sorting, shell/platform matchin
276
355
  and phase side-effect checks have passed. The ABI has no unload hook: the loader
277
356
  does not implement module rollback or unload semantics.
278
357
 
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.
358
+ A meta module has no `load`; it has `requires` and no body. Nothing is
359
+ sourced for it, but its dependency closure loads and its dependencies'
360
+ export checks still apply.
284
361
 
285
- Module bodies and bridge hooks must treat `_spl_*`, `_sp_loader_*`, and
362
+ Module bodies must treat `_spl_*`, `_sp_loader_*`, and
286
363
  `_SP_LOADER_*` names as loader-private reserved namespaces. Native Fish
287
364
  transactions additionally reserve `_sp_fish_loader_scope_before_*`,
288
365
  `__sp_fish_loader_scope_before_*`, `_sp_fish_loader_scope_after_*`, and
@@ -295,8 +372,14 @@ module and must never use module-controlled private state for recursive cleanup.
295
372
  Modules must also not define shell functions that shadow critical shell control
296
373
  utilities (`command`, `unset`, `readonly`, `return`, or `eval`) or mark those
297
374
  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.
375
+ control after sourcing a module; direct definitions in module bodies are
376
+ rejected before the body is invoked.
377
+
378
+ After a body runs, the loader restores the caller's physical directory, logical
379
+ `PWD` and `OLDPWD`, `IFS`, and the shell options it snapshotted. A body that
380
+ makes any of those variables readonly is rejected as caller-state modification:
381
+ the loader fails the module without aborting the caller. Cleanup resolves `rm`
382
+ from a fixed absolute path, so a module that changes `PATH` cannot prevent it.
300
383
 
301
384
  Failure handling is selected once by the loader caller with
302
385
  `VALISE_MODE=strict|permissive`; it is not a per-module metadata promise.
@@ -309,21 +392,6 @@ Permissive mode reports the affected module and continues with unrelated modules
309
392
  paths, `..` path segments, spaces, quotes, shell metacharacters, and glob
310
393
  characters are invalid. Reading metadata must not source these paths.
311
394
 
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
395
  ## Validation implementation
328
396
 
329
397
  `lib/metadata.sh` provides the current metadata parser and validator:
@@ -332,9 +400,5 @@ executing arbitrary new module bodies during discovery.
332
400
  - `sp_metadata_validate_identifier <identifier>` validates ABI identifiers.
333
401
  - `sp_metadata_validate_source_side_effect <side_effect>` rejects phase-unsafe
334
402
  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
403
 
340
404
  The executable contract is `tests/loader/metadata-abi.test.sh`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Current status
4
4
 
5
- `valise-sh` is at **0.1.0**. Before 1.0, the CLI, module ABI, and release
5
+ `valise-sh` is at **0.2.0**. Before 1.0, the CLI, module ABI, and release
6
6
  format do not have a backward-compatibility guarantee. Consumers should pin
7
7
  updates, review release notes, and run their profile's tests before upgrading.
8
8
 
@@ -19,26 +19,121 @@ The compatibility contract has three separate parts:
19
19
 
20
20
  A change affecting one of these is a compatibility change even if the other
21
21
  surfaces remain unchanged. Before 1.0, incompatible changes may occur between
22
- minor releases; release notes must identify affected consumers and migration
23
- steps. After 1.0, use semantic versioning: patch for compatible fixes,
22
+ minor releases, and release notes must state what consumers have to change.
23
+ After 1.0, use semantic versioning: patch for compatible fixes,
24
24
  minor for backward-compatible features, and major for incompatible changes.
25
25
 
26
+ ## What changed in 0.2.0
27
+
28
+ `0.1.0` had no external users, so `0.2.0` provides no migration path for its
29
+ CLI or shell integration. The items below describe the 0.2.0 contract; each is
30
+ also recorded in [CHANGELOG.md](../CHANGELOG.md).
31
+
32
+ ### The registry is a generated artifact
33
+
34
+ `shell/release/module-registry.tsv` is generated from the project's metadata
35
+ by `valise registry`, and `valise build` regenerates it while `valise registry
36
+ --check` validates it. A hand-written registry is no longer the source of truth, and a
37
+ project needs no registry file at all to build: it keeps its `.meta` files,
38
+ their bodies, the bundle roots, and the variants. A committed registry is
39
+ accepted as input, but it is overwritten on the next build.
40
+
41
+ The row order is fixed and does not depend on the caller's locale: the
42
+ primary modules of `shell/module.d/` first, then the layer modules of each
43
+ `shell/layers/<provider>/module.d/`, each group in C-locale name order.
44
+
45
+ ### The metadata ABI gained fields and classes
46
+
47
+ - **Meta modules.** Metadata with `requires=` and no `load` is a
48
+ dependency-only aggregator. It loads successfully without a body. `load`
49
+ and `requires` may be combined.
50
+ - **`side_effect=local-state`.** A source-safe class for modules that write
51
+ under the XDG state directory. It loads in the ordinary phase where
52
+ `generated-file` still fails.
53
+ - **`shells=sh` covers every POSIX shell.** A module that declares `shells=sh`
54
+ loads under `sh`, `dash`, `bash`, `zsh`, and `ksh`, and is skipped under
55
+ Fish unless it is requested directly in strict mode.
56
+
57
+ ### Compiled plans follow the loader
58
+
59
+ A compiled plan uses the loader's dependency rules. The required closure is
60
+ the bundle's roots plus their transitive `requires`. A module whose required
61
+ dependency is unavailable, failed, or skipped for the platform does not run.
62
+ Strict mode stops at the first failure in that closure and returns nonzero;
63
+ permissive mode reports each failure, continues with unrelated modules, and
64
+ returns zero. An optional dependency loads first when it is available for the
65
+ shell and variant; its absence never blocks the module that declared it. A
66
+ cross-platform profile declares platform-specific modules with `optional=`.
67
+ A meta module that declares exports is a compile error, and a module that
68
+ fails has its declared exports removed from the function namespace.
69
+
70
+ ### Capabilities are available at runtime
71
+
72
+ The `sp_cap_*` helpers remain sourced and callable after a compiled plan or a
73
+ loader load finishes. A module may call `sp_cap_os` and the other helpers
74
+ from its own functions. A plan that removed them at the end of the load left
75
+ module functions unusable.
76
+
77
+ ### Startup integration is managed
78
+
79
+ `valise install` writes a managed block into `~/.profile` and into each
80
+ further startup file it manages: the existing `~/.bash_profile` or
81
+ `~/.bash_login`, `~/.bashrc` and `~/.zshrc` when that shell is current or the
82
+ file exists, and `~/.zprofile` when zsh is on `PATH` or the file exists. It
83
+ does not create `~/.bash_profile` or `~/.bash_login`, because either would
84
+ stop bash from reading `~/.profile`. A Fish `conf.d` hook is written when
85
+ valise finds a Fish executable in a trusted location. `--no-integration`
86
+ writes no startup file and no Fish hook. `valise apply` uses the same writer
87
+ after building and installing.
88
+
89
+ Each POSIX file receives the block between `# BEGIN valise managed block` and
90
+ `# END valise managed block`. A repeat install replaces the block in place
91
+ and preserves the rest of the file. An unpaired marker is refused and stops
92
+ the install; a file already processed keeps its updated block. A symlinked
93
+ startup file is reported and left untouched.
94
+
95
+ ### Commands
96
+
97
+ The command surface has one command per job. `valise init` takes `posix` or
98
+ `fish`; with no argument it prints the Fish hook when `$SHELL` names fish and
99
+ the POSIX block otherwise. `valise new`, `valise module`, `valise build`,
100
+ `valise apply`, `valise status`, and `valise registry` are new. `valise build`
101
+ builds a release tree, and `valise apply` builds, installs, writes the shell
102
+ integration, and verifies the result.
103
+
104
+ Installed releases are managed by `valise release list|use|prune|verify`.
105
+ `variant list`, `variant use`, and `verify-active` are gone; `variant` now
106
+ refers only to a build variant. `valise doctor` gained `--pre-install` and
107
+ `--no-integration`; it reads the startup files the running shell actually
108
+ reads and exits nonzero when the active release is missing or fails
109
+ verification, or when no startup file carries the block. `valise
110
+ explain-bundles` and `valise layer-lint` no longer use Python.
111
+
26
112
  ## Release checklist
27
113
 
28
114
  Before tagging or publishing a release:
29
115
 
30
116
  1. Update `package.json` and `bin/valise` version together; add a dated entry
31
- to [CHANGELOG.md](../CHANGELOG.md).
117
+ to [CHANGELOG.md](../CHANGELOG.md). The CLI's test suite asserts that the
118
+ two versions match, so a bump that updates one and forgets the other
119
+ fails the build.
32
120
  2. Run `make check-fast`, then run `make VALISE_REQUIRE_TOOLING=1 check`
33
121
  locally or use the GitHub Actions job. Keep the successful strict run as
34
122
  release evidence for every lane, including Fish, zsh, BusyBox, dash, and
35
123
  ksh.
36
124
  3. Run the README isolated quickstart from a clean checkout and verify that
37
- install, `verify-active`, and example module loading work in the temporary
125
+ install, `release verify`, and example module loading work in the temporary
38
126
  home. Confirm cleanup removes the entire demo state.
39
127
  4. Inspect the result of `npm pack --dry-run` and install/test the packed
40
128
  tarball. The CLI's tests currently cover pack contents, installation, and
41
- module loading.
129
+ module loading. The probe in `tests/cli-posix.test.sh` builds a tree from
130
+ the real `files` whitelist, plants build directories at more than one
131
+ depth under `examples/`, and requires the tarball to exclude every one of
132
+ them while keeping the surrounding example sources. `package.json` keeps
133
+ them out with `!examples/**/build/`, because npm applies `files` without
134
+ consulting `.gitignore` and `valise apply` writes build output into
135
+ `examples/profile-project/`. The `**` is load-bearing: a single `*` still
136
+ packs a build directory nested one level deeper.
42
137
  5. Review the changed CLI, module ABI, release manifest, docs, and examples;
43
138
  check `git diff --check` and doc links.
44
139
  6. Publish only through the chosen distribution channel. The repository
@@ -48,3 +143,12 @@ Before tagging or publishing a release:
48
143
 
49
144
  Do not claim a platform or shell has passed unless its runtime lane actually
50
145
  ran. An explicit skip is useful development evidence, not release evidence.
146
+
147
+ ## Test isolation
148
+
149
+ Every suite runs in a quarantined environment: a fresh temporary `HOME`,
150
+ controlled `TMPDIR` and `PATH`, and cleared startup hooks. A suite must also
151
+ pass when it is run directly rather than through `make`, because the Makefile
152
+ pins `TMPDIR=/tmp` and that can hide a defect that a developer's own
153
+ environment exposes. macOS sets `TMPDIR` to a path ending in a slash, so a
154
+ path comparison in a suite must resolve both sides before comparing them.
@@ -1,8 +1,9 @@
1
1
  # Shared modules: governance and index
2
2
 
3
3
  This policy covers the shared modules shipped here and the requirements for
4
- provider repositories. It defines the layer namespace, review expectations,
5
- and how modules are indexed, deprecated, and removed.
4
+ provider repositories. It defines the layer namespace, what belongs in a
5
+ layer, review expectations, and how modules are indexed, deprecated, and
6
+ removed.
6
7
 
7
8
  ## Namespace and ownership
8
9
 
@@ -24,6 +25,121 @@ review responsibility.
24
25
  discover what exists; they read this index to discover what the engine
25
26
  ships.
26
27
 
28
+ ### Use an owner segment
29
+
30
+ The `contrib_` prefix is the only part the tools enforce. Nothing
31
+ mechanically distinguishes one provider from another, so two providers that
32
+ both ship `contrib_path_utils` fail closed against each other with a
33
+ same-name conflict, and neither can be swapped for the other.
34
+
35
+ **Use a provider segment after the prefix: `contrib_<owner>_<thing>`.**
36
+
37
+ ```
38
+ contrib_acme_widget_setup
39
+ contrib_acme_widget_apply
40
+ contrib_globex_path_helpers
41
+ ```
42
+
43
+ This is a recommendation, not a rule the tools check, but it is what keeps
44
+ two providers from colliding. A layer should use one owner segment
45
+ consistently across all of its modules, so a consumer can see at a glance
46
+ which provider owns what.
47
+
48
+ If you must ship a module whose name another provider might already use,
49
+ prefix it with your owner segment anyway — a name collision between two
50
+ vendored providers is otherwise unresolvable without renaming one of them.
51
+
52
+ ## What belongs in a layer
53
+
54
+ A layer is a vendorable set of modules: something another person can take
55
+ unchanged, use, and maintain on their own terms. The practical test is
56
+ simple: **could another person vendor this module unchanged and still be
57
+ happy with it?**
58
+
59
+ A layer module must have:
60
+
61
+ - **No employer material.** No account IDs, hostnames, cloud profiles,
62
+ project names, or anything else tied to one organization.
63
+ - **No personal configuration.** No hard-coded home paths, no chosen
64
+ editor, no particular toolchain, model, or theme.
65
+ - **No dependency outside the layer**, apart from the engine's own modules.
66
+ A layer module may require an engine module, but it may not require a
67
+ module that lives in the consumer's primary root — that dependency cannot
68
+ be satisfied when someone else vendors the layer.
69
+
70
+ Everything else stays in the primary root: environment setup, completions,
71
+ tool wrappers, work-specific modules, and the aggregators.
72
+
73
+ `valise layer-lint <layer-name>` enforces the mechanical half of this. It
74
+ checks that the layer has a `module.d/` directory, that every metadata file
75
+ is a regular file with a safe mode, that every module name carries the
76
+ `contrib_` prefix, that the metadata validates as data, that the version is
77
+ present, that the load target exists as a regular file, and that every
78
+ dependency resolves inside the layer or to an engine module. It needs no
79
+ Python.
80
+
81
+ ### 1. Separate generic engines from opinionated presets
82
+
83
+ A reusable mechanism and the opinionated defaults built on it are different
84
+ modules with different audiences, and they belong in different files.
85
+
86
+ The engine is the part anyone might want. The preset is a list of specific
87
+ choices that only some users want. Ship them as two modules so a consumer
88
+ can take the engine without the presets.
89
+
90
+ ```
91
+ contrib_gitignore_engine # the matching logic
92
+ contrib_gitignore_preset # one opinionated list of paths
93
+ ```
94
+
95
+ A consumer who wants their own path list takes the engine and writes their
96
+ own preset. A consumer who wants the shipped defaults takes both. If the
97
+ engine and the preset were one module, every consumer would be forced to
98
+ either accept the opinionated list or fork the whole thing.
99
+
100
+ ### 2. Avoid grab-bag modules
101
+
102
+ A module's dependencies and `shells=` are the union of its parts. Splitting
103
+ one module into several narrow ones lets each part declare only what it
104
+ actually needs.
105
+
106
+ The failure this avoids is concrete: a helper that used `local` was bundled
107
+ into a general-purpose utilities module, and because `local` is not POSIX,
108
+ that one function excluded ksh for **every** function in the module. The
109
+ module's `shells=` list had to be the union, so one non-portable helper
110
+ cost the whole module its ksh support.
111
+
112
+ Split instead:
113
+
114
+ ```
115
+ contrib_owner_cli_quote # POSIX, no dependencies
116
+ contrib_owner_cli_local # needs a non-POSIX shell
117
+ ```
118
+
119
+ Now the first module keeps `shells=sh bash zsh ksh` and the second declares
120
+ what it actually needs. The same rule applies to dependencies: a module that
121
+ pulls in a heavy dependency should be separate from one that does not.
122
+
123
+ ### 3. Use owner namespaces
124
+
125
+ Covered above: `contrib_<owner>_<thing>`, one segment per provider, used
126
+ consistently.
127
+
128
+ ### 4. Runtime capability use is allowed
129
+
130
+ A layer module **may** call `sp_cap_*` helpers at run time. A compiled plan
131
+ sources the capability prelude and leaves it in place, so a function the
132
+ module defines can call `sp_cap_os`, `sp_cap_cpu_count`, or any other helper
133
+ long after the profile finished loading. The loader behaves the same way.
134
+
135
+ This is a change from earlier engine behavior, where the helpers were
136
+ removed after loading and a module function that called one broke. If you
137
+ have a layer that avoided the helpers to work around that, you can now use
138
+ them directly.
139
+
140
+ Prefer the `sp_cap_*` helpers over open-coding platform-specific commands.
141
+ See [contrib-modules.md](contrib-modules.md) for the list.
142
+
27
143
  ## Index of engine-shipped shared modules
28
144
 
29
145
  | Module | Version | Exports | Source | Status |
@@ -42,6 +158,10 @@ The example provider tree in this repository
42
158
  | `contrib_shared_paths` | 1.0.0 | `contrib_shared_paths_setup` | `examples/shared/modules/shared/contrib_shared_paths.sh` | active |
43
159
  | `contrib_shared_umask` | 1.0.0 | `contrib_shared_umask_secure` | `examples/shared/modules/shared/contrib_shared_umask.sh` | active |
44
160
 
161
+ These engine-shipped modules predate the owner-segment recommendation and
162
+ keep their existing names; renaming them would break every consumer that
163
+ already vendors them. New modules should use the owner-segment form.
164
+
45
165
  Update the index in the same change that adds, renames, deprecates, or
46
166
  removes a shared module. Provider repositories should maintain a corresponding
47
167
  index for their own layer modules.
@@ -53,6 +173,8 @@ Every change to a shared module passes the same floor:
53
173
  1. The engine gate: `make check-fast` during iteration and
54
174
  `make VALISE_REQUIRE_TOOLING=1 check` before a release claim (strict
55
175
  tooling is automatic when CI sets `CI=true` or `GITHUB_ACTIONS=true`).
176
+ These targets live in the source repository; the npm package ships the
177
+ runtime, the docs, and the examples, not the Makefile.
56
178
  2. The authoring checklist in
57
179
  [contrib-modules.md](contrib-modules.md): metadata validates, the
58
180
  module loads strict from a clean environment, behavior is idempotent