diff --git a/.github/agents/zsh-plugin-standard-reviewer.agent.md b/.github/agents/zsh-plugin-standard-reviewer.agent.md index 240562255..fffc8a614 100644 --- a/.github/agents/zsh-plugin-standard-reviewer.agent.md +++ b/.github/agents/zsh-plugin-standard-reviewer.agent.md @@ -30,33 +30,37 @@ Check the plugin entry file and its supporting files: 2. **Entry-path resolution**: verify that the `ZERO`-aware source-path expression is evaluated at the call site and passed into localized work without assigning special parameter `0` or using function-local `${0:h}`. -3. **Plugin registration**: if the plugin uses the optional `Plugins` profile, - verify a unique key and the snapshot needed to restore it. Do not require - manager-specific registration for a portable plugin. Document every - intentional global effect. Cite `zsh/plugin/document-global-state`. +3. **Plugin registration**: if the plugin uses a shared `Plugins` parameter, + report it as non-portable migration debt. Portable code neither requires nor + mutates a shared manager or plugin registry. Do not require manager-specific + registration for a portable plugin. 4. **Autoload path**: verify that a controlled `functions/` directory is added only when the loader has not already handled it and the exact entry is absent. Cite `zsh/security/trust-paths`. -5. **Unload lifecycle**: when unload is part of the plugin contract, verify that - it reverses every owned side effect, restores any `Plugins` key it owns to - its pre-load state, and self-destructs. When cleanup identifies an appended - `fpath` entry as the last exact match, require an invariant against inserting - or reordering an indistinguishable equal entry after it. Cite - `zsh/plugin/restore-state`. -6. **Passive loading**: verify that plugin and completion load paths perform no +5. **Namespace and configuration**: verify one documented portable ASCII + identifier, project-prefixed persistent names, one namespaced `zstyle` + configuration context, and no scattered public configuration parameters. +6. **Unload lifecycle**: verify an idempotent, partial-load-safe unload function + that reverses every owned side effect and self-destructs. It restores a prior + pre-load state only when the installed value is unchanged and preserves + newer user state. +7. **Passive loading**: verify that plugin and completion load paths perform no implicit network activity. Cite `zsh/security/no-passive-network`. -7. **Autoloaded functions**: evaluate function initialization under the +8. **Autoloaded functions**: evaluate function initialization under the canonical `autoload-function` rules. Do not impose a universal option bundle. -8. **Native syntax**: when a Zsh file is intended to parse independently, run: +9. **Runtime proof**: require a clean-process lifecycle test for the declared + load surface, repeated source, partial failure, hostile state, and post-load + user changes. Static analysis does not prove runtime restoration. +10. **Native syntax**: when a Zsh file is intended to parse independently, run: - ```sh - zsh -f -n - ``` +```sh +zsh -f -n +``` - This is native syntax validation only. It is not behavioral validation and - does not prove every system startup source was skipped. Distinguish - native-invalid Zsh from gaps in supplemental tools. +This is native syntax validation only. It is not behavioral validation and +does not prove every system startup source was skipped. Distinguish +native-invalid Zsh from gaps in supplemental tools. Do not add ShellCheck or `shfmt` as Zsh validators. diff --git a/.github/instruction-surfaces.json b/.github/instruction-surfaces.json index e499de3a5..26365dbe0 100644 --- a/.github/instruction-surfaces.json +++ b/.github/instruction-surfaces.json @@ -845,6 +845,24 @@ "review_owner": "z-shell maintainers", "canonical_for": ["branching-model"] }, + { + "id": "decision-0020", + "path": "decisions/0020-adopt-zsh-plugin-standard-2.md", + "kind": "decision", + "authority": "canonical-detail", + "consumers": ["codex", "claude-code", "copilot", "gemini-cli", "human"], + "tasks": [ + "architecture-decision", + "zsh-plugin-creation", + "zsh-plugin-review", + "zsh-plugin-code-change", + "zsh-plugin-template" + ], + "file_patterns": ["**"], + "required": true, + "review_owner": "z-shell maintainers", + "canonical_for": ["zsh-plugin-standard-adoption"] + }, { "id": "zsh-standard-policy", "path": "lib/zsh-standard-policy.json", diff --git a/.github/instructions/code-review-generic.instructions.md b/.github/instructions/code-review-generic.instructions.md index a078122ee..0e127898a 100644 --- a/.github/instructions/code-review-generic.instructions.md +++ b/.github/instructions/code-review-generic.instructions.md @@ -57,7 +57,7 @@ Structure code review feedback with concrete evidence and actionable fixes: ````markdown - **Severity**: [CRITICAL | IMPORTANT | SUGGESTION] -- **Rule / Category**: [e.g., zsh/plugin/restore-state or security/untrusted-eval] +- **Rule / Category**: [e.g., zsh/plugin/exact-lifecycle or security/untrusted-eval] - **Location**: `path/to/file:line` - **Impact**: Explanation of the concrete failure mode or risk. - **Correction**: diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index d3c7026b9..60aedd674 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -42,8 +42,9 @@ definitions come from `decisions/0007-release-publication-flow.md`. `zpmod`. Never cut a `vX.Y.Z` tag from a red commit. - **Class 3, git-consumed:** **validation-only as the required organization gate.** Run existing repository-owned tests and add regression coverage when - behavior changes, but do not impose a release suite or coverage gate. The - baseline remains syntax, compilation, and clean loading. + behavior changes, but do not impose a release suite or coverage gate. A + maintained plugin also proves its Standard 2 load surface and exact lifecycle + contract in a clean process. - **Class 4 — meta:** baseline plus workflow/markdown linting. ## Coverage @@ -57,8 +58,15 @@ Do not add an org-wide coverage number. integration or system tests are also valid when they exercise boundaries that do not fit a unit test. - Test plugins by sourcing them in a clean Zsh session; there is no build step. -- When unload is part of the subject's contract, assert that its unload function - reverses the owned side effects. +- Prime lifecycle observers before the baseline. Compare functions, parameters + and attributes, aliases, options, traps, modules, hooks, widgets, bindings, + styles, `path`, and `fpath` without printing captured values. +- Assert the documented load allowlist, harmless repeated source, cleanup after + partial failure, hostile caller options, non-interactive behavior, and exact + unload restoration. +- Use ownership-aware cleanup assertions: restore the pre-load value only when + the user did not change the installed value, otherwise preserve the user's + newer state. ## Required checks @@ -70,6 +78,10 @@ suite against the exact commit before a release tag is published. For `zi`, ordinary pull requests validate against `next`; the promotion pull request into `main` runs the full stable-branch check set on its exact head SHA. +Organization templates must pin zsh-lint and ZUnit to exact commits belonging +to published releases. Do not use mutable branches, tags, or unreleased pull +request commits as a required organization gate. + ## See also - `decisions/0009-testing-ci-strategy.md` diff --git a/.github/instructions/zsh-plugin-standard.instructions.md b/.github/instructions/zsh-plugin-standard.instructions.md index a978f3505..9d2163bdd 100644 --- a/.github/instructions/zsh-plugin-standard.instructions.md +++ b/.github/instructions/zsh-plugin-standard.instructions.md @@ -18,6 +18,18 @@ version, follow the manual and report the documentation drift. ## Organization requirements +- Treat version 2 as one clean portable contract. Do not preserve an older + namespace, shared registry, configuration parameter, or directory convention + merely as a compatibility path in a refactored plugin. +- Choose and document one portable ASCII project identifier. Derive every + persistent public or private shell-visible name from it, using a leading + underscore for private state and callbacks. +- Use one project-owned `zstyle` context for ordinary public configuration. + Keep project parameters private and do not expose scattered configuration + globals or environment variables as a parallel interface. +- Portable code must neither require nor mutate a shared manager or plugin + registry parameter. Manager integration belongs to an optional, independently + tested profile. - Write Zsh-first code; do not substitute Bash syntax or portability advice for documented Zsh behavior. - Namespace plugin-owned functions, parameters, aliases, hooks, widgets, and @@ -25,21 +37,24 @@ version, follow the manual and report the documentation drift. - Scope option changes with `emulate -L zsh` or save and restore the prior option state when a change must outlive one function call. - Make load-time side effects explicit, minimal, and documented. -- When the plugin declares an unload contract, provide lifecycle cleanup that - reverses plugin-owned side effects, including hooks, functions, parameters, - aliases, widgets, path entries, and temporary resources. +- Provide idempotent lifecycle cleanup that tolerates partial initialization + and reverses plugin-owned side effects, including hooks, functions, + parameters, aliases, widgets, path entries, and temporary resources. Restore + prior state only while the installed value remains unchanged; preserve newer + user state. - Do not perform network activity during plugin load. Network access must be an explicit user action. -- Validate syntax with native Zsh. When unload is part of the contract, exercise - load and unload behavior in a clean Zsh process. +- Validate syntax with native Zsh. Separately exercise the declared load + surface, repeated source, partial failure, hostile caller state, and exact + unload behavior in a clean Zsh process. ## Portable requirements and manager profiles Keep portable plugin requirements separate from optional plugin-manager -profiles. Manager APIs such as Zi metadata, `PMSPEC`, or a manager-maintained -plugin registry may improve integration, but they are not portable Zsh -requirements. Use them only behind an intentional profile or capability guard, -and never present one manager's API as shell semantics. +profiles. Manager APIs such as Zi metadata or `PMSPEC` may improve integration, +but they are not portable Zsh requirements. Use them only behind an intentional +profile or capability guard, never mutate a manager-owned registry from the +portable entrypoint, and never present one manager's API as shell semantics. Zi is the Z-Shell reference manager for examples and testing under `decisions/0002-zi-as-canonical-plugin-manager.md`. This affects defaults, not diff --git a/.github/instructions/zsh-scripting.instructions.md b/.github/instructions/zsh-scripting.instructions.md index c9968a033..9b5812881 100644 --- a/.github/instructions/zsh-scripting.instructions.md +++ b/.github/instructions/zsh-scripting.instructions.md @@ -806,19 +806,60 @@ Plugin and completion load paths perform no implicit network activity. ## Plugin lifecycle and documentation -### `zsh/plugin/document-global-state` +### `zsh/plugin/stable-namespace` - Level: `required` - Profiles: `sourced-library` - Minimum Zsh: `null` - Basis: `organization-policy` - Evidence: `parameters`, `functions` -- Enforcement: `human-review` +- Enforcement: `lint`, `human-review` + +Choose one portable ASCII project identifier. Derive every persistent public +and private shell-visible name from it, with a leading underscore for private +state and callbacks. Do not retain punctuation-only semantic roles or a second +legacy namespace in refactored plugins. + +### `zsh/plugin/coherent-configuration` + +- Level: `required` +- Profiles: `sourced-library` +- Minimum Zsh: `null` +- Basis: `organization-policy` +- Evidence: `parameters` +- Enforcement: `lint`, `human-review` + +Use one namespaced `zstyle` context for ordinary public configuration. Keep +project parameters private and do not expose scattered global parameters or +environment variables as a parallel configuration interface. + +### `zsh/plugin/no-shared-registry` + +- Level: `required` +- Profiles: `sourced-library` +- Minimum Zsh: `null` +- Basis: `organization-policy` +- Evidence: `parameters` +- Enforcement: `lint`, `runtime-test`, `human-review` + +Portable plugin code neither requires nor mutates a shared manager or plugin +registry parameter. Manager-owned registries and capabilities belong to +optional, independently tested profiles. + +### `zsh/plugin/document-load-surface` + +- Level: `required` +- Profiles: `sourced-library` +- Minimum Zsh: `null` +- Basis: `organization-policy` +- Evidence: `parameters`, `functions` +- Enforcement: `runtime-test`, `human-review` -Document every intentional global parameter, hook, widget, alias, function, -option, path, descriptor, and directory effect. +Document and test every intentional persistent function, parameter, hook, +widget, alias, style, option, path, descriptor, module, and directory effect. +Setup-only helpers do not remain after loading. -### `zsh/plugin/restore-state` +### `zsh/plugin/exact-lifecycle` - Level: `required` - Profiles: `sourced-library` @@ -827,8 +868,11 @@ option, path, descriptor, and directory effect. - Evidence: `functions`, `parameters`, `options` - Enforcement: `runtime-test`, `human-review` -When unload is part of the contract, reverse every owned side effect and remove -the unload function. +Provide an idempotent, partial-load-safe unload function that reverses every +owned side effect and removes itself. Restore prior state only while the value +installed by the plugin remains unchanged; preserve newer user state. Prove the +contract in a clean process after observer priming, including repeated source, +hostile caller state, partial failure, and post-load user changes. ### `zsh/documentation/comment-invariants` diff --git a/.github/skills/new-zsh-plugin/SKILL.md b/.github/skills/new-zsh-plugin/SKILL.md index dd00a8024..4a6cd4cbb 100644 --- a/.github/skills/new-zsh-plugin/SKILL.md +++ b/.github/skills/new-zsh-plugin/SKILL.md @@ -26,23 +26,28 @@ semantics. or default to a multi-repository checkout path. - Plugin name in kebab-case, for example `zsh-foo` with entry file `zsh-foo.plugin.zsh`. + - One portable ASCII project identifier, for example `zsh_foo`. This owns + every persistent public and private shell name and the + `:zsh_foo:config` style context. -3. **Create the layout**: +3. **Create the layout**. Create only the authoritative entrypoint initially. + Add each optional directory only when its execution role is required: ``` - / + / .plugin.zsh - functions/ - lib/ - docs/ + lib/ # optional private eager sources + functions/ # optional autoload functions + completions/ # optional native completion functions + bin/ # optional user-invoked executables ``` 4. **Write the entry file** from `templates/plugin.plugin.zsh`, replacing - `__NAME__` (kebab name) and `__FPATH_VAR__` (an upper-snake project-owned - parameter such as `ZSH_FOO_FPATH`). Keep the modelines as the first two lines - verbatim. The first source owns the `fpath` decision; repeated sources must - not reset it. Add manager-specific registration only when the user requests - and identifies that optional profile. + `__IDENTIFIER__` with the ASCII project identifier. Keep the modelines as the + first two lines verbatim. Do not create shared `Plugins` state, scattered + public configuration parameters, or a second legacy namespace. Add + manager-specific behavior only when the user requests and identifies that + optional profile, and keep it outside the portable contract. 5. **Write autoload function bodies**: begin each generated function body with `builtin emulate -L zsh`. Select only the correctness-affecting options that @@ -52,13 +57,12 @@ semantics. 6. **Verify syntax and lifecycle**: - Run `zsh -f -n .plugin.zsh` for native syntax validation under `zsh/validation/native-authority`. - - In an isolated shell with temporary `HOME` and `ZDOTDIR`, source the entry - file, verify its declared load effects, invoke `_plugin_unload`, and - assert post-unload restoration of `fpath`, scaffold parameters, functions, - hooks, aliases, options, and every other declared side effect. - - The scaffold removes the last exact `fpath` match that it appended. Do not - insert or reorder an indistinguishable equal entry after that append - before unloading; Zsh arrays do not retain occurrence identity. + - In an isolated shell with temporary `HOME` and `ZDOTDIR`, prime the ZUnit + lifecycle observer, snapshot the baseline, source the entry file, and + assert the exact documented load allowlist. + - Test repeated source, partial initialization failure, hostile caller + options, non-interactive loading, and post-load user changes. Invoke + `_plugin_unload` and assert ownership-aware restoration. - Remove the temporary directory. `zsh -f` suppresses normal RCS processing, but a system `zshenv` may still execute. @@ -69,10 +73,13 @@ semantics. - Caller-state preservation: `zsh/sourced/preserve-caller-state`. - Autoload body initialization: `zsh/autoload/initialize`. -- Documented plugin effects: `zsh/plugin/document-global-state`. -- Owned-effect cleanup: `zsh/plugin/restore-state`. +- Stable namespace: `zsh/plugin/stable-namespace`. +- Coherent configuration: `zsh/plugin/coherent-configuration`. +- Documented plugin effects: `zsh/plugin/document-load-surface`. +- Owned-effect cleanup: `zsh/plugin/exact-lifecycle`. - Controlled autoload paths: `zsh/security/trust-paths`. Keep the rule rationale in the canonical instruction. The scaffold must reverse every owned side effect and self-destruct; syntax success alone is not a -behavioral result. +behavioral result. Pin zsh-lint and ZUnit only to commits from published +releases when wiring required CI. diff --git a/.github/skills/new-zsh-plugin/templates/plugin.plugin.zsh b/.github/skills/new-zsh-plugin/templates/plugin.plugin.zsh index 97a293c6b..528e86559 100644 --- a/.github/skills/new-zsh-plugin/templates/plugin.plugin.zsh +++ b/.github/skills/new-zsh-plugin/templates/plugin.plugin.zsh @@ -1,52 +1,27 @@ # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et # -# Zsh Plugin Standard -# https://wiki.zshell.dev/community/zsh_plugin_standard#zero-handling +# Zsh Plugin Standard 2 +# https://wiki.zshell.dev/community/zsh_plugin_standard () { builtin emulate -L zsh typeset -r source_path="${${(M)1:#/*}:-$PWD/$1}" - typeset -r plugin_dir=${source_path:h} - typeset -r functions_dir=$plugin_dir/functions + typeset -r plugin_dir=${source_path:a:h} - # https://wiki.zshell.dev/community/zsh_plugin_standard#functions-directory - # Canonical rule: zsh/security/trust-paths - # The first source owns the persistent path decision. Re-sourcing must not - # reset that ownership record. - if (( ! ${+parameters[__FPATH_VAR__]} )); then - typeset -g __FPATH_VAR__=$functions_dir - typeset -gi __FPATH_VAR___ADDED=0 + # Source private eager helpers from "$plugin_dir/lib" only when required. + # Keep setup-only functions local to this loader. Autoloaded functions and + # completions belong in their documented directories and are not sourced. - if (( ${fpath[(Ie)${__FPATH_VAR__}]} == 0 )); then - fpath+=("${__FPATH_VAR__}") - __FPATH_VAR___ADDED=1 - fi - fi + # Define the documented public functions and register only namespaced, + # explicitly owned side effects here. Ordinary public configuration uses + # the ':__IDENTIFIER__:config' zstyle context. - # --- Plugin body ----------------------------------------------------------- - # Source library files or autoload functions here, e.g.: - # source "$plugin_dir/lib/setup.zsh" - # autoload -Uz +X .__NAME__ && .__NAME__ - # Pair every added side effect with its exact cleanup in the unload function. - - # https://wiki.zshell.dev/community/zsh_plugin_standard#unload-function - # Canonical rule: zsh/plugin/restore-state - __NAME___plugin_unload() { + __IDENTIFIER___plugin_unload() { builtin emulate -L zsh - typeset -r functions_dir=${__FPATH_VAR__-} - integer added=${__FPATH_VAR___ADDED:-0} - integer index=0 - - # The scaffold-owned append is the last exact match. Do not insert or - # reorder an indistinguishable equal entry after it before unloading. - if (( added )) && [[ -n $functions_dir ]]; then - index=${fpath[(Ie)$functions_dir]} - (( index )) && fpath[index]=() - fi - - builtin unset __FPATH_VAR__ __FPATH_VAR___ADDED - builtin unfunction __NAME___plugin_unload + # Reverse each owned side effect explicitly. Restore prior state only while + # the installed value remains unchanged, and preserve newer user state. + builtin unfunction __IDENTIFIER___plugin_unload } } "${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" diff --git a/.github/skills/zunit-test/SKILL.md b/.github/skills/zunit-test/SKILL.md index eb9c2f9bc..a9999f913 100644 --- a/.github/skills/zunit-test/SKILL.md +++ b/.github/skills/zunit-test/SKILL.md @@ -22,44 +22,53 @@ Before writing tests: `zsh/test/isolate-environment`. 4. Load the subject the same way production does under `zsh/test/match-production-profile`. -5. When unload is part of the subject's contract, test actual restoration under - `zsh/plugin/restore-state`. +5. Prime the plugin contract observer before the baseline, then test exact + ownership-aware restoration under `zsh/plugin/exact-lifecycle`. `zsh -f` is useful where applicable, but it does not prove every system startup source was skipped; a system `zshenv` may still execute. ## Test file shape -The example below exercises a subject that declares an unload contract and an -optional `Plugins` registration. Omit those parts when the subject declares -neither behavior. +The example below exercises a Standard 2 plugin with one documented public +function and its unload function. ```zsh #!/usr/bin/env zunit -typeset -ga saved_fpath - @setup { - # Runs before each @test; load the plugin as production does. - saved_fpath=("${fpath[@]}") - typeset -gA Plugins - unset 'Plugins[MY_PLUGIN]' + zunit_plugin_contract_prime + zunit_plugin_contract_snapshot before load "../my-plugin.plugin.zsh" + zunit_plugin_contract_snapshot loaded } @teardown { # A lifecycle test can already have invoked the self-destructing function. - if (( ${+functions[my-plugin_plugin_unload]} )); then - my-plugin_plugin_unload + if (( ${+functions[my_plugin_plugin_unload]} )); then + my_plugin_plugin_unload fi } -@test 'unload restores state and self-destructs' { - my-plugin_plugin_unload +@test 'load exposes only the documented surface' { + assert before plugin_load_surface loaded \ + function:my_plugin_action \ + function:my_plugin_plugin_unload +} + +@test 'repeated source is harmless' { + load "../my-plugin.plugin.zsh" + zunit_plugin_contract_snapshot repeated + + assert loaded plugin_restored repeated +} + +@test 'unload restores owned state and self-destructs' { + zunit_plugin_contract_snapshot user_state + my_plugin_plugin_unload + zunit_plugin_contract_snapshot after - assert "${(j:|:)fpath}" same_as "${(j:|:)saved_fpath}" - assert "${+Plugins[MY_PLUGIN]}" equals 0 - assert "${+functions[my-plugin_plugin_unload]}" equals 0 + assert before plugin_unloaded loaded user_state after } @test 'descriptive name of the behavior' { @@ -80,11 +89,12 @@ typeset -ga saved_fpath Cross-reference real examples in `z-shell/zunit:tests/` and `z-shell/zsh-eza:tests/zsh-eza.zunit`. -When unload is part of the subject's contract, add explicit lifecycle tests for -each declared side effect. Assert that unload removes only plugin-owned state, -restores any registered `Plugins` key to its pre-load state, and -self-destructs. Test absent and existing key states when the plugin registers -one. Omit unload-specific fixtures for subjects without that contract. +Add explicit lifecycle tests for each declared side effect. Test partial +initialization failure, hostile caller options, non-interactive loading, and a +post-load user change. Assert that unload removes only plugin-owned state, +restores pre-load state only when still owned, preserves the user's newer state, +and self-destructs. Portable fixtures do not create or mutate a shared +`Plugins` parameter. Declare each intentional negative fixture in repository metadata under `zsh/test/declare-negative-fixtures`. Name the exact fixture and expected @@ -100,15 +110,14 @@ zunit # run the whole suite zunit tests/my-plugin.zunit # run one file ``` -Follow the canonical GitHub Actions policy when wiring CI. Do not copy a mutable -reusable-workflow reference; select an immutable ref only after its owning -rollout has approved and published one. +Follow the canonical GitHub Actions policy when wiring CI. Pin ZUnit only to an +exact commit from a published release. Do not copy a mutable reusable-workflow +reference or an unreleased pull-request commit. ## Conventions -- Pair `@setup` production-equivalent loading with `@teardown` cleanup. When - unload is part of the contract, assert post-unload state rather than only - invoking the unload function. +- Pair `@setup` production-equivalent loading with `@teardown` cleanup. Assert + post-unload state rather than only invoking the unload function. - Keep one behavior per `@test`; name it as a sentence describing the expected behavior. - Keep `.zunit` files under the plugin's `tests/` directory. diff --git a/AGENTS.md b/AGENTS.md index 19e647264..d4fe29196 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ When working in z-shell repositories, optimize for: supplemental parser, linter, or formatter limitations. Report relevant defects during read-only work, but that does not authorize unrelated cleanup. - **Naming:** plugins use `zsh-`, annexes use `z-a-`, modules keep short descriptive names. -- **Plugin authoring:** read the canonical [Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) for plugin creation, code changes, reviews, templates, and documentation. Official Zsh documentation remains authoritative for shell semantics; manager-specific profiles are optional. +- **Plugin authoring:** maintained plugins and new scaffolds follow version 2 of the canonical [Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) as one clean portable contract. Refactored plugins do not retain legacy declaration systems. Official Zsh documentation remains authoritative for shell semantics; manager-specific profiles are optional. - **Canonical plugin manager:** `zi`. See `decisions/0002-zi-as-canonical-plugin-manager.md`. - **Commits and PR titles:** Conventional Commits. See `decisions/0003-conventional-commits.md`. - **Commit trailers:** `Co-authored-by` crediting a real human, including the PR author crediting themselves, is fine. Never credit a bot, AI agent, or automation as a co-author. `z-shell/.github` and `z-shell/zi` enforce this in CI. Other repositories remain author-enforced until their own verified caller is live; do not infer enforcement from organization policy alone. diff --git a/PATTERNS.md b/PATTERNS.md index 655dfbaaa..0eafd4fab 100644 --- a/PATTERNS.md +++ b/PATTERNS.md @@ -59,8 +59,8 @@ observed in at least two listed repositories. Reference: -Relevant canonical rules: `zsh/plugin/document-global-state` and -`zsh/plugin/restore-state`. +Relevant canonical rules: `zsh/plugin/no-shared-registry` and +`zsh/plugin/exact-lifecycle`. ## Guard `fpath` additions @@ -84,7 +84,7 @@ unload-restoration shape has not been observed in at least two listed repositories. Relevant canonical rules: `zsh/security/trust-paths` and -`zsh/plugin/restore-state`. +`zsh/plugin/exact-lifecycle`. ## Mandatory SHA-pinning for GitHub Actions diff --git a/decisions/0020-adopt-zsh-plugin-standard-2.md b/decisions/0020-adopt-zsh-plugin-standard-2.md new file mode 100644 index 000000000..c9a3f854f --- /dev/null +++ b/decisions/0020-adopt-zsh-plugin-standard-2.md @@ -0,0 +1,98 @@ +# 20. Adopt Zsh Plugin Standard 2 as a Clean Portable Contract + +- **Status:** PROPOSED +- **Date:** 2026-08-28 +- **Deciders:** ss-o +- **Supersedes:** None +- **Superseded by:** None + +## Context + +Zsh plugins share one shell namespace and one caller process. Repository-local +conventions for globals, helper functions, configuration, directories, and +cleanup therefore accumulate as ecosystem-wide maintenance cost. A user or +contributor should not need to learn a new declaration system for every plugin. + +The public Zsh Plugin Standard now defines version 2 as a clean portable +contract. The organization must apply that contract consistently without +copying it into a second z-shell-only standard or treating Zi integration as a +portable requirement. + +## Decision + +Adopt version 2 of the canonical public +[Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) +for maintained z-shell plugins and new plugin scaffolds. + +1. The wiki remains the sole owner of the portable interoperability contract. + Organization policy links to it and owns only z-shell adoption, + verification, and migration requirements. +2. Each plugin documents one portable ASCII project identifier and derives + every persistent shell-visible name from it. +3. Public declarative configuration uses one namespaced `zstyle` context. + Project-owned parameters are private runtime state, not parallel public + configuration. +4. Portable plugin code neither requires nor mutates shared manager or plugin + registry parameters. Zi remains the reference manager, but Zi-specific + behavior is an optional, independently tested profile. +5. A maintained plugin provides an idempotent, partial-load-safe unload + function. Cleanup is ownership-aware: it restores a prior value only while + the installed value remains unchanged and preserves newer user state. +6. Plugin repositories distinguish the authoritative entrypoint, private + eagerly sourced `lib/`, autoloaded `functions/`, native `completions/`, and + user-invoked `bin/` roles. Optional directories are omitted when unused. +7. Static analysis verifies structure and namespace rules. A clean-process + runtime suite separately proves the declared load surface, repeated source, + partial failure, hostile caller state, and exact unload behavior. +8. New scaffolds conform immediately. Maintained plugins migrate through + owning issues. Refactored plugins do not retain legacy aliases, duplicate + configuration variables, shared-registry writes, or alternate declaration + systems merely for compatibility. +9. Required CI pins use exact released commits of the organization tools. + Unreleased branches and pull-request commits are not substituted for a + release pin. + +## Consequences + +### Positive + +- Users see a predictable configuration, namespace, layout, and lifecycle. +- Contributors can transfer knowledge between plugin repositories. +- Static diagnostics and runtime lifecycle proof have distinct, testable jobs. +- Zi remains a useful reference integration without narrowing portability. + +### Costs and risks + +- Existing plugins need deliberate breaking migrations. +- Exact cleanup requires more tests than load-only smoke checks. +- Organization enforcement cannot become required until the corresponding + zsh-lint and ZUnit releases exist. +- The wiki and organization surfaces must be reviewed together to prevent + duplicated or contradictory rules. + +## Alternatives considered + +### Add a z-shell profile above a looser portable standard + +Rejected because the desired practices are useful to all plugin authors. A +second organization contract would recreate the inconsistency this decision is +intended to remove. + +### Preserve legacy interfaces indefinitely + +Rejected because the maintained plugins are being refactored and compatibility +layers would retain duplicate state, naming, and configuration systems. + +### Rely on static analysis alone + +Rejected because static source cannot prove runtime restoration, partial-load +cleanup, or preservation of post-load user changes. + +## References + +- [z-shell/.github#557](https://github.com/z-shell/.github/issues/557) +- [Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard) +- `decisions/0002-zi-as-canonical-plugin-manager.md` +- `decisions/0009-testing-ci-strategy.md` +- `.github/instructions/zsh-plugin-standard.instructions.md` +- `.github/instructions/testing.instructions.md` diff --git a/lib/zsh-standard-policy.json b/lib/zsh-standard-policy.json index 6f4b9a85d..279c4d0f5 100644 --- a/lib/zsh-standard-policy.json +++ b/lib/zsh-standard-policy.json @@ -831,16 +831,43 @@ "enforcement": ["runtime-test"] }, { - "id": "zsh/plugin/document-global-state", + "id": "zsh/plugin/stable-namespace", "level": "required", "profiles": ["sourced-library"], "minimum_zsh": null, "basis": "organization-policy", "evidence": ["parameters", "functions"], - "enforcement": ["human-review"] + "enforcement": ["lint", "human-review"] + }, + { + "id": "zsh/plugin/coherent-configuration", + "level": "required", + "profiles": ["sourced-library"], + "minimum_zsh": null, + "basis": "organization-policy", + "evidence": ["parameters"], + "enforcement": ["lint", "human-review"] + }, + { + "id": "zsh/plugin/no-shared-registry", + "level": "required", + "profiles": ["sourced-library"], + "minimum_zsh": null, + "basis": "organization-policy", + "evidence": ["parameters"], + "enforcement": ["lint", "runtime-test", "human-review"] + }, + { + "id": "zsh/plugin/document-load-surface", + "level": "required", + "profiles": ["sourced-library"], + "minimum_zsh": null, + "basis": "organization-policy", + "evidence": ["parameters", "functions"], + "enforcement": ["runtime-test", "human-review"] }, { - "id": "zsh/plugin/restore-state", + "id": "zsh/plugin/exact-lifecycle", "level": "required", "profiles": ["sourced-library"], "minimum_zsh": null, diff --git a/runbooks/new-repository.md b/runbooks/new-repository.md index 649d2b879..330a1c775 100644 --- a/runbooks/new-repository.md +++ b/runbooks/new-repository.md @@ -75,19 +75,22 @@ template cannot express. ```text zsh-.plugin.zsh -functions/ # only when autoloaded functions are needed -lib/ # only when sourced helpers are needed -docs/ # short repository-local usage only +lib/ # optional private, eagerly sourced helpers +functions/ # optional autoload functions +completions/ # optional native completion functions +bin/ # optional user-invoked executables ``` -Follow the entry-point, `ZERO`, namespaced state, ownership-tracked `fpath`, and -unload patterns in `PATTERNS.md` and the +Omit every optional directory the plugin does not need. Follow the entrypoint, +namespaced state, coherent `zstyle` configuration, and exact lifecycle contract +in the [Zsh Plugin Standard](https://wiki.zshell.dev/community/zsh_plugin_standard). Official Zsh documentation remains authoritative for shell semantics. Treat `PMSPEC` and similar manager capabilities as optional profiles rather than -portable requirements. Namespace plugin-owned state, scope option changes, keep -network activity out of the load path, and reverse only plugin-owned side -effects during unload. +portable requirements. Portable code neither requires nor mutates a shared +plugin registry. Namespace plugin-owned state with one documented ASCII +identifier, scope option changes, keep network activity out of the load path, +and reverse only plugin-owned side effects during unload. ### Annex @@ -160,10 +163,14 @@ Before opening the bootstrap pull request: 1. Run `git diff --check`. 2. Parse every workflow YAML file. 3. Run the repository's syntax and smoke checks. -4. Confirm action references are immutable SHAs. -5. Confirm no generic AI orchestration files, secrets, local paths, or generated +4. For plugins, run the released zsh-lint Standard 2 profile and the released + ZUnit lifecycle assertions against repeated source, partial failure, hostile + state, and post-load user changes. +5. Confirm action and reusable-workflow references are immutable SHAs from + published releases where a versioned organization tool is required. +6. Confirm no generic AI orchestration files, secrets, local paths, or generated output were added. -6. Link the tracker issue and leave an `Agent handoff` comment for deferred +7. Link the tracker issue and leave an `Agent handoff` comment for deferred template or release work. ## Deferred scaffold assets diff --git a/scripts/test_validate_zsh_standard_policy.py b/scripts/test_validate_zsh_standard_policy.py index ede757e2f..b855afce0 100644 --- a/scripts/test_validate_zsh_standard_policy.py +++ b/scripts/test_validate_zsh_standard_policy.py @@ -1977,7 +1977,7 @@ def test_repair_2_consumer_parser_outputs_match_frozen_golden(self) -> None: }, "parsed_rules": validator._markdown_rules(instruction), } - self.assertEqual(len(snapshot["rule_blocks"]), 60) + self.assertEqual(len(snapshot["rule_blocks"]), 63) digest = hashlib.sha256( json.dumps( snapshot, @@ -1989,7 +1989,7 @@ def test_repair_2_consumer_parser_outputs_match_frozen_golden(self) -> None: self.assertEqual( digest, - "4a32a4cdbbdb4efec978371f65b8c8389a7ce17e646bac2fceaee72564456b01", + "15d3c2c6f7bcf5192f8fe4beb1b8e324262f0ea5779407b0c0609beadc13151c", ) def test_rejects_list_and_nested_container_rule_headings(self) -> None: @@ -2813,8 +2813,8 @@ def test_rejects_retired_patterns_contract_mutations(self) -> None: "z-shell/z-a-meta-plugins:z-a-meta-plugins.plugin.zsh", ), ( - "zsh/plugin/document-global-state", - "zsh/plugin/restore-state", + "zsh/plugin/no-shared-registry", + "zsh/plugin/exact-lifecycle", ), ), "Guard `fpath` additions": ( @@ -2825,7 +2825,7 @@ def test_rejects_retired_patterns_contract_mutations(self) -> None: ), ( "zsh/security/trust-paths", - "zsh/plugin/restore-state", + "zsh/plugin/exact-lifecycle", ), ), } @@ -3463,223 +3463,81 @@ def test_rendered_plugin_template_restores_lifecycle_state(self) -> None: template_path = ( PUBLIC_ROOT / ".github/skills/new-zsh-plugin/templates/plugin.plugin.zsh" ) - temporary_path: Path with tempfile.TemporaryDirectory() as temporary_directory: - temporary_path = Path(temporary_directory) - plugin_root = temporary_path / "plugin [literal]*? space" - functions_path = plugin_root / "functions" - functions_path.mkdir(parents=True) + plugin_root = Path(temporary_directory) / "plugin [literal]*? space" + plugin_root.mkdir(parents=True) entry_path = plugin_root / "demo.plugin.zsh" - rendered = ( - template_path.read_text(encoding="utf-8") - .replace("__NAME__", "demo") - .replace("__FPATH_VAR__", "DEMO_FPATH") + rendered = template_path.read_text(encoding="utf-8").replace( + "__IDENTIFIER__", + "demo", ) entry_path.write_text(rendered, encoding="utf-8") environment = os.environ.copy() environment.pop("ZERO", None) - try: - syntax = subprocess.run( # nosec B603 - [zsh_path, "-f", "-n", str(entry_path)], - check=False, - capture_output=True, - text=True, - env=environment, - timeout=10, - ) - except subprocess.TimeoutExpired as exc: - self.fail(f"template syntax timed out after {exc.timeout} seconds") - self.assertEqual( - syntax.returncode, - 0, - syntax.stdout + syntax.stderr, + syntax = subprocess.run( # nosec B603 + [zsh_path, "-f", "-n", str(entry_path)], + check=False, + capture_output=True, + text=True, + env=environment, + timeout=10, ) - - common = textwrap.dedent(r""" - check_fpath() { - builtin emulate -L zsh - local actual=${(j:|:)fpath} - local expected=${(j:|:)argv} - [[ $actual == $expected ]] || { - print -u2 -r -- "fpath mismatch: actual=${actual} expected=${expected}" - return 1 - } - } - - check_scaffold_removed() { - (( ! ${+functions[demo_plugin_unload]} )) && - (( ! ${+parameters[DEMO_FPATH]} )) && - (( ! ${+parameters[DEMO_FPATH_ADDED]} )) - } + self.assertEqual(syntax.returncode, 0, syntax.stdout + syntax.stderr) + + lifecycle = textwrap.dedent(r""" + typeset caller_zero=$0 + typeset -ga fpath=( /baseline ) + typeset -gA Plugins=( OTHER caller-other ) + + . "$1" || exit 10 + (( ${+functions[demo_plugin_unload]} )) || exit 11 + [[ ${(j:|:)fpath} == /baseline ]] || exit 12 + [[ ${Plugins[OTHER]} == caller-other ]] || exit 13 + [[ $0 == "$caller_zero" ]] || exit 14 + + . "$1" || exit 20 + (( ${+functions[demo_plugin_unload]} )) || exit 21 + [[ ${(j:|:)fpath} == /baseline ]] || exit 22 + [[ ${Plugins[OTHER]} == caller-other ]] || exit 23 + + demo_plugin_unload || exit 30 + (( ! ${+functions[demo_plugin_unload]} )) || exit 31 + [[ ${(j:|:)fpath} == /baseline ]] || exit 32 + [[ ${Plugins[OTHER]} == caller-other ]] || exit 33 + [[ $0 == "$caller_zero" ]] || exit 34 """) cases = { - "default-native": textwrap.dedent(r""" - typeset -ga fpath=( /baseline ) - typeset -gA Plugins=( OTHER caller-other ) - unset PMSPEC - . "$1" || exit 10 - check_fpath /baseline "$2" || exit 11 - (( DEMO_FPATH_ADDED == 1 )) || exit 13 - demo_plugin_unload || exit 14 - check_fpath /baseline || exit 15 - [[ ${Plugins[OTHER]} == caller-other ]] || exit 17 - check_scaffold_removed || exit 18 - """), - "preexisting-single": textwrap.dedent(r""" - typeset -ga fpath=( "$2" /tail ) - typeset -gA Plugins - . "$1" || exit 20 - (( DEMO_FPATH_ADDED == 0 )) || exit 21 - check_fpath "$2" /tail || exit 22 - demo_plugin_unload || exit 23 - check_fpath "$2" /tail || exit 24 - check_scaffold_removed || exit 25 - """), - "preexisting-duplicates": textwrap.dedent(r""" - typeset -ga fpath=( "$2" /middle "$2" ) - typeset -gA Plugins - . "$1" || exit 30 - (( DEMO_FPATH_ADDED == 0 )) || exit 31 - demo_plugin_unload || exit 32 - check_fpath "$2" /middle "$2" || exit 33 - check_scaffold_removed || exit 34 - """), - "unset-pmspec-no-unset": textwrap.dedent(r""" - setopt no_unset - typeset -ga fpath=( /baseline ) - typeset -gA Plugins - unset PMSPEC - . "$1" || exit 40 - [[ ! -o UNSET ]] || exit 41 - demo_plugin_unload || exit 42 - [[ ! -o UNSET ]] || exit 43 - check_fpath /baseline || exit 44 - check_scaffold_removed || exit 45 - """), - "caller-ksh-arrays": textwrap.dedent(r""" - setopt ksh_arrays - typeset -ga fpath=( "$2" /tail ) - typeset -gA Plugins - . "$1" || exit 50 - [[ -o KSH_ARRAYS ]] || exit 51 - check_fpath "$2" /tail || exit 52 - demo_plugin_unload || exit 53 - [[ -o KSH_ARRAYS ]] || exit 54 - check_fpath "$2" /tail || exit 55 - check_scaffold_removed || exit 56 - """), - "caller-no-function-argzero": textwrap.dedent(r""" - unsetopt function_argzero - typeset caller_zero=$0 - typeset -ga fpath=( /baseline ) - typeset -gA Plugins - . "$1" || exit 60 - [[ ! -o FUNCTION_ARGZERO ]] || exit 61 - [[ $0 == "$caller_zero" ]] || exit 62 - demo_plugin_unload || exit 64 - [[ ! -o FUNCTION_ARGZERO ]] || exit 65 - [[ $0 == "$caller_zero" ]] || exit 66 - check_fpath /baseline || exit 67 - check_scaffold_removed || exit 68 - """), - "repeated-source": textwrap.dedent(r""" - typeset -ga fpath=( /baseline ) - typeset -gA Plugins - . "$1" || exit 70 - (( DEMO_FPATH_ADDED == 1 )) || exit 71 - . "$1" || exit 72 - (( DEMO_FPATH_ADDED == 1 )) || exit 73 - check_fpath /baseline "$2" || exit 74 - demo_plugin_unload || exit 75 - check_fpath /baseline || exit 76 - check_scaffold_removed || exit 78 - """), - "preexisting-plugin-key": textwrap.dedent(r""" - typeset -ga fpath=( /baseline ) - typeset -gA Plugins=( - DEMO 'caller original [literal]*? value' - OTHER caller-other - ) - . "$1" || exit 80 - [[ ${Plugins[DEMO]} == 'caller original [literal]*? value' ]] || - exit 81 - . "$1" || exit 82 - demo_plugin_unload || exit 83 - [[ ${Plugins[DEMO]} == 'caller original [literal]*? value' ]] || - exit 84 - [[ ${Plugins[OTHER]} == caller-other ]] || exit 85 - check_fpath /baseline || exit 86 - check_scaffold_removed || exit 87 - """), - "equal-entry-inserted-before-owned-append": textwrap.dedent(r""" - typeset -ga fpath=( /baseline ) - typeset -gA Plugins - . "$1" || exit 90 - fpath=( "$2" "${fpath[@]}" ) - check_fpath "$2" /baseline "$2" || exit 91 - demo_plugin_unload || exit 92 - check_fpath "$2" /baseline || exit 93 - check_scaffold_removed || exit 94 - """), - "manager-profile-does-not-suppress-portable-path": textwrap.dedent(r""" - typeset -ga fpath=( /baseline ) - typeset -gA Plugins - PMSPEC=f - . "$1" || exit 100 - (( DEMO_FPATH_ADDED == 1 )) || exit 101 - check_fpath /baseline "$2" || exit 102 - demo_plugin_unload || exit 106 - check_fpath /baseline || exit 107 - (( ! ${+Plugins[DEMO]} )) || exit 108 - check_scaffold_removed || exit 109 - """), + "default-native": "", + "caller-no-function-argzero": "unsetopt function_argzero", + "caller-posix-argzero": "setopt posix_argzero", + "caller-ksh-arrays": "setopt ksh_arrays", + "caller-no-unset": "setopt no_unset", + "caller-hostile-globbing": "setopt glob_subst glob_assign", } - for case_name, body in cases.items(): - with self.subTest(case=case_name): - home = temporary_path / f"home-{case_name}" - zdotdir = temporary_path / f"zdot-{case_name}" - home.mkdir() - zdotdir.mkdir() - child_environment = environment.copy() - child_environment.update( - { - "HOME": str(home), - "ZDOTDIR": str(zdotdir), - } + for name, setup in cases.items(): + with self.subTest(case=name): + completed = subprocess.run( # nosec B603 + [ + zsh_path, + "-f", + "-c", + setup + "\n" + lifecycle, + "zsh", + str(entry_path), + ], + check=False, + capture_output=True, + text=True, + env=environment, + timeout=10, ) - try: - completed = subprocess.run( # nosec B603 - [ - zsh_path, - "-f", - "-c", - common + body, - case_name, - str(entry_path), - str(functions_path), - ], - check=False, - capture_output=True, - text=True, - env=child_environment, - timeout=10, - ) - except subprocess.TimeoutExpired as exc: - self.fail( - f"{case_name} timed out after " f"{exc.timeout} seconds" - ) self.assertEqual( completed.returncode, 0, completed.stdout + completed.stderr, ) - self.assertFalse( - temporary_path.exists(), - "TemporaryDirectory must remove the rendered template tree", - ) - def test_consumer_contract_uses_safe_text_reads(self) -> None: root = self.make_fixture() relative_path = ".github/skills/zunit-test/SKILL.md" @@ -3751,8 +3609,8 @@ def test_public_zsh_consumers_defer_to_canonical_standard(self) -> None: "sourced-library", "autoload-function", "isolated", - "invoke `_plugin_unload`", - "assert post-unload restoration", + "Invoke\n `_plugin_unload`", + "assert ownership-aware restoration", ): with self.subTest(new_plugin_contract=fragment): self.assertIn(fragment, new_plugin_skill) @@ -3764,7 +3622,7 @@ def test_public_zsh_consumers_defer_to_canonical_standard(self) -> None: "test-fixture", "zsh/test/isolate-environment", "zsh/test/match-production-profile", - "zsh/plugin/restore-state", + "zsh/plugin/exact-lifecycle", "Declare each intentional negative fixture", ): with self.subTest(zunit_contract=fragment): @@ -3778,11 +3636,9 @@ def test_public_zsh_consumers_defer_to_canonical_standard(self) -> None: self.assertNotIn('\n0="', template) self.assertEqual(template.count("builtin emulate -L zsh"), 2) for fragment in ( - "__FPATH_VAR___ADDED", - "fpath[(Ie)${__FPATH_VAR__}]", - "unfunction __NAME___plugin_unload", - "zsh/security/trust-paths", - "zsh/plugin/restore-state", + "__IDENTIFIER___plugin_unload", + "unfunction __IDENTIFIER___plugin_unload", + ":__IDENTIFIER__:config", ): with self.subTest(template_contract=fragment): self.assertIn(fragment, template) @@ -3839,7 +3695,7 @@ def test_patterns_retire_unsafe_zsh_lifecycle_snippets(self) -> None: ".github/skills/new-zsh-plugin/templates/plugin.plugin.zsh", "not publish a replacement", "zsh/sourced/preserve-caller-state", - "zsh/plugin/restore-state", + "zsh/plugin/exact-lifecycle", "zsh/security/trust-paths", ): with self.subTest(retirement_contract=fragment): @@ -3864,10 +3720,10 @@ def test_lifecycle_harness_is_option_sensitive_bounded_and_zero_neutral( requirements = ( ('environment.pop("ZERO", None)', 1), ("timeout=10", 2), - ('"manager-profile-does-not-suppress-portable-path"', 1), - ("PMSPEC=f", 1), - ("[[ ! -o UNSET ]]", 2), - ("[[ ! -o FUNCTION_ARGZERO ]]", 2), + ('"caller-posix-argzero"', 1), + ('"caller-no-function-argzero"', 1), + ('"caller-hostile-globbing"', 1), + ("[[ ${Plugins[OTHER]} == caller-other ]]", 2), ) for fragment, minimum_count in requirements: with self.subTest(fragment=fragment): @@ -3885,22 +3741,22 @@ def test_zunit_example_guards_and_demonstrates_unload_lifecycle( ) self.assertIn( - "if (( ${+functions[my-plugin_plugin_unload]} )); then", + "if (( ${+functions[my_plugin_plugin_unload]} )); then", text, ) self.assertIn( - "@test 'unload restores state and self-destructs'", + "@test 'unload restores owned state and self-destructs'", text, ) self.assertIn( - 'assert "${(j:|:)fpath}" same_as "${(j:|:)saved_fpath}"', + "assert before plugin_load_surface loaded", text, ) - self.assertIn('assert "${+Plugins[MY_PLUGIN]}" equals 0', text) self.assertIn( - 'assert "${+functions[my-plugin_plugin_unload]}" equals 0', + "assert before plugin_unloaded loaded user_state after", text, ) + self.assertNotIn("typeset -gA Plugins", text) self.assertIn( "one `@setup` and one `@teardown`, each running around every test", text, diff --git a/scripts/validate-zsh-standard-policy.py b/scripts/validate-zsh-standard-policy.py index aeac36a21..e9fc579e1 100644 --- a/scripts/validate-zsh-standard-policy.py +++ b/scripts/validate-zsh-standard-policy.py @@ -50,8 +50,8 @@ "z-shell/z-a-meta-plugins:z-a-meta-plugins.plugin.zsh", ), "rules": ( - "zsh/plugin/document-global-state", - "zsh/plugin/restore-state", + "zsh/plugin/no-shared-registry", + "zsh/plugin/exact-lifecycle", ), }, "Guard `fpath` additions": { @@ -62,7 +62,7 @@ ), "rules": ( "zsh/security/trust-paths", - "zsh/plugin/restore-state", + "zsh/plugin/exact-lifecycle", ), }, } @@ -180,8 +180,11 @@ "zsh/security/no-restricted-shell-sandbox", "zsh/security/trust-paths", "zsh/security/no-passive-network", - "zsh/plugin/document-global-state", - "zsh/plugin/restore-state", + "zsh/plugin/stable-namespace", + "zsh/plugin/coherent-configuration", + "zsh/plugin/no-shared-registry", + "zsh/plugin/document-load-surface", + "zsh/plugin/exact-lifecycle", "zsh/documentation/comment-invariants", "zsh/documentation/track-deferred-work", "zsh/validation/native-authority", diff --git a/templates/readme/zsh-plugin.md b/templates/readme/zsh-plugin.md index 3fe9c2df3..ca9d1041b 100644 --- a/templates/readme/zsh-plugin.md +++ b/templates/readme/zsh-plugin.md @@ -57,6 +57,16 @@ available and the asset must be reviewed manually when output changes. --> - `` available on `PATH` - +## Portable shell contract + +- Project identifier: `` +- Authoritative entrypoint: `.plugin.zsh` +- Public configuration context: `::config` +- Public functions: `` +- Unload function: `_plugin_unload` +- Optional directories: + ## Installation ### Zi @@ -76,12 +86,12 @@ manager-specific APIs as optional profiles rather than portable requirements.> ## Configuration - + -| Name | Type | Default | Effect | +| Style property | Type | Default | Effect | | ------------------ | -------- | ----------- | ------------------- | -| `` | `` | `` | | +| `` | `` | `` | | ## Usage @@ -133,8 +143,11 @@ This project is distributed under the terms in [LICENSE](LICENSE). - [ ] Zi is the first installation path. - [ ] Other manager examples are intentionally supported or verified. - [ ] Manager-specific profiles are distinguished from portable requirements. +- [ ] One portable ASCII identifier owns every persistent shell-visible name. +- [ ] Ordinary public configuration uses one namespaced `zstyle` context. +- [ ] Portable code neither requires nor mutates a shared plugin registry. - [ ] Public settings, aliases, functions, hooks, and defaults are complete. -- [ ] Load failures and unload behavior are documented. +- [ ] Load failures, partial cleanup, and ownership-aware unload behavior are documented. - [ ] Plugin-owned state is namespaced, option changes are scoped, and unload reverses every owned side effect. - [ ] Plugin load performs no network activity.