weft-command 2.1.0__tar.gz
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.
- weft_command-2.1.0/.gitignore +78 -0
- weft_command-2.1.0/LICENSE +21 -0
- weft_command-2.1.0/NOTICE +24 -0
- weft_command-2.1.0/PKG-INFO +9 -0
- weft_command-2.1.0/pyproject.toml +20 -0
- weft_command-2.1.0/src/weft_command/__init__.py +43 -0
- weft_command-2.1.0/src/weft_command/contract.py +221 -0
- weft_command-2.1.0/src/weft_command/permission.py +44 -0
- weft_command-2.1.0/src/weft_command/py.typed +0 -0
- weft_command-2.1.0/src/weft_command/render.py +67 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# The donor checkout, reachable for reading and lifting only.
|
|
2
|
+
# Never a build input: see CLAUDE.md and docs/README.md.
|
|
3
|
+
/donor
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
.venv/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
|
|
13
|
+
# Tooling
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.pyright/
|
|
17
|
+
|
|
18
|
+
# `uv.lock` was here, filed between two caches. It is not a cache: `ci.yml` runs
|
|
19
|
+
# `uv sync --frozen` in three of its four jobs, and every one of them died at that step
|
|
20
|
+
# the first time CI ever ran. It is tracked now — the resolution CI installs and the
|
|
21
|
+
# resolution a developer installs must be one artefact, which is the same argument
|
|
22
|
+
# `docs/README.md` makes about single-sourcing anything two readers can disagree about.
|
|
23
|
+
#
|
|
24
|
+
# It constrains nobody downstream. A lockfile is not a dependency bound; what a consumer
|
|
25
|
+
# resolves is decided by each distribution's `pyproject.toml`.
|
|
26
|
+
|
|
27
|
+
# Session-local attempt counter for the guard_quality_gates.py PreToolUse hook — per-session
|
|
28
|
+
# scratch, not a record anyone should read later. See the hook's module docstring.
|
|
29
|
+
.claude/.gate-attempts.json
|
|
30
|
+
|
|
31
|
+
# Secrets. `.env` holds live provider keys; only the documented, valueless
|
|
32
|
+
# example is tracked. Listed before any tooling rule so a stray `git add -A`
|
|
33
|
+
# cannot reach it.
|
|
34
|
+
.env
|
|
35
|
+
.env.*
|
|
36
|
+
!.env.example
|
|
37
|
+
|
|
38
|
+
# Corpus payload. Every subdirectory of /corpus is a materialised document set and
|
|
39
|
+
# is deliberately untracked; `corpus/manifest.toml` and `scripts/fetch_corpus.py`
|
|
40
|
+
# are the tracked artefact. `09` §4 V1 permits exactly this:
|
|
41
|
+
# a corpus is "either redistributable or fetched by a pinned, checksummed script",
|
|
42
|
+
# and the mRMR papers are published under publisher copyright, so committing them
|
|
43
|
+
# would be redistribution this repository has no right to perform. The manifest
|
|
44
|
+
# carries a sha256 per document, which is what makes the set reproducible without
|
|
45
|
+
# the bytes being here. The pattern is a directory glob rather than a list of names
|
|
46
|
+
# so that scaling the corpus up cannot silently start tracking a paper.
|
|
47
|
+
/corpus/*/
|
|
48
|
+
|
|
49
|
+
# Where a baseline run stages the corpus it indexes and writes the `weft.toml` it measures
|
|
50
|
+
# through (`eval/run_baseline.py`). The staged copies are the same untracked papers one
|
|
51
|
+
# directory over, and the configuration is reproduced by the harness rather than kept — what
|
|
52
|
+
# is tracked is the run it produced, under `eval/baselines/`.
|
|
53
|
+
/.baseline-run/
|
|
54
|
+
|
|
55
|
+
# Working artefacts of a build session — a generated map of the codebase and a design
|
|
56
|
+
# record produced while planning. Untracked on purpose: `docs/README.md` routes every
|
|
57
|
+
# document this project owns, and a design that matters belongs in the `docs/` file
|
|
58
|
+
# that owns its content, not in a root-level file nothing points at.
|
|
59
|
+
/.phase2-*.md
|
|
60
|
+
/.phase3-*.md
|
|
61
|
+
/.phase4-*.md
|
|
62
|
+
|
|
63
|
+
# Gate-session preparation: the Bring lists of `docs/05-grilling-sessions.md`, measured on the day
|
|
64
|
+
# a session is about to run. Untracked for the same reason — the session's outcome belongs in
|
|
65
|
+
# `docs/README.md`'s decision log and in the reference document the decision changes, never here.
|
|
66
|
+
/.gate-brief-*.md
|
|
67
|
+
|
|
68
|
+
# Claude Code local state
|
|
69
|
+
.claude/settings.local.json
|
|
70
|
+
|
|
71
|
+
.DS_Store
|
|
72
|
+
|
|
73
|
+
# The build harness driving Phase 2, alongside its brief, findings and design record.
|
|
74
|
+
/.phase2-build.js
|
|
75
|
+
|
|
76
|
+
# Transient: harvested subagent findings, promoted into docs/lessons.md by implement-ll.
|
|
77
|
+
# Never committed — its content belongs in the queue or nowhere.
|
|
78
|
+
.claude/lessons-spool.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Krysztopa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Weft
|
|
2
|
+
Copyright (c) 2026 Adam Krysztopa
|
|
3
|
+
|
|
4
|
+
This product is licensed under the MIT License. See the LICENSE file at the root
|
|
5
|
+
of this repository.
|
|
6
|
+
|
|
7
|
+
--------------------------------------------------------------------------------
|
|
8
|
+
Original work
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
**Weft contains no source text from any other codebase.** Every line here is
|
|
12
|
+
written for this project, against this project's contracts.
|
|
13
|
+
|
|
14
|
+
This is a rule, not a description of the current state: no file may be copied or
|
|
15
|
+
adapted from another project's source, and no third-party source text may be
|
|
16
|
+
pasted into this repository. Where a prior system informed a design, what was
|
|
17
|
+
carried across is understanding — an approach, an ordering, a measurement, a
|
|
18
|
+
reason a guard exists — restated in this project's own words and implemented
|
|
19
|
+
fresh. Copyright does not reach any of that, and nothing in this repository
|
|
20
|
+
depends on a licence granted by anyone else.
|
|
21
|
+
|
|
22
|
+
`docs/04-donor-inventory.md` records what was learned from prior work and what
|
|
23
|
+
was deliberately not taken. It is a design record. Nothing in it authorises a
|
|
24
|
+
copy, because copying is not permitted here at all.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: weft-command
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: First-party command pack. Publishes the Command contract and PermissionClass.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: weft-kernel<1.0.0,>=0.1.0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "weft-command"
|
|
3
|
+
version = "2.1.0"
|
|
4
|
+
description = "First-party command pack. Publishes the Command contract and PermissionClass."
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
8
|
+
dependencies = ["weft-kernel>=0.1.0,<1.0.0"]
|
|
9
|
+
|
|
10
|
+
# No `[project.entry-points."weft.packs"]` block — the identical reasoning
|
|
11
|
+
# `packages/weft-prompts/pyproject.toml` already carries: fitness function 2 asserts that
|
|
12
|
+
# every distribution declaring this entry point is active *and contributing*, and this
|
|
13
|
+
# distribution registers no `Command` of its own. `weft-cli`'s own built-in commands
|
|
14
|
+
# (`weft index`, `weft ask`, ...) stay on `weft_cli.cli.COMMANDS` until a later task moves
|
|
15
|
+
# them onto this contract; the first plugin-contributed `Command` adds the entry-point line
|
|
16
|
+
# in the same commit it lands in, exactly as `weft-retrieve` did at 2.13.
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""First-party command pack. Publishes the `Command` contract and `PermissionClass`.
|
|
2
|
+
|
|
3
|
+
Task **3.1**: "a pack registers a command exactly as it registers a retriever, and a command
|
|
4
|
+
that declares no permission class fails to register while its author is standing there."
|
|
5
|
+
`weft_command.contract`'s own module docstring carries the full reasoning for why this
|
|
6
|
+
distribution exists at all — in particular why `Command` lives here rather than in
|
|
7
|
+
`weft-kernel` (G1: the kernel names no capability) or `weft-cli` (a pack implementing a
|
|
8
|
+
contract must not depend on the driving adapter that calls it).
|
|
9
|
+
|
|
10
|
+
**Registers nothing, and declares no `weft.packs` entry point**, on `packages/weft-prompts`'s
|
|
11
|
+
own precedent (see that distribution's module docstring for the identical reasoning): fitness
|
|
12
|
+
function 2 requires every distribution declaring that entry point to be active *and
|
|
13
|
+
contributing*, and every first-party command belongs to the pack whose behaviour it exposes —
|
|
14
|
+
none of which is this one's to own. `weft_cli.commands` carries every built-in command since
|
|
15
|
+
task 3.2, registered through `weft-cli`'s own entry point (see that module's docstring for why
|
|
16
|
+
`weft-cli` is the owner rather than a twelfth distribution); a third party's first
|
|
17
|
+
plugin-contributed `Command` adds the entry-point line to *its own* `pyproject.toml` in the same
|
|
18
|
+
commit it registers, exactly as `weft-retrieve` did at task 2.13.
|
|
19
|
+
|
|
20
|
+
There is therefore no `Settings` model and no `register` function here — a pack declaring no
|
|
21
|
+
entry point is never handed either, and declaring them unused would be two more things to keep
|
|
22
|
+
true. `packages/weft-generate` and `packages/weft-prompts` take the same shape for the same
|
|
23
|
+
reason.
|
|
24
|
+
|
|
25
|
+
**Task 6.20 (G13) adds `ExitCode` and `Rendered` to this namespace** — `weft_command.render`'s
|
|
26
|
+
own module docstring carries the reasoning in full: a renderer a third-party pack registers
|
|
27
|
+
through `weft_kernel.discovery.PackRegistrar.add_renderer` has to answer with a `Rendered`
|
|
28
|
+
carrying an `ExitCode`, and a pack implementing `Command` must not depend on `weft-cli`, the
|
|
29
|
+
driving adapter, for the vocabulary its own renderer speaks.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from weft_command.contract import COMMAND_CONTRACT_VERSION, Command, CommandResult
|
|
33
|
+
from weft_command.permission import PermissionClass
|
|
34
|
+
from weft_command.render import ExitCode, Rendered
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"COMMAND_CONTRACT_VERSION",
|
|
38
|
+
"Command",
|
|
39
|
+
"CommandResult",
|
|
40
|
+
"ExitCode",
|
|
41
|
+
"PermissionClass",
|
|
42
|
+
"Rendered",
|
|
43
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""The `Command` contract — published here, never by the kernel and never by `weft-cli`.
|
|
2
|
+
|
|
3
|
+
Task **3.1**: "a pack registers a command exactly as it registers a retriever, and a command
|
|
4
|
+
that declares no permission class fails to register while its author is standing there"
|
|
5
|
+
(`docs/build-ledger.md`). `docs/03-cli.md` → *Plugin-contributed commands*: "A pack registers
|
|
6
|
+
commands against the `Command` contract exactly as it registers a retriever."
|
|
7
|
+
|
|
8
|
+
**Why this distribution, and not `weft-kernel` or `weft-cli` — an architectural decision this
|
|
9
|
+
task took, which no prior document had settled.**
|
|
10
|
+
|
|
11
|
+
- **Not `weft-kernel`.** G1 draws the line exactly: the kernel expresses, loads and runs
|
|
12
|
+
contracts it knows nothing about, and publishes none of its own (`docs/02-extension-model.md`
|
|
13
|
+
§1 → *Who publishes a contract*). `Command` names a capability — "a CLI-invoked action" — the
|
|
14
|
+
same way `Extractor`, `Chunker` and `Prompt` do, and CLAUDE.md states the test plainly: "if
|
|
15
|
+
you cannot describe the kernel without naming a capability, it is too big."
|
|
16
|
+
- **Not `weft-cli`.** `weft-cli` is `docs/03-cli.md`'s **driving adapter** — "every operation it
|
|
17
|
+
performs is a library call a FastAPI route could make identically" — and a pack that
|
|
18
|
+
contributes a command is a **driven** side of that same call, exactly as a pack contributing a
|
|
19
|
+
retriever is. Publishing `Command` from `weft-cli` would mean a third-party command pack
|
|
20
|
+
depending on the adapter to implement the contract the adapter itself drives, which inverts
|
|
21
|
+
the dependency direction the whole extension model is organised around (`docs/02` §1: "a pack
|
|
22
|
+
depends on the pack that publishes the contract it implements, exactly as it would on any
|
|
23
|
+
third-party protocol" — never on the thing that *calls* it). Concretely, `weft-cli`'s own
|
|
24
|
+
`pyproject.toml` already names nine other distributions as dependencies
|
|
25
|
+
(`weft-kernel`, `weft-extract`, `weft-chunk`, `weft-embed`, `weft-store`, `weft-llm`,
|
|
26
|
+
`weft-prompts`, `weft-retrieve`, `weft-generate`); a command pack that had to depend on
|
|
27
|
+
`weft-cli` to get `Command` would inherit every one of those transitively for the sole purpose
|
|
28
|
+
of reading one Protocol.
|
|
29
|
+
- **The precedent is `weft-prompts`.** A distribution that publishes a contract and registers no
|
|
30
|
+
plugin of its own is not a new shape — `weft_prompts.contract.Prompt` lived exactly this way
|
|
31
|
+
from task 2.10 until the first prompt landed and added the entry-point line. `weft-command`
|
|
32
|
+
takes the identical position: it publishes `Command` and `PermissionClass`
|
|
33
|
+
(`weft_command.permission`), declares no `[project.entry-points."weft.packs"]` block (fitness
|
|
34
|
+
function 2 requires every distribution declaring that entry point to be active *and
|
|
35
|
+
contributing*, and this one contributes nothing of its own), and `weft-cli` gains it as a
|
|
36
|
+
ninth-turned-tenth dependency the same way it already depends on `weft-prompts` for `Prompt`.
|
|
37
|
+
|
|
38
|
+
**`Command` is not a pipeline position**, on `weft_prompts.contract.Prompt`'s own footing and
|
|
39
|
+
for the identical reason: `.phase2-design.md` §3 groups `Prompt` and `LLMProvider` as "named and
|
|
40
|
+
registered, but not pipeline positions", and a CLI command is invoked once per `weft` invocation
|
|
41
|
+
(or once per REPL turn), never composed into an ingest or query `StageSpec` list. It therefore
|
|
42
|
+
declares no `Stage` base — `weft_kernel.runner.Runner.resolve` has no reason to see it, and
|
|
43
|
+
`weft_cli.contract_reference.capability_siblings`' own `_is_stage_protocol` filter, built for
|
|
44
|
+
exactly this family of "one method, several unrelated contracts sharing its name", never needs a
|
|
45
|
+
special case for it.
|
|
46
|
+
|
|
47
|
+
**`args_model` and `result_model` are declared in the Protocol body, deliberately** — the
|
|
48
|
+
identical choice `weft_prompts.contract.Prompt` makes for `input_model`/`output_model`, and for
|
|
49
|
+
the same reason stated there: a command whose arguments are an untyped `argparse.Namespace` is
|
|
50
|
+
the shape this contract exists to replace, and a renderer needs `result_model` to introspect a
|
|
51
|
+
result's shape without having been written against any one command by name. Required for
|
|
52
|
+
`isinstance` is the cost, stated once so nobody rediscovers it: `issubclass` against a Protocol
|
|
53
|
+
with non-method members raises `TypeError`, so a future walk over registered `Command`s must use
|
|
54
|
+
`isinstance` against a constructed instance, never `issubclass` against the class.
|
|
55
|
+
|
|
56
|
+
**`run` takes `args` at the call, not at construction — `Prompt.render`'s shape, not `Stage`'s.**
|
|
57
|
+
A `Stage` plugin binds its `with:` configuration once, at construction, because one pipeline
|
|
58
|
+
position runs many payloads through the identical configuration. A `Command` is the opposite: the
|
|
59
|
+
same registered instance answers a different, freshly parsed `args` on every invocation — one
|
|
60
|
+
shot from the CLI, or turn after turn in the REPL (`docs/03-cli.md` → *Two modes, one
|
|
61
|
+
implementation*: "Both go through the same command objects.") — so binding `args` at construction
|
|
62
|
+
would mean constructing a fresh instance per invocation for no reason `Lifetime.PROCESS` could
|
|
63
|
+
ever amortise.
|
|
64
|
+
|
|
65
|
+
**The governing property this task builds into the signature, per `docs/03-cli.md` → *Two modes,
|
|
66
|
+
one implementation*:** "a `Command` returns a typed result and never writes to a stream." `run`
|
|
67
|
+
returns a decided `Outcome[CommandResult]` — `Produced` / `NothingToProduce` / `Failed`, `02` §1's
|
|
68
|
+
own vocabulary applied here as it is everywhere else a contract answers — never a printed line and
|
|
69
|
+
never a bare value. `weft_cli` renders `Produced.value` for a human, `--json` renders the
|
|
70
|
+
identical value for a script, and the REPL renders it again with the session's context around
|
|
71
|
+
it; none of that is buildable if this method had already chosen a renderer by printing.
|
|
72
|
+
|
|
73
|
+
**`permission_class` is mandatory and declared nowhere in this file** — the same asymmetry
|
|
74
|
+
`weft_chunk.contract.Chunker` has with `destroys`, and for the identical reason. `docs/03-cli.md`
|
|
75
|
+
→ *Permissions*: "The class is a `ClassVar` on the `Command` contract, read at registration
|
|
76
|
+
alongside `lifetime`, `requires` and `provides`; a command that declares none fails to register,
|
|
77
|
+
loudly, while its author is standing right there. No default is safe." Declaring
|
|
78
|
+
`permission_class` directly in the Protocol body would make it a required `isinstance` member,
|
|
79
|
+
checked only once an *instance* exists — too late, since refusing at registration means refusing
|
|
80
|
+
before any instance is ever constructed. `Command.required_declarations = ("permission_class",)`
|
|
81
|
+
is instead read by `weft_kernel.registry`'s generalised mandatory-declaration check (task 3.1's
|
|
82
|
+
own repair to that module — see its docstring, *"`destroys` generalised into
|
|
83
|
+
`required_declarations`, not duplicated"*), the identical mechanism `Chunker.
|
|
84
|
+
publishes_property_vocabulary` already drives for `destroys`, applied to a second name without
|
|
85
|
+
the kernel ever learning the word `Command` or `permission`.
|
|
86
|
+
|
|
87
|
+
**What this task deliberately leaves to 3.2 and 3.4.** Namespacing a pack's commands under its
|
|
88
|
+
own name (`docs/03-cli.md`: "`weft graph build`") is a registration-*name* convention, not a
|
|
89
|
+
contract shape — `registrar.add(Command, "graph build", GraphBuildCommand)` registers under
|
|
90
|
+
`weft_kernel.registry` exactly as any other string name would, and turning that string into
|
|
91
|
+
argparse subcommand wiring, generated `--help` text and REPL completion is task 3.2's own
|
|
92
|
+
("`weft --help` cannot drift from what is installed, because core has no command list to edit").
|
|
93
|
+
Likewise, rewiring `weft_cli.cli.COMMANDS`'s five built-in commands onto this contract, and
|
|
94
|
+
building the renderer that actually turns a `CommandResult` into text/JSON/REPL output, are 3.4's
|
|
95
|
+
("the interactive session and the one-shot invocation are the same commands with a different
|
|
96
|
+
implementation") — `weft_cli.permissions.CliCommand` was expected to stay untouched a task
|
|
97
|
+
longer than it did: task 3.2 found the 3.2/3.4 split stated above too narrow (converting the
|
|
98
|
+
built-ins requires exactly the parser-generation and a minimal renderer it names, neither the
|
|
99
|
+
REPL nor the plural `--json`/REPL renderers 3.4 owns) and rewired all five onto this contract
|
|
100
|
+
immediately, deleting `CliCommand` and `weft_cli.permissions` as dead code — see
|
|
101
|
+
`weft_cli.commands`'s own module docstring for the full account.
|
|
102
|
+
|
|
103
|
+
**Task 3.2 adds `help` — the one piece of metadata 3.1 left this contract without, and the one a
|
|
104
|
+
generated `--help` cannot do without.** `docs/03-cli.md` → *Plugin-contributed commands*: "Core has
|
|
105
|
+
no list of commands to edit. The help text is generated from the registry" — which needs a
|
|
106
|
+
one-line string to generate *from*, and nothing on `Command` as 3.1 shipped it carried one.
|
|
107
|
+
`required_declarations` grows to `("permission_class", "help")` rather than gaining a second,
|
|
108
|
+
parallel mechanism: `help` is exactly as mandatory as `permission_class` and for the identical
|
|
109
|
+
reason — a command with no help text is not a smaller problem than one with no permission class,
|
|
110
|
+
it is `weft --help` printing a blank line or crashing outright for whichever plugin forgot it,
|
|
111
|
+
discovered by a stranger long after the author who could have been told at registration is gone.
|
|
112
|
+
Declared under `if TYPE_CHECKING:`, exactly like `version` and `required_declarations` themselves
|
|
113
|
+
— never a required `isinstance` member, for the same reason `permission_class` is not one: refusing
|
|
114
|
+
at registration means refusing before any instance exists to check, and `hasattr` on the class is
|
|
115
|
+
the only test available at that point.
|
|
116
|
+
|
|
117
|
+
**`COMMAND_CONTRACT_VERSION` was recorded as `"1.1.0"` here, and that was a mis-recorded major —
|
|
118
|
+
corrected to `"2.0.0"` at task **5.2a**, per `docs/README.md` → G9's settled two-audience rule
|
|
119
|
+
(`docs/09-release.md` §2.3).** Adding `help` to `required_declarations` is a minor change for a
|
|
120
|
+
*caller* — nothing that only ever reads a `Command` through this Protocol changes — but a **major**
|
|
121
|
+
change for an *implementer*: every existing `Command` that does not already declare `help` breaks
|
|
122
|
+
at registration, loudly, the instant this contract is depended on at the corrected range. G9's
|
|
123
|
+
table classifies exactly this row ("add a name to `required_declarations`") as `—` / **major** /
|
|
124
|
+
**major**, and the bump a Protocol change takes is the maximum of the two audiences, never the
|
|
125
|
+
caller's alone. At the time this docstring first recorded `"1.1.0"`, G9 had not yet settled that
|
|
126
|
+
rule, so the bump was chosen from the caller's side only; it is corrected here rather than left
|
|
127
|
+
mis-recorded once the rule existed to correct it against — the identical honesty G9's own settled
|
|
128
|
+
row applies to itself.
|
|
129
|
+
|
|
130
|
+
**Task 6.20 (G13) bumps `COMMAND_CONTRACT_VERSION` again, to `"2.1.0"` — this time minor, not
|
|
131
|
+
major, and the reasoning is stated at the time the bump is taken rather than left for a later
|
|
132
|
+
task to reconstruct.** `weft_command.render.Rendered` and `.ExitCode` are published from this
|
|
133
|
+
distribution and re-exported here (see that module's own docstring for why a renderer's
|
|
134
|
+
vocabulary belongs beside the contract that produces the result it formats), and
|
|
135
|
+
`weft_kernel.discovery.PackRegistrar` gains a further registration seam, `add_renderer` — but
|
|
136
|
+
nothing on the `Command` Protocol itself changed, `required_declarations` is untouched, and a
|
|
137
|
+
pack that registers no renderer still works exactly as it did before this task, because the
|
|
138
|
+
unregistered-result structured-dump fallback is the floor and stays the floor. Applying G9's
|
|
139
|
+
table honestly: additive for a *caller* (nothing that reads `Command` through this Protocol
|
|
140
|
+
changes) and additive for an *implementer* (no existing `Command` implementation breaks at
|
|
141
|
+
registration), and the maximum of the two is minor.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
|
|
145
|
+
|
|
146
|
+
from pydantic import BaseModel, ConfigDict
|
|
147
|
+
|
|
148
|
+
from weft_command.render import ExitCode as ExitCode
|
|
149
|
+
from weft_command.render import Rendered as Rendered
|
|
150
|
+
from weft_kernel.context import Context
|
|
151
|
+
from weft_kernel.payload import Outcome
|
|
152
|
+
|
|
153
|
+
#: Fitness function 6's subject for this contract — see the module docstring's note on the
|
|
154
|
+
#: correction at task 5.2a: the 3.2 bump to "1.1.0" (when `help` joined `required_declarations`)
|
|
155
|
+
#: was a mis-recorded major, per G9's two-audience rule, corrected to "2.0.0".
|
|
156
|
+
#:
|
|
157
|
+
#: **Task 6.20 bumps this to "2.1.0" — additive for both audiences, so minor, not major.**
|
|
158
|
+
#: `docs/09-release.md` §2.3, G9's two-audience rule, applied at the time this bump is taken
|
|
159
|
+
#: rather than left for a later task to reconstruct: nothing on the `Command` Protocol itself
|
|
160
|
+
#: changed, `required_declarations` is untouched, and a pack that registers no renderer through
|
|
161
|
+
#: the new `PackRegistrar.add_renderer` seam still works exactly as before — the unregistered-
|
|
162
|
+
#: result structured-dump fallback is the floor, and stays the floor. Publishing `Rendered` and
|
|
163
|
+
#: `ExitCode` from this module (see `weft_command.render`) grows the surface without breaking
|
|
164
|
+
#: anything either audience already depended on, which is what "minor" means under G9's table:
|
|
165
|
+
#: the maximum of "additive for a caller" and "additive for an implementer" is still additive.
|
|
166
|
+
COMMAND_CONTRACT_VERSION = "2.1.0"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class CommandResult(BaseModel):
|
|
170
|
+
"""The typed result a `Command` returns — never printed text, per the module docstring.
|
|
171
|
+
|
|
172
|
+
Frozen per CLAUDE.md ("Frozen where the value is a domain object"), and the base every
|
|
173
|
+
concrete command's own result model subclasses with its own fields, the same relationship
|
|
174
|
+
`weft_kernel.payload.outcome`'s `Produced[T]` has to whatever `T` a contract names: a
|
|
175
|
+
renderer built against this base type can accept any command's result without having been
|
|
176
|
+
written against that command by name, which is what lets `weft_cli` render a plugin's
|
|
177
|
+
command exactly as it renders a built-in one. Carries no fields of its own — a bare
|
|
178
|
+
`CommandResult()` states nothing, and every real command adds what it actually produced.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@runtime_checkable
|
|
185
|
+
class Command(Protocol):
|
|
186
|
+
"""One CLI-invoked action a pack contributes, registered exactly as it registers a retriever.
|
|
187
|
+
|
|
188
|
+
Not a pipeline position — see the module docstring's *"`Command` is not a pipeline
|
|
189
|
+
position"* paragraph. `run` accepts the parsed, already-validated arguments and returns a
|
|
190
|
+
decided `Outcome[CommandResult]`, never printed text — see the module docstring's paragraph
|
|
191
|
+
on *"The governing property this task builds into the signature."*
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
if TYPE_CHECKING:
|
|
195
|
+
#: See `weft_extract.contract`'s module docstring for the full `__protocol_attrs__`
|
|
196
|
+
#: reasoning behind the `if TYPE_CHECKING:` / assign-after-the-class-body split, which
|
|
197
|
+
#: applies here unchanged for both of these: neither may become a required `isinstance`
|
|
198
|
+
#: member, or a structurally-conforming `Command` that never restates them would fail a
|
|
199
|
+
#: capability check that has nothing to do with capability.
|
|
200
|
+
version: ClassVar[str]
|
|
201
|
+
#: See the module docstring's paragraph on `permission_class` — this names the class
|
|
202
|
+
#: attribute every implementation must carry; it does not carry the value itself.
|
|
203
|
+
required_declarations: ClassVar[tuple[str, ...]]
|
|
204
|
+
#: The one-line summary `weft --help` prints beside this command's name — see the
|
|
205
|
+
#: module docstring's *"Task 3.2 adds `help`"* paragraph. Mandatory via
|
|
206
|
+
#: `required_declarations`, not a required `isinstance` member, for the identical
|
|
207
|
+
#: reason `permission_class` is not one.
|
|
208
|
+
help: ClassVar[str]
|
|
209
|
+
|
|
210
|
+
#: The model `run` accepts. Required for `isinstance` — see the module docstring's
|
|
211
|
+
#: `args_model`/`result_model` paragraph.
|
|
212
|
+
args_model: ClassVar[type[BaseModel]]
|
|
213
|
+
#: The `CommandResult` subclass `run` produces on success. Required for `isinstance`, for
|
|
214
|
+
#: the identical reason `args_model` is.
|
|
215
|
+
result_model: ClassVar[type[CommandResult]]
|
|
216
|
+
|
|
217
|
+
async def run(self, args: BaseModel, ctx: Context) -> Outcome[CommandResult]: ...
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
Command.version = COMMAND_CONTRACT_VERSION
|
|
221
|
+
Command.required_declarations = ("permission_class", "help")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""`PermissionClass` — the five-member, closed vocabulary `docs/03-cli.md` → *Permissions* fixes.
|
|
2
|
+
|
|
3
|
+
Task **3.1** moves this out of `weft_cli.permissions`, where it lived as a stand-in until the
|
|
4
|
+
`Command` contract itself existed (`weft_cli.permissions`'s own former docstring: "the `Command`
|
|
5
|
+
*contract* ... is Phase 3's ... gated on G8"). G8 settled 2026-08-18 and this distribution is
|
|
6
|
+
that contract's home — see `weft_command.contract`'s module docstring for why `Command` lives
|
|
7
|
+
here rather than in `weft-kernel` or `weft-cli`. `PermissionClass` moves with it because a
|
|
8
|
+
permission class is meaningless without the contract that reads it: `Command.required_declarations`
|
|
9
|
+
names `permission_class` as mandatory (`weft_kernel.registry`'s generalised check, task 3.1), and
|
|
10
|
+
the vocabulary that field's values are drawn from belongs beside the contract, not beside one
|
|
11
|
+
particular renderer of it.
|
|
12
|
+
|
|
13
|
+
**`weft_cli.permissions` — the re-export this class briefly had — is gone as of task 3.2.** It
|
|
14
|
+
existed only to keep `weft_cli.cli.COMMANDS`'s hand-written table importing one name from two
|
|
15
|
+
places during the 3.1→3.2 transition; 3.2 deleted `COMMANDS` itself (every built-in command is
|
|
16
|
+
now a `weft_command.contract.Command` plugin in `weft_cli.commands`, which imports
|
|
17
|
+
`PermissionClass` directly from here, on the same footing any third-party command pack does), so
|
|
18
|
+
the stand-in had no remaining caller and staying would have been the exact kind of drift-prone
|
|
19
|
+
duplicate name this contract's own placement exists to avoid.
|
|
20
|
+
|
|
21
|
+
`Enum`, never `Literal[...]`, per CLAUDE.md. The five members are fixed by `docs/03-cli.md`'s own
|
|
22
|
+
table — `read`/`write` default to allow, `overwrite`/`destroy` default to ask, `network` defaults
|
|
23
|
+
to allow but stays configurable — and defaults are policy a renderer or `weft.toml` enforces,
|
|
24
|
+
not a fact this enum carries; see `docs/03-cli.md` → *Permissions* for the table in full.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from enum import StrEnum
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PermissionClass(StrEnum):
|
|
31
|
+
"""The five classes `docs/03-cli.md` → *Permissions* names.
|
|
32
|
+
|
|
33
|
+
**No default anywhere a `Command` implementation reads this from.** A plugin-contributed
|
|
34
|
+
command must declare exactly one of these — `Command.required_declarations` makes silence a
|
|
35
|
+
registration refusal, never a fallback to `read` — because `read` silently under-protects and
|
|
36
|
+
`destroy` trains an operator to pass `--yes` reflexively, disarming the whole table
|
|
37
|
+
(`docs/03-cli.md` → *Permissions*).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
READ = "read"
|
|
41
|
+
WRITE = "write"
|
|
42
|
+
OVERWRITE = "overwrite"
|
|
43
|
+
DESTROY = "destroy"
|
|
44
|
+
NETWORK = "network"
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""`ExitCode` and `Rendered` — a renderer's own vocabulary, published beside the `Command`
|
|
2
|
+
contract that produces the result a renderer formats.
|
|
3
|
+
|
|
4
|
+
Task **6.20**, G13's third repair (`docs/03-cli.md` → *Plugin-contributed commands*): "a
|
|
5
|
+
result type nobody outside the CLI can format is only half a contract." Both classes moved
|
|
6
|
+
here from `weft_cli` — `ExitCode` out of `weft_cli.exit_codes`, `Rendered` out of
|
|
7
|
+
`weft_cli.render` — because a renderer registered by a third-party pack (`weft_kernel.
|
|
8
|
+
discovery.PackRegistrar.add_renderer`) has to build one of these to answer with, and a pack
|
|
9
|
+
implementing `Command` must not depend on `weft-cli`, the driving adapter that calls it, for
|
|
10
|
+
the vocabulary its own renderer speaks — the identical reasoning `weft_command.contract`'s
|
|
11
|
+
own module docstring already gives for why `Command` itself lives here rather than in
|
|
12
|
+
`weft-cli`.
|
|
13
|
+
|
|
14
|
+
**`ExitCode` travels with `Rendered` rather than staying behind, and that is a deliberate
|
|
15
|
+
choice, not a convenience.** A renderer that cannot say the run failed is a renderer a
|
|
16
|
+
built-in could not have used: `weft delete` and `weft index` both compute their own exit
|
|
17
|
+
code from their own result's fields (a partial deletion, or an index run with failures, is
|
|
18
|
+
success at the process level and failure at the operation level — see `weft_cli.render`'s
|
|
19
|
+
own `_render_delete`/`_render_index`), so a third-party renderer needs the identical
|
|
20
|
+
five-value vocabulary to state the same kind of fact about its own result. `weft_command.
|
|
21
|
+
permission.PermissionClass` is the standing precedent for a `docs/03-cli.md` concept living
|
|
22
|
+
in this pack rather than in `weft-cli`: this pack already publishes a vocabulary that
|
|
23
|
+
document defines, because the contract is unusable without it.
|
|
24
|
+
|
|
25
|
+
**`CommandResult` still names no exit code — what moved is the *renderer's* vocabulary, not
|
|
26
|
+
the result's.** `weft_cli.render`'s retired docstring argued exactly this, and the argument
|
|
27
|
+
still holds unchanged now that `ExitCode` and `Rendered` live here instead: `CommandResult`
|
|
28
|
+
is capability-agnostic, and `IndexCommandResult.summary.failed > 0` still has to become exit
|
|
29
|
+
code `1` somewhere — that "somewhere" is still whichever renderer a pack registers through
|
|
30
|
+
`PackRegistrar.add_renderer`, computed from the typed fields a command actually returned,
|
|
31
|
+
never smuggled into the result itself.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from enum import IntEnum
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExitCode(IntEnum):
|
|
41
|
+
"""`docs/03-cli.md` → *Output*: "0 success, 1 operation failed, 2 bad usage, 3 refused for
|
|
42
|
+
permissions, 4 pipeline failed to resolve." `2` is never assigned by a renderer — `argparse`
|
|
43
|
+
itself calls `sys.exit(2)` on a usage error, before any command runs, so nothing downstream
|
|
44
|
+
of a `Command`'s own `run` ever needs to produce it.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
SUCCESS = 0
|
|
48
|
+
OPERATION_FAILED = 1
|
|
49
|
+
BAD_USAGE = 2
|
|
50
|
+
POLICY_REFUSED = 3
|
|
51
|
+
RESOLUTION_FAILED = 4
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class Rendered:
|
|
56
|
+
"""What a renderer answers with — stdout, stderr, and the exit code the process should
|
|
57
|
+
report. `None` on either stream means nothing prints on it.
|
|
58
|
+
|
|
59
|
+
Every renderer registered through `weft_kernel.discovery.PackRegistrar.add_renderer`
|
|
60
|
+
returns one of these, whether it is one of `weft_cli`'s own eighteen built-in renderers
|
|
61
|
+
or a third party's — a `Rendered` built by a stranger's pack is indistinguishable from
|
|
62
|
+
one `weft_cli.render` built itself, which is the property task 6.20 exists to make true.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
stdout: str | None
|
|
66
|
+
stderr: str | None
|
|
67
|
+
exit_code: ExitCode
|