weft-kernel 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1181 @@
1
+ """Entry-point discovery and the trust model — G3 in code.
2
+
3
+ Specified in `docs/06-phase-0-build.md` step 5 and `docs/02-extension-model.md`
4
+ section 2 ("Packs and discovery", *The trust model*). The threat G3 settled on
5
+ is **installed-and-ambient, not malicious**: entry points turn *installed*
6
+ into *executed with the application's privileges*, and the only defensible
7
+ posture is to state that plainly rather than simulate a control CPython
8
+ cannot enforce. Two consequences follow directly into this module's shape.
9
+
10
+ **Discovery is eager, and refusal precedes it.** `discover()` enumerates every
11
+ distribution that declares a `weft.packs` entry point and, for each one still
12
+ permitted, imports it and calls its `register()` — because bare plugin names
13
+ (`docs/02` section 3) mean nothing can answer "who provides this?" without
14
+ running `register()` to find out. A pack outside an active `[packs] allow`
15
+ pin is never imported at all: the check happens before `entry_point.load()`
16
+ is ever called, not after, because "refused" that still executed is not
17
+ refusal. This is fitness function 8(a), the property the canary in
18
+ `testing/weft-canary/` exists to prove — see `tests/architecture/`.
19
+
20
+ **One status vocabulary answers "why is this pack not contributing?" for
21
+ every reason at once** — refused, failed, partially registered, or simply not
22
+ installed — because a refused pack and a pack that lost half its
23
+ registrations to a missing optional dependency are the same question asked
24
+ twice. `PARTIAL` is part of that vocabulary now because the vocabulary is one
25
+ piece; the *mechanism* that produces it is G4's conditional registration, a
26
+ later step's job.
27
+
28
+ **Two more pieces of the trust model attach here because there was nowhere
29
+ else for them to attach.** `DISCLOSURE` is a module-level, optional, purely
30
+ informational value the kernel reads immediately after import and before
31
+ `register()` runs — never a claim weft checks, never a permission it grants.
32
+ A `DISCLOSURE` that is present but not a `Disclosure` instance is not the
33
+ same fact as no `DISCLOSURE` at all — the pack tried to say something and
34
+ said it wrong — so that case is `FAILED`, naming the pack and what
35
+ the attribute must be, rather than silently read as "not disclosed."
36
+ Pack settings (`docs/02`'s `packs:` block, keyed by **pack** name — the
37
+ `weft.packs` entry-point name, so `[packs.store]`, never `[packs.store]`)
38
+ are validated against the pack's own Pydantic model *before* `register()` is
39
+ called, because a pack author's `register(registry, settings)` should never
40
+ have to guard against malformed input the kernel could have rejected first —
41
+ and `${env:VAR}` interpolation happens on that settings data here, so no pack
42
+ ever reads `os.environ` itself for a secret. A `packs:` key naming a
43
+ pack nothing installed declares is the strict half of that same
44
+ model: `docs/02` states the asymmetry exactly — "`packs:` expresses a
45
+ requirement, `allow` expresses a permission" — so once every entry point is
46
+ enumerated, any settings key no entry point claims raises, naming every pack
47
+ that did declare a `weft.packs` entry point, rather than being read and
48
+ quietly discarded.
49
+
50
+ **`pack` and `distribution` are two different identities and this module is
51
+ where they part.** A pack is what an entry point names; a distribution is what
52
+ an index ships. They were the same string while every pack had a wheel of its
53
+ own, and stopped being it when one wheel started shipping fourteen. Everything
54
+ an operator points at a *pack* keys on the entry-point name — the rows `weft
55
+ plugins list|doctor` prints, `[packs.<pack>]` settings, and the pack named in a
56
+ settings or disclosure failure. Everything about *provenance* keys on the
57
+ distribution — `[packs] allow`, the version column, and version skew. `docs/
58
+ 02-extension-model.md` §2 owns the split.
59
+
60
+ **Registration is transactional per pack.** `PackRegistrar.add` buffers
61
+ every call instead of writing through to the `Registry` immediately; nothing
62
+ lands there until `register()` returns without raising, at which point the
63
+ whole buffer commits in one atomic step (`Registry.add_many`) or none of it
64
+ does. A pack that raises partway through therefore contributes exactly the
65
+ `0` its `FAILED` report claims — the report and the registry can never
66
+ disagree about how much of a half-finished pack is actually live.
67
+
68
+ **`PackRegistrar` exists because attribution is not a pack author's to supply, and
69
+ attribution is never a pack author's to supply** — `registry.py`'s own
70
+ docstring names this exact gap: "A pack's own `register()` calls the
71
+ seam-bound surface with `(contract, name, factory)`." This is that surface:
72
+ `Registry.add` minus its keyword-only `distribution` parameter, bound once
73
+ per pack by whatever called `discover()`, so a pack author writes
74
+ `registrar.add(Contract, "name", factory)` and never sees attribution at all.
75
+
76
+ **Task 5.2g — `add_ext_model` closes the gap `docs/02-extension-model.md` §1's
77
+ Phase 0 step 8 note left open: "a pack's `register()` does not contribute one
78
+ automatically."** Buffered exactly like `add_pipeline_resource` and
79
+ `deprecate` above, for the identical structural reason — a pack whose
80
+ `register()` raises partway through must not leave a namespace claimed that
81
+ was never actually committed. `weft_kernel.payload.ext.ExtModel` is a payload
82
+ primitive the kernel already owns (`Node.ext`'s own declared value type), not
83
+ a capability, so buffering a `type[ExtModel]` here teaches the kernel nothing
84
+ about stores: it is inert data until something that *does* know what a store
85
+ is reads `PackReport.ext_models` back off every report and registers each
86
+ class with the namespace-to-class registry that actually rehydrates one —
87
+ `weft_store.rehydrate.register_from_reports`, called once, generically, by
88
+ whatever already calls `discover()` (`weft_cli.registry_bootstrap.
89
+ build_dependencies`). No pack-specific knowledge sits in that call site: it
90
+ walks whatever `PackReport.ext_models` any report carries, so a future pack
91
+ shipping a new `ExtModel` needs no edit here and no edit in `weft-cli` at all.
92
+
93
+ **Task 5.3a (`S8`) — `add_contribution` closes the identical gap for `02` §3 →
94
+ *Slots*: a pack could describe a stage contribution as a `weft_kernel.resolution.
95
+ Contribution` value, but nothing let a pack's own `register()` hand one to anything.
96
+ Buffered exactly like `add_ext_model` just above, for the identical structural
97
+ reason — a pack whose `register()` raises partway through must not leave a slot
98
+ looking filled that was never actually committed. `Contribution` is a pipeline-model
99
+ value the kernel already owns (`weft_kernel.resolution` is this same distribution),
100
+ not a capability, so this teaches the kernel nothing new either: it stops at "this
101
+ pack offered this contribution," and `PackReport.contributions` is read back off
102
+ every report by whatever assembles a `resolve()` call's own `contributions=` tuple —
103
+ `weft_cli.registry_bootstrap.build_dependencies`, the same caller `Contribution`'s
104
+ own docstring names, now real. No pack-specific knowledge sits there either: it
105
+ concatenates whatever `PackReport.contributions` every report carries, so a future
106
+ pack contributing into a slot needs no edit here and no edit in `weft-cli` at all.
107
+
108
+ **Task 6.20 (G13) — `add_renderer` closes the analogous gap for a `Command` result:**
109
+ `docs/03-cli.md` → *Plugin-contributed commands*, "a result type nobody outside the CLI
110
+ can format is only half a contract." Buffered exactly like `add_ext_model` and
111
+ `add_contribution`, on the identical structural reason and the identical restraint: the
112
+ kernel names neither `weft_command.contract.CommandResult` nor `weft_command.render.
113
+ Rendered` — `RendererOffer` remembers only that this pack offered *some* type and *some*
114
+ callable, exactly as `add_ext_model` stops at "this pack declared this class." `PackReport.
115
+ renderers` is read back off every report by `weft_cli.render.register_renderers_from_reports`,
116
+ called once, generically, by whatever already calls `discover()` — the same shape one
117
+ surface over, and the CLI's own built-in renderers register through the identical call so
118
+ no built-in keeps a private path.
119
+ """
120
+
121
+ from __future__ import annotations
122
+
123
+ import inspect
124
+ import os
125
+ import re
126
+ import sys
127
+ from collections.abc import Callable, Collection, Iterable, Mapping
128
+ from dataclasses import dataclass
129
+ from enum import StrEnum
130
+ from importlib import metadata
131
+ from typing import Protocol, cast, get_type_hints
132
+
133
+ from pydantic import BaseModel, ConfigDict, ValidationError
134
+
135
+ from weft_kernel.context import ServiceRole
136
+ from weft_kernel.errors import UnresolvedNameError, WeftError
137
+ from weft_kernel.payload.ext import ExtModel
138
+ from weft_kernel.pipeline import StageDeclaration
139
+ from weft_kernel.registry import Registry
140
+ from weft_kernel.resolution import Contribution
141
+ from weft_kernel.seam import Deprecation, Unavailable, removal_for, warn_deprecated
142
+
143
+ #: The one entry-point group a pack declares. `docs/02-extension-model.md` section 2.
144
+ ENTRY_POINT_GROUP = "weft.packs"
145
+
146
+ _ENV_TOKEN = re.compile(r"^\$\{env:([^}]+)\}$")
147
+
148
+
149
+ class PackStatus(StrEnum):
150
+ """Why a pack is, or is not, contributing to the registry right now.
151
+
152
+ One vocabulary for every reason, shared with G4's conditional
153
+ registration (`docs/02-extension-model.md`, *The trust model*): a refused
154
+ pack and one that registered only part of what it offers are the same
155
+ question — *why is this not here?* — answered the same way.
156
+ """
157
+
158
+ ACTIVE = "active"
159
+ REFUSED = "refused"
160
+ FAILED = "failed"
161
+ PARTIAL = "partial"
162
+ ALLOWED_NOT_INSTALLED = "allowed, not installed"
163
+
164
+
165
+ class Disclosure(BaseModel):
166
+ """What a pack says it touches. Optional, informational, and enforces nothing.
167
+
168
+ `docs/02-extension-model.md`: "a disclosure to the operator, never a claim
169
+ weft checks." Concrete strings, never booleans — a hostname is
170
+ information, `network: true` is noise. Absence (no module-level
171
+ `DISCLOSURE`) is reported by `doctor` as "not disclosed", which is honest;
172
+ an empty `Disclosure()` asserts something the kernel cannot verify either,
173
+ so this model exists to be read, never to gate anything.
174
+ """
175
+
176
+ model_config = ConfigDict(frozen=True, extra="forbid")
177
+
178
+ network: tuple[str, ...] = ()
179
+ filesystem: tuple[str, ...] = ()
180
+ subprocess: tuple[str, ...] = ()
181
+ note: str = ""
182
+
183
+
184
+ @dataclass(frozen=True, slots=True)
185
+ class PipelineResource:
186
+ """One pipeline document a pack ships from inside its own installed package.
187
+
188
+ Task **2.8**. `weft_retrieve.contract.RouteCatalogue`'s own docstring: "populated by
189
+ the same eager discovery pass that builds the registry, from the pipelines packs
190
+ contribute — so a third party's pipeline becomes routable on install with no edit
191
+ anywhere under `packages/`." `package` and `resource` are exactly
192
+ `importlib.resources.files(package).joinpath(resource)`'s two arguments — a locator,
193
+ never an opened file: the kernel still names no capability and opens nothing, the same
194
+ restraint `weft_kernel.pipeline`'s own module docstring states for the `Pipeline`
195
+ model itself ("the kernel publishes the model and opens no file"). Reading `resource`
196
+ and parsing it as a pipeline document is `weft-cli`'s job — the one distribution that
197
+ already owns `weft_cli.pipeline_catalogue`'s YAML parser.
198
+ """
199
+
200
+ distribution: str
201
+ package: str
202
+ resource: str
203
+
204
+
205
+ class RendererOffer(BaseModel):
206
+ """One `(result type, renderer)` pair a pack offered, attributed to the pack that offered it.
207
+
208
+ Task **6.20**, G13's third repair (`docs/03-cli.md` → *Plugin-contributed commands*): a
209
+ result type nobody outside the CLI can format is only half a `Command` contract, so a
210
+ renderer is registered at the same seam a command is — `PackRegistrar.add_renderer`
211
+ buffers this, exactly as `add_ext_model` buffers a bare class reference. **The kernel
212
+ names neither of the two `weft-command` types involved** — not `weft_command.contract.
213
+ CommandResult`, the type a real `result_type` will always actually be, and not
214
+ `weft_command.render.Rendered`, the type a real `render` will always actually return: it
215
+ remembers only that this pack offered *some* type and *some* callable, and goes no
216
+ further. Turning the buffer into something a renderer dispatch can actually use is
217
+ `weft_cli.render.register_renderers_from_reports`'s job, run once every report is final.
218
+ """
219
+
220
+ model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
221
+
222
+ distribution: str
223
+ result_type: type[object]
224
+ render: Callable[[object], object]
225
+
226
+
227
+ class ServiceRoleOffer(BaseModel):
228
+ """One `ServiceRole` a pack declared, attributed to the pack that declared it.
229
+
230
+ Ledger task **9.0**: `docs/02-extension-model.md` §1's own Phase 0 narrowing named the
231
+ hole this closes — a pack had no seam through which to declare that `[services].<key>`
232
+ selects an implementation of a contract it publishes. `_read_service_roles` builds one of
233
+ these per entry in the pack's module-level `SERVICE_ROLES`: `distribution` is filled in
234
+ from the entry point currently being imported, never something the pack states and never
235
+ something it could misattribute to a distribution not its own. `role` is the declaration
236
+ itself; this wrapper adds only attribution.
237
+ """
238
+
239
+ model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
240
+
241
+ distribution: str
242
+ role: ServiceRole
243
+
244
+
245
+ class PackReport(BaseModel):
246
+ """One pack's discovery outcome — the row `weft plugins list|doctor` will print.
247
+
248
+ `pack` is the pack's identity — its `weft.packs` **entry-point name** (`chunk =
249
+ "weft_chunk:register"` names the pack `chunk`), which is what every operator-facing
250
+ surface prints and what a `[packs.<pack>]` settings block keys on. `distribution` is a
251
+ different fact: the thing PyPI installs, which is what the `[packs] allow` trust
252
+ boundary, `weft plugins doctor`'s version column and `weft_cli.skew` all key on. They
253
+ were the same string while every pack shipped in a distribution of its own; they stopped
254
+ being the same the moment one distribution shipped fourteen packs, and a report that
255
+ carried only the second could no longer tell fourteen rows apart.
256
+
257
+ `pack` is `None` for exactly one row: `ALLOWED_NOT_INSTALLED`, where `[packs] allow`
258
+ named a distribution nothing installed claims. There is no entry point, so there is no
259
+ pack — said out loud rather than filled in with the distribution name, which would
260
+ assert a pack that does not exist.
261
+
262
+ `ambient` is only ever meaningful on `ACTIVE`: it means "running, and not
263
+ a direct dependency" (`docs/02-extension-model.md`), which `discover()`
264
+ can only judge when its caller supplies `direct_dependencies` — the
265
+ dependency graph itself lives outside the kernel, in whatever built the
266
+ project manifest. Left `None`-less and `False` by default rather than a
267
+ tri-state, because "cannot determine" is exactly what an absent
268
+ `direct_dependencies` already means to a caller that asked for it.
269
+
270
+ `pipeline_resources` is empty for every pack until task 2.8, and empty for any pack
271
+ that never calls `PackRegistrar.add_pipeline_resource` — most packs ship no pipeline
272
+ at all, and requiring an empty declaration from every one of them would be a
273
+ registration tax with no failure behind it, `weft_enhance.contract`'s own declining
274
+ argument for `destroys` applied here to a different field.
275
+
276
+ `deprecations` — task 5.2e — is every `weft_kernel.seam.Deprecation` the pack buffered
277
+ through `PackRegistrar.deprecate`, empty for every pack that never calls it. `docs/
278
+ 02-extension-model.md` §2's status vocabulary gains no member for this: `weft plugins
279
+ doctor` reads a non-empty tuple as a flag beside `status`, the same way it already
280
+ reads `ambient`, never as a status of its own — `docs/09-release.md` §3, "a flag on
281
+ an existing status... no new status."
282
+
283
+ `ext_models` — task 5.2g — is every `weft_kernel.payload.ext.ExtModel` subclass the pack
284
+ buffered through `PackRegistrar.add_ext_model`, empty for any pack that ships no
285
+ namespaced extension data (most packs). This is the *declared* half of fitness function
286
+ 14's runtime property; `weft_store.rehydrate.register_from_reports` reads it back off
287
+ every report to compute the *present* half.
288
+
289
+ `contributions` — task 5.3a — is every `weft_kernel.resolution.Contribution` the pack
290
+ buffered through `PackRegistrar.add_contribution`, empty for any pack that offers no
291
+ slot contribution (most packs). `weft_cli.registry_bootstrap.build_dependencies` is the
292
+ one place every report's own tuple is concatenated into the `contributions=` argument
293
+ every `weft_kernel.resolution.resolve` call site now passes — `02` §3 → *Slots*: "a pack
294
+ may... contribute into a slot a pipeline opted into."
295
+
296
+ `renderers` — task **6.20**, G13 — is every `RendererOffer` the pack buffered through
297
+ `PackRegistrar.add_renderer`, empty for any pack that contributes no `Command` result a
298
+ person needs formatted (most packs). `weft_cli.render.register_renderers_from_reports`
299
+ is the one place every report's own tuple is read back off and made reachable for
300
+ dispatch — the identical shape `ext_models` and `weft_store.rehydrate.
301
+ register_from_reports` already have, one surface over.
302
+
303
+ `service_roles` — task **9.0** — is every `ServiceRoleOffer` read from the pack's
304
+ module-level `SERVICE_ROLES`, empty for any pack that declares no `[services]` role (most
305
+ packs). Unlike `renderers`, it is **not** gated on a clean `commit()`: a role key is a
306
+ static fact about the pack, and a pack whose settings failed must still be able to tell an
307
+ operator that its `[services]` key exists — see `_read_service_roles` for the case that
308
+ settles it.
309
+ """
310
+
311
+ model_config = ConfigDict(frozen=True, extra="forbid")
312
+
313
+ pack: str | None
314
+ distribution: str
315
+ status: PackStatus
316
+ ambient: bool = False
317
+ contributed: int = 0
318
+ reason: str | None = None
319
+ disclosure: Disclosure | None = None
320
+ pipeline_resources: tuple[PipelineResource, ...] = ()
321
+ deprecations: tuple[Deprecation, ...] = ()
322
+ unavailable: tuple[Unavailable, ...] = ()
323
+ ext_models: tuple[type[ExtModel], ...] = ()
324
+ contributions: tuple[Contribution, ...] = ()
325
+ renderers: tuple[RendererOffer, ...] = ()
326
+ service_roles: tuple[ServiceRoleOffer, ...] = ()
327
+
328
+
329
+ class PackSettingsError(WeftError):
330
+ """A pack's settings could not be built: wrong `register()` shape, or validation failed.
331
+
332
+ Raised before `register()` ever runs — `docs/02-extension-model.md`: "the
333
+ kernel validates before `register` is called and fails naming the pack
334
+ and the field." Inside `discover()` this is caught and folded into a
335
+ `FAILED` report rather than propagated, for the same reason a raising
336
+ `register()` is: one broken pack must not stop every other pack from
337
+ loading. Raised directly (uncaught) when the resolution helpers are
338
+ exercised on their own.
339
+ """
340
+
341
+
342
+ class EnvInterpolationError(WeftError):
343
+ """`${env:VAR}` named an environment variable this process does not have set.
344
+
345
+ Never resolves to an empty string instead — a silently-empty secret is a
346
+ credential that looks like it works and is not, which is a worse failure
347
+ than refusing to start.
348
+ """
349
+
350
+
351
+ class InertPluginPinError(WeftError):
352
+ """A `[plugins]` pin never got the chance to arbitrate anything, once discovery finished.
353
+
354
+ `docs/02-extension-model.md` §3: "a pin for a pair that has no collision"
355
+ must fail loudly rather than being read as a harmless no-op — "an inert
356
+ pin is a lie about what is running." Raised here, after every entry
357
+ point has been enumerated and every permitted pack activated, off
358
+ `weft_kernel.registry.Registry.unconsulted_pins()` — the same shape as
359
+ `UnknownPackSettingsError` just above: not folded into any one pack's
360
+ report, because this is not one pack's failure, it is the configuration
361
+ naming a fight that never happened. Distinct from
362
+ `weft_kernel.registry.UnresolvedPluginPinError`, which fires *during* a
363
+ real collision when the pin names neither side of it; this one fires
364
+ when no collision for the pinned key ever occurred at all.
365
+
366
+ Repair for a reviewer finding: this used to be unconditionally fatal to
367
+ every registry-needing command, `weft plugins doctor` and `weft plugins
368
+ list` included — the two commands whose whole job is explaining what is
369
+ installed, one of which `manual/troubleshooting.md`'s own remedy for this
370
+ exact error names as the next thing to run. `discover`'s `strict_pins`
371
+ parameter is what a diagnostic caller sets `False` to see every
372
+ `PackReport` anyway; `weft_cli.cli.dispatch` does this for `plugins
373
+ list`/`plugins doctor` and no other command, on the same reasoning
374
+ `weft_cli.registry_bootstrap`'s own module docstring already gives for
375
+ `WEFT_DATABASE_URL`: "a bare crash on every registry-needing command —
376
+ including `plugins doctor`, the one command meant to diagnose exactly
377
+ this — would be worse than" a report.
378
+ """
379
+
380
+
381
+ class UnknownPackSettingsError(WeftError, UnresolvedNameError):
382
+ """A `packs:` settings key names a pack no installed `weft.packs` entry point claims.
383
+
384
+ Raised by `discover()` itself, after every entry point has been
385
+ enumerated — never caught and folded into a `PackReport`, because this is
386
+ not one pack failing; it is the configuration asking for a pack that is
387
+ not there at all. `docs/02-extension-model.md`: "A `packs:` key naming a
388
+ pack that is not installed is an error... loud, naming the distribution
389
+ to install." Keyed on the **pack** — the entry-point name — since `02` §2's
390
+ settings block is `[packs.store]`, not `[packs.store]`: several packs may
391
+ ship in one distribution, and each configures itself. This is the strict half
392
+ of the `allow`/`packs:` asymmetry — `allow` naming an absent distribution is
393
+ `ALLOWED_NOT_INSTALLED`, reported and not fatal, because `allow` only ever
394
+ narrows what is already there; `packs:` names a requirement, and an unmet
395
+ requirement raises.
396
+
397
+ Fitness function 12's family: `valid_options` is every pack that does declare
398
+ a `weft.packs` entry point.
399
+ """
400
+
401
+ def __init__(self, message: str, *, valid_options: tuple[str, ...]) -> None:
402
+ super().__init__(message)
403
+ self.valid_options = valid_options
404
+
405
+
406
+ class MalformedDisclosureError(WeftError):
407
+ """A pack's module-level `DISCLOSURE` is present but is not a `Disclosure` instance.
408
+
409
+ Distinct from absence: `docs/02-extension-model.md` calls "not
410
+ disclosed" honest reporting of a pack that disclosed nothing, and that
411
+ honesty breaks if a pack that *tried* to disclose something malformed —
412
+ a bare dict, a `Disclosure` from an incompatible version — is reported
413
+ identically. Folded into a `FAILED` report by `_activate`, naming the
414
+ distribution and what `DISCLOSURE` must be, rather than propagated.
415
+ """
416
+
417
+
418
+ class MalformedServiceRolesError(WeftError):
419
+ """A pack's module-level `SERVICE_ROLES` is not a tuple of `ServiceRole`.
420
+
421
+ Task **9.0**, on `MalformedDisclosureError`'s own footing: a pack that tried to declare a
422
+ role and got the shape wrong is a different fact from a pack that declared none, and
423
+ collapsing the two would leave an operator with a `[services]` key that silently does not
424
+ exist. Folded into a `FAILED` report by `_activate`, naming the pack.
425
+ """
426
+
427
+
428
+ class MissingDistributionMetadataError(WeftError):
429
+ """An entry point in the `weft.packs` group carries no distribution metadata.
430
+
431
+ Folded into a `FAILED` report keyed by the entry point's own name, the
432
+ same way an import failure or a raising `register()` is — one malformed
433
+ `.dist-info` degrades to a single row, not a hard stop for every other
434
+ installed pack.
435
+ """
436
+
437
+
438
+ class _DistributionLike(Protocol):
439
+ """The one attribute `discover()` needs from `importlib.metadata`'s distribution object."""
440
+
441
+ @property
442
+ def name(self) -> str: ...
443
+
444
+
445
+ class EntryPointLike(Protocol):
446
+ """The slice of `importlib.metadata.EntryPoint` `discover()` actually uses.
447
+
448
+ A `Protocol` rather than the concrete class so a test can hand `discover()`
449
+ a lightweight double instead of an installed distribution — real
450
+ `EntryPoint` objects already satisfy this structurally, with nothing to
451
+ subclass or register.
452
+ """
453
+
454
+ @property
455
+ def name(self) -> str: ...
456
+ @property
457
+ def module(self) -> str: ...
458
+ @property
459
+ def dist(self) -> _DistributionLike | None: ...
460
+ def load(self) -> Callable[..., None]: ...
461
+
462
+
463
+ class PackRegistrar:
464
+ """The registry view a pack's own `register()` receives — see the module docstring.
465
+
466
+ `Registry.add` minus its keyword-only `distribution` parameter: a pack
467
+ author calls `registrar.add(contract, name, factory)`, exactly the shape
468
+ `docs/02-extension-model.md` shows, and attribution is filled in from
469
+ whichever distribution `discover()` is currently importing — never
470
+ something the author states, and never something the author could get
471
+ wrong.
472
+ """
473
+
474
+ def __init__(self, registry: Registry, *, distribution: str) -> None:
475
+ self._registry = registry
476
+ self._distribution = distribution
477
+ self._pending: list[tuple[type[object], str, Callable[..., object]]] = []
478
+ self._pending_resources: list[PipelineResource] = []
479
+ self._pending_deprecations: list[Deprecation] = []
480
+ self._pending_unavailable: list[Unavailable] = []
481
+ self._pending_ext_models: list[type[ExtModel]] = []
482
+ self._pending_contributions: list[Contribution] = []
483
+ self._pending_renderers: list[RendererOffer] = []
484
+
485
+ @property
486
+ def contributed(self) -> int:
487
+ """How many registrations this pack has buffered — and, after `commit`, has landed."""
488
+ return len(self._pending)
489
+
490
+ def add(self, contract: type[object], name: str, factory: Callable[..., object]) -> None:
491
+ """Buffer `factory` as `name` for `contract`, attributed to this pack.
492
+
493
+ Nothing reaches the shared `Registry` yet — see `commit`. A pack's
494
+ own `register()` never calls `commit` itself; `_activate` does, once
495
+ `register()` has returned without raising.
496
+ """
497
+ self._pending.append((contract, name, factory))
498
+
499
+ def add_pipeline_resource(self, package: str, resource: str) -> None:
500
+ """Buffer a `PipelineResource(distribution, package, resource)`, attributed to this pack.
501
+
502
+ Task **2.8**. Buffered exactly like `add` — see `commit` — and for the identical
503
+ reason: a pack whose `register()` raises after calling this must not leave a
504
+ catalogue advertising a pipeline it never actually shipped. A pack calls this once
505
+ per pipeline document it ships, typically its own package name and a
506
+ `pipelines/<name>.yaml` resource path — `weft_cli.pipeline_catalogue.
507
+ load_contributed` is what turns the buffered locator into a parsed `Pipeline`.
508
+ """
509
+ self._pending_resources.append(
510
+ PipelineResource(distribution=self._distribution, package=package, resource=resource)
511
+ )
512
+
513
+ def deprecate(self, surface: str, *, reason: str) -> None:
514
+ """Mark `surface` — a plugin name, a `"Contract:name"` pair, or the pack itself —
515
+ deprecated, attributed to this pack.
516
+
517
+ Task 5.2e. Buffered exactly like `add_pipeline_resource`, for the identical reason:
518
+ a pack whose `register()` raises after calling this must not leave a warning
519
+ standing about a mark that never actually committed. Marking is all this method
520
+ does — the warning itself is `weft_kernel.seam.warn_deprecated`'s job, run by
521
+ `_activate` once `register()` has returned without raising, so a pack author states
522
+ the fact once and never writes the warning by hand; see that function's own
523
+ docstring for why the notice is a `DeprecationWarning`, not a `WeftError`.
524
+ """
525
+ self._pending_deprecations.append(
526
+ Deprecation(
527
+ distribution=self._distribution,
528
+ surface=surface,
529
+ reason=reason,
530
+ removal=removal_for(self._distribution),
531
+ )
532
+ )
533
+
534
+ def unavailable(self, surface: str, *, reason: str) -> None:
535
+ """Declare that this pack cannot provide `surface`, and why — ledger task **6.29**.
536
+
537
+ `01` → *Fitness functions* 5's second half: a capability that does not resolve to a live
538
+ implementation must be *declared unavailable at discovery time*, with a reason. Calling
539
+ this makes the pack's report `PackStatus.PARTIAL` — `02` §2's own word for "registered,
540
+ but a conditional dependency it wanted was not available, so part of what it offers did
541
+ not" — so `weft plugins doctor` says it before a run does.
542
+
543
+ Buffered exactly like `deprecate`, for the identical reason: a pack whose `register()`
544
+ raises after calling this must not leave a report standing about a mark that never
545
+ committed. Registering nothing else and declaring one surface unavailable is a perfectly
546
+ ordinary pack; what it must never be is silent.
547
+ """
548
+ self._pending_unavailable.append(
549
+ Unavailable(distribution=self._distribution, surface=surface, reason=reason)
550
+ )
551
+
552
+ def add_ext_model(self, model: type[ExtModel]) -> None:
553
+ """Buffer `model` — a pack's own `ExtModel` subclass — attributed to this pack.
554
+
555
+ Task **5.2g**. Buffered exactly like `add_pipeline_resource` and `deprecate`, for
556
+ the identical reason: a pack whose `register()` raises after calling this must not
557
+ leave a namespace looking claimed that never actually committed. `model` is a bare
558
+ class reference, not an instance — nothing here validates, instantiates or reads
559
+ `model.__namespace__`; this method only remembers that this pack offered it.
560
+ Turning the buffer into an entry `weft_store.rehydrate.rehydrate_ext` can actually
561
+ use is `weft_store.rehydrate.register_from_reports`'s job, run once every report is
562
+ final — the kernel names no capability, so it stops at "this pack declared this
563
+ class" and goes no further.
564
+ """
565
+ self._pending_ext_models.append(model)
566
+
567
+ def add_contribution(self, slot: str, stage: StageDeclaration) -> None:
568
+ """Offer `stage` into `slot` of whichever pipeline declares it, attributed to this pack.
569
+
570
+ Task **5.3a** (`S8`). Buffered exactly like `add_ext_model`, for the identical reason:
571
+ a pack whose `register()` raises after calling this must not leave a slot looking
572
+ filled that was never actually committed. `distribution` — `weft_kernel.resolution.
573
+ Contribution`'s own required field — is filled in from this registrar, on `add`'s own
574
+ footing: attribution is never something a pack author states, or could misattribute to
575
+ a distribution that is not its own. `stage.id` is the contribution's own **local**
576
+ name, unqualified by distribution — `Contribution`'s own docstring, and `02` §3 →
577
+ *Slots*: qualification happens only once a contribution is actually placed. This
578
+ method validates nothing about `slot` itself: a slot no installed pipeline declares is
579
+ `02` §3's own "recorded no-op", not a registration-time refusal — `weft_kernel.
580
+ resolution.resolve` is where that distinction is actually drawn.
581
+ """
582
+ self._pending_contributions.append(
583
+ Contribution(slot=slot, distribution=self._distribution, stage=stage)
584
+ )
585
+
586
+ def add_renderer(self, result_type: type[object], renderer: Callable[[object], object]) -> None:
587
+ """Offer `renderer` for `result_type`, attributed to this pack.
588
+
589
+ Task **6.20**, G13's third repair. Buffered exactly like `add_ext_model` and
590
+ `add_contribution`, for the identical reason: a pack whose `register()` raises after
591
+ calling this must not leave a result type looking renderable that never actually
592
+ committed. `distribution` is filled in from this registrar, on `add`'s own footing —
593
+ attribution is never something a pack author states. This method validates nothing
594
+ about `result_type` or `renderer`: it does not know either is a `weft_command.
595
+ contract.CommandResult` subclass or a `weft_command.render.Rendered`-returning
596
+ callable — the kernel names neither type, exactly as the module docstring's own
597
+ rule requires. Turning the buffer into a working dispatch is `weft_cli.render.
598
+ register_renderers_from_reports`'s job.
599
+ """
600
+ self._pending_renderers.append(
601
+ RendererOffer(distribution=self._distribution, result_type=result_type, render=renderer)
602
+ )
603
+
604
+ def commit(self) -> None:
605
+ """Write every buffered registration to the `Registry`, all at once or not at all.
606
+
607
+ Delegates to `Registry.add_many`, which is what makes this atomic —
608
+ see the module docstring. Called by `_activate` exactly once, after
609
+ a pack's `register()` returns without raising; never called at all
610
+ if `register()` raises, which is what keeps a half-registered pack
611
+ from ever being written.
612
+ """
613
+ self._registry.add_many(self._pending, distribution=self._distribution)
614
+
615
+ @property
616
+ def pipeline_resources(self) -> tuple[PipelineResource, ...]:
617
+ """Every `PipelineResource` this pack has buffered so far — `_activate`'s own read,
618
+ once `register()` has returned without raising. See `add_pipeline_resource`.
619
+ """
620
+ return tuple(self._pending_resources)
621
+
622
+ @property
623
+ def deprecations(self) -> tuple[Deprecation, ...]:
624
+ """Every `Deprecation` this pack has buffered so far — `_activate`'s own read, once
625
+ `register()` has returned without raising. See `deprecate`.
626
+ """
627
+ return tuple(self._pending_deprecations)
628
+
629
+ @property
630
+ def unavailable_surfaces(self) -> tuple[Unavailable, ...]:
631
+ """Every `Unavailable` this pack has buffered so far — `_activate`'s own read, once
632
+ `register()` has returned without raising. See `unavailable`.
633
+ """
634
+ return tuple(self._pending_unavailable)
635
+
636
+ @property
637
+ def ext_models(self) -> tuple[type[ExtModel], ...]:
638
+ """Every `ExtModel` subclass this pack has buffered so far — `_activate`'s own read,
639
+ once `register()` has returned without raising. See `add_ext_model`.
640
+ """
641
+ return tuple(self._pending_ext_models)
642
+
643
+ @property
644
+ def contributions(self) -> tuple[Contribution, ...]:
645
+ """Every `Contribution` this pack has buffered so far — `_activate`'s own read, once
646
+ `register()` has returned without raising. See `add_contribution`.
647
+ """
648
+ return tuple(self._pending_contributions)
649
+
650
+ @property
651
+ def renderers(self) -> tuple[RendererOffer, ...]:
652
+ """Every `RendererOffer` this pack has buffered so far — `_activate`'s own read, once
653
+ `register()` has returned without raising. See `add_renderer`.
654
+ """
655
+ return tuple(self._pending_renderers)
656
+
657
+
658
+ def interpolate_env(value: object, *, environ: Mapping[str, str] | None = None) -> object:
659
+ """Recursively resolve every `${env:VAR}` string in `value` against `environ`.
660
+
661
+ `docs/02-extension-model.md`: "`${env:VAR}` interpolation [is] performed
662
+ by the config loader, so no component reads the environment itself" —
663
+ this is that interpolation, and the only place in the kernel that reads
664
+ `os.environ` by default. A string that is *exactly* `${env:VAR}` becomes
665
+ the variable's value; a string that merely contains the token as a
666
+ substring is left untouched, because partial substitution inside a
667
+ longer string is a template engine this project does not have and does
668
+ not need for a credential or an endpoint. Walks `dict` and `list`
669
+ recursively so a whole settings block can be interpolated in one call;
670
+ every other type passes through unchanged. Raises `EnvInterpolationError`,
671
+ naming the variable, rather than substituting an empty string, if it is
672
+ unset.
673
+ """
674
+ env = os.environ if environ is None else environ
675
+ return _interpolate(value, env)
676
+
677
+
678
+ def _interpolate(value: object, env: Mapping[str, str]) -> object:
679
+ if isinstance(value, str):
680
+ match = _ENV_TOKEN.match(value)
681
+ if match is None:
682
+ return value
683
+ name = match.group(1)
684
+ if name not in env:
685
+ raise EnvInterpolationError(
686
+ f"'${{env:{name}}}' names an environment variable that is not set. Set "
687
+ f"{name}, or remove the reference from the configuration."
688
+ )
689
+ return env[name]
690
+ if isinstance(value, Mapping):
691
+ items = cast("Mapping[str, object]", value)
692
+ return {key: _interpolate(item, env) for key, item in items.items()}
693
+ if isinstance(value, list):
694
+ entries = cast("list[object]", value)
695
+ return [_interpolate(item, env) for item in entries]
696
+ return value
697
+
698
+
699
+ def allow_list_from_config(document: Mapping[str, object]) -> tuple[str, ...] | None:
700
+ """The exhaustive pin from a parsed `weft.toml`-shaped mapping's `[packs] allow`.
701
+
702
+ `None` means absent, which `docs/02-extension-model.md` states plainly:
703
+ "`weft.toml` — optional. Absent means open." Present but empty
704
+ (`allow = []`) is a real, if severe, policy — refuse every pack — and is
705
+ returned as `()`, never coerced to `None`: the two mean different things
706
+ and this function does not collapse them.
707
+
708
+ **A `packs` key that is present but not a table is refused, not absorbed
709
+ into absence.** `docs/02` §2's *The trust model*:
710
+ `packs = ["weft-store"]` is the plausible typo for `[packs]\\nallow =
711
+ [...]` — TOML parses it to a `list`, and a bare `isinstance(packs,
712
+ Mapping)` guard used to fail that check the same way a genuinely absent
713
+ `packs` does, silently landing on open-by-default with the operator's
714
+ allow-list never read. `None` only for a key that is not in `document`
715
+ at all; anything present that is not a `Mapping` raises `WeftError`
716
+ naming the shape found and the shape expected, so the typo is loud
717
+ instead of a policy that quietly never applied. `allow = []` inside a
718
+ *valid* `[packs]` table is unaffected — that case reaches the check
719
+ below unchanged, and stays "refuse every pack", not this one.
720
+ """
721
+ if "packs" not in document:
722
+ return None
723
+ packs = document["packs"]
724
+ if not isinstance(packs, Mapping):
725
+ raise WeftError(
726
+ f"weft.toml's [packs] must be a table, not {type(packs).__name__} — found "
727
+ f"`packs = {packs!r}`. Did you mean `[packs]\\nallow = [...]`? See "
728
+ f"docs/02-extension-model.md section 2, The trust model."
729
+ )
730
+ packs_table = cast("Mapping[str, object]", packs)
731
+ allow = packs_table.get("allow")
732
+ if allow is None:
733
+ return None
734
+ if not isinstance(allow, list):
735
+ raise WeftError(
736
+ f"[packs] allow must be a list of distribution names (strings); found "
737
+ f"{type(allow).__name__}."
738
+ )
739
+ entries = cast("list[object]", allow)
740
+ if not all(isinstance(item, str) for item in entries):
741
+ raise WeftError("[packs] allow must be a list of distribution names (strings).")
742
+ return tuple(cast("list[str]", entries))
743
+
744
+
745
+ def plugin_pins_from_config(document: Mapping[str, object]) -> dict[str, str]:
746
+ """The exhaustive `[plugins]` table from a parsed `weft.toml`-shaped mapping.
747
+
748
+ `docs/02-extension-model.md` §3's exact shape — `"Enhancer:keybert" =
749
+ "weft-kw"` — a `{"Contract:name": "distribution"}` mapping handed
750
+ straight to `weft_kernel.registry.Registry(plugin_pins=...)`, never
751
+ interpreted here: this function's whole job is the same one
752
+ `allow_list_from_config` already does for `[packs] allow`, turning a
753
+ piece of an already-parsed mapping into the shape the kernel accepts,
754
+ with no file ever opened by this module. Absent `[plugins]` returns
755
+ `{}` — "absent means open" is `[packs] allow`'s reading; here it means
756
+ no pin exists, so every collision still refuses exactly as it always
757
+ has. A key naming no real `(contract, name)` collision is not caught
758
+ here — that is `weft_kernel.discovery.discover`'s `InertPluginPinError`,
759
+ raised only once discovery knows what actually collided.
760
+ """
761
+ plugins = document.get("plugins")
762
+ if not isinstance(plugins, Mapping):
763
+ return {}
764
+ table = cast("Mapping[str, object]", plugins)
765
+ if not all(isinstance(value, str) for value in table.values()):
766
+ raise WeftError('[plugins] must map "Contract:name" strings to distribution-name strings.')
767
+ return {str(key): cast("str", value) for key, value in table.items()}
768
+
769
+
770
+ def discover(
771
+ registry: Registry,
772
+ *,
773
+ allow: Collection[str] | None = None,
774
+ pack_settings: Mapping[str, Mapping[str, object]] | None = None,
775
+ direct_dependencies: Collection[str] | None = None,
776
+ entry_points: Iterable[EntryPointLike] | None = None,
777
+ strict_pins: bool = True,
778
+ ) -> tuple[PackReport, ...]:
779
+ """Discover every installed `weft.packs` pack, importing and registering what is permitted.
780
+
781
+ `allow` is keyed on the **distribution**, not the pack: it is a trust
782
+ boundary, and trust attaches to the thing you installed from an index, not
783
+ to a name chosen inside a wheel you had already accepted.
784
+
785
+ `allow=None` is the open-by-default posture: everything installed is
786
+ imported and registered. `allow` present is an exhaustive pin — anything
787
+ installed but unlisted is `REFUSED` **and its entry point is never
788
+ loaded**, which is fitness function 8(a): refusal precedes execution, not
789
+ a `try` block around it. Every name in `allow` that no installed
790
+ distribution claims comes back as `ALLOWED_NOT_INSTALLED`, reported, not
791
+ fatal — `docs/02-extension-model.md`'s distinction between a permission
792
+ (`allow`) and a requirement (`packs:` settings, which errors on the same
793
+ absence).
794
+
795
+ `pack_settings` is keyed on the **pack** — the `weft.packs` entry-point name,
796
+ so `[packs.store]` and not `[packs.store]`. It is interpolated for
797
+ `${env:VAR}` once, up front, then
798
+ each pack's own slice is validated against the Pydantic model its
799
+ `register(registrar, settings: Settings)` declares — before `register` is
800
+ called. `direct_dependencies`, when supplied, is what lets an `ACTIVE`
801
+ report carry `ambient=True`; the dependency graph itself is not this
802
+ module's to compute.
803
+
804
+ Every `pack_settings` key that no enumerated entry point claims raises
805
+ `UnknownPackSettingsError`, naming the pack and every pack that did declare a
806
+ `weft.packs` entry point — `packs:` expresses a requirement, unlike `allow`,
807
+ so this is fatal where `ALLOWED_NOT_INSTALLED` is not.
808
+
809
+ `entry_points` defaults to every real, installed `weft.packs` entry
810
+ point; a caller passes its own only to test discovery without installing
811
+ a distribution.
812
+
813
+ `strict_pins=False` — repair for a reviewer finding against the task 1.12
814
+ commit — skips the `InertPluginPinError` check below rather than raising
815
+ it, so a caller whose whole job is *explaining* the environment (`weft
816
+ plugins list`/`doctor`) can still get every `PackReport` back even when a
817
+ `[plugins]` pin never arbitrated anything. Every other caller keeps the
818
+ default `True`: `registry.unconsulted_pins()` is unaffected either way —
819
+ a `plugins doctor` caller passing `strict_pins=False` can still read it
820
+ off the registry it was handed back and report it, rather than losing
821
+ the information the way a raised exception discarding every built
822
+ `PackReport` would.
823
+ """
824
+ candidates: list[EntryPointLike] = (
825
+ list(entry_points)
826
+ if entry_points is not None
827
+ # `importlib.metadata.EntryPoint` satisfies this protocol at runtime — every
828
+ # attribute `EntryPointLike` names is present — but its typeshed stub mixes
829
+ # plain fields and properties in a way pyright's strict structural check
830
+ # cannot verify statically. The cast bridges that one stdlib boundary; it
831
+ # asserts nothing this module does not already rely on being true.
832
+ else cast("list[EntryPointLike]", list(metadata.entry_points(group=ENTRY_POINT_GROUP)))
833
+ )
834
+ allow_set = set(allow) if allow is not None else None
835
+ settings_source = _as_settings_source(interpolate_env(dict(pack_settings or {})))
836
+
837
+ reports: list[PackReport] = []
838
+ seen: set[str] = set()
839
+ seen_packs: set[str] = set()
840
+
841
+ for entry_point in candidates:
842
+ # The pack's own identity, and never the distribution's: an entry-point name is
843
+ # unique within the group and stays the pack's own even when fourteen packs ship
844
+ # in one wheel. Read before `_distribution_name`, because it is available even
845
+ # when distribution metadata is not.
846
+ pack = entry_point.name
847
+ try:
848
+ distribution = _distribution_name(entry_point)
849
+ except MissingDistributionMetadataError as exc:
850
+ # No distribution name means no attribution for the trust boundary and no
851
+ # version to report — fold into FAILED, keyed by the entry point's own name,
852
+ # rather than aborting discovery for every other pack. The pack identity is
853
+ # intact here; it is the distribution that is missing.
854
+ reports.append(
855
+ PackReport(
856
+ pack=pack,
857
+ distribution=entry_point.name,
858
+ status=PackStatus.FAILED,
859
+ reason=str(exc),
860
+ )
861
+ )
862
+ continue
863
+ seen.add(distribution)
864
+ seen_packs.add(pack)
865
+
866
+ # `allow` stays keyed on the **distribution**, deliberately. It is a trust
867
+ # boundary, and trust is placed in a thing you install from an index and can pin,
868
+ # audit and revoke — not in a name a pack chose for itself inside a wheel you had
869
+ # already accepted. `[packs] allow = ["weft-rag"]` therefore permits every pack
870
+ # that wheel ships, which is the same posture as before: what you installed is
871
+ # what you allowed. `[packs.<pack>]` settings key on `pack` instead, because that
872
+ # is configuration of one pack's behaviour, not a decision about provenance.
873
+ if allow_set is not None and distribution not in allow_set:
874
+ reports.append(
875
+ PackReport(
876
+ pack=pack,
877
+ distribution=distribution,
878
+ status=PackStatus.REFUSED,
879
+ reason=(
880
+ f"'{distribution}' is not listed in [packs] allow. Add it there to "
881
+ f"permit it."
882
+ ),
883
+ )
884
+ )
885
+ continue
886
+
887
+ reports.append(
888
+ _activate(
889
+ entry_point,
890
+ pack=pack,
891
+ distribution=distribution,
892
+ registry=registry,
893
+ raw_settings=settings_source.get(pack, {}),
894
+ direct_dependencies=direct_dependencies,
895
+ )
896
+ )
897
+
898
+ if allow_set is not None:
899
+ for missing in sorted(allow_set - seen):
900
+ reports.append(
901
+ PackReport(pack=None, distribution=missing, status=PackStatus.ALLOWED_NOT_INSTALLED)
902
+ )
903
+
904
+ unclaimed = sorted(set(settings_source) - seen_packs)
905
+ if unclaimed:
906
+ named = ", ".join(f"'{name}'" for name in unclaimed)
907
+ options = tuple(sorted(seen_packs))
908
+ available = ", ".join(f"'{name}'" for name in options) or "none"
909
+ raise UnknownPackSettingsError(
910
+ f"[packs] settings name {named}, which {'is' if len(unclaimed) == 1 else 'are'} "
911
+ f"not installed. Install the distribution that ships it, or remove its settings "
912
+ f"block. Packs that declare a '{ENTRY_POINT_GROUP}' entry point: {available}.",
913
+ valid_options=options,
914
+ )
915
+
916
+ # Every pack that was going to collide with anyone already has, since every candidate
917
+ # above has been activated (or refused, or reported missing) by this point — so a pin
918
+ # `registry` was given that `Registry.unconsulted_pins()` still lists never arbitrated a
919
+ # real collision at all. `docs/02-extension-model.md` §3: "an inert pin is a lie about
920
+ # what is running." Raised here, not folded into any one pack's report — see
921
+ # `InertPluginPinError`'s own docstring for why. Skipped, not silenced, when
922
+ # `strict_pins` is `False`: the state is still sitting on `registry.unconsulted_pins()`
923
+ # for a caller that asked not to have it be fatal to read.
924
+ if strict_pins:
925
+ unconsulted = sorted(registry.unconsulted_pins())
926
+ if unconsulted:
927
+ named = ", ".join(f"'{pin}'" for pin in unconsulted)
928
+ raise InertPluginPinError(
929
+ f"[plugins] pins {named}, but weft never saw two distributions contend for "
930
+ f"what {'it names' if len(unconsulted) == 1 else 'they name'} — nothing to "
931
+ f"arbitrate. Remove the pin, or check that both distributions it should choose "
932
+ f"between are installed and actually registering that name."
933
+ )
934
+
935
+ return tuple(reports)
936
+
937
+
938
+ def _activate(
939
+ entry_point: EntryPointLike,
940
+ *,
941
+ pack: str,
942
+ distribution: str,
943
+ registry: Registry,
944
+ raw_settings: Mapping[str, object],
945
+ direct_dependencies: Collection[str] | None,
946
+ ) -> PackReport:
947
+ """Import one permitted pack, validate its settings, call `register()`, and report.
948
+
949
+ Registration is transactional: `registrar.commit()` — which is what
950
+ actually writes to `registry` — runs *inside* the same `try` as
951
+ `register()` itself, so a pack that raises, whether from `register()` or
952
+ from a collision `commit()` discovers, is caught by the same branch and
953
+ reported with `contributed=0` by default, which is true precisely because
954
+ nothing was written. Only a `register()` that returns and then commits
955
+ cleanly reaches `ACTIVE`, where `contributed=registrar.contributed`
956
+ reports what actually landed.
957
+
958
+ **Task 5.2e** — a clean commit is also the one point `registrar.deprecations`
959
+ is known to have actually landed, so this is where `weft_kernel.seam.warn_deprecated`
960
+ is called: once, with whatever the pack buffered, never for a pack that raised.
961
+
962
+ **Task 5.2g** — `registrar.ext_models` reaches `PackReport.ext_models` on the identical
963
+ terms: only once `register()` has returned and `commit()` has not raised. This function
964
+ does nothing else with it — reading the buffer back off every final report and making
965
+ the classes it names reachable for rehydration is `weft_store.rehydrate.
966
+ register_from_reports`'s job, run by whatever calls `discover()`.
967
+
968
+ **Task 5.3a** — `registrar.contributions` reaches `PackReport.contributions` on the
969
+ identical terms, for the identical reason: a pack that raises must never look like it
970
+ offered a slot contribution it never actually committed. This function does nothing else
971
+ with it either — assembling every report's own tuple into one `contributions=` argument
972
+ for `weft_kernel.resolution.resolve` is `weft_cli.registry_bootstrap.build_dependencies`'s
973
+ job, the one caller `weft_kernel.resolution.Contribution`'s own docstring names.
974
+
975
+ **Task 6.20** — `registrar.renderers` reaches `PackReport.renderers` on the identical
976
+ terms: only once `register()` has returned and `commit()` has not raised. A `register()`
977
+ that raises after calling `add_renderer` leaves `renderers == ()`, the same atomicity
978
+ every other buffer already has — the CLI must never advertise a way to format a result a
979
+ pack never actually finished offering.
980
+
981
+ **Task 9.0** — `service_roles` does **not** follow that pattern, deliberately. It is read
982
+ from the pack's module-level `SERVICE_ROLES` before settings are validated, and travels on
983
+ every report from that point on, `FAILED` included: which `[services]` keys exist is a fact
984
+ about what is installed, not about what successfully configured itself.
985
+ """
986
+ ambient = direct_dependencies is not None and distribution not in direct_dependencies
987
+
988
+ try:
989
+ register_fn = entry_point.load()
990
+ except Exception as exc: # a pack's import can raise anything; that is FAILED, not a crash
991
+ return PackReport(
992
+ pack=pack, distribution=distribution, status=PackStatus.FAILED, reason=str(exc)
993
+ )
994
+
995
+ try:
996
+ disclosure = _read_disclosure(entry_point, pack=pack)
997
+ except MalformedDisclosureError as exc:
998
+ return PackReport(
999
+ pack=pack, distribution=distribution, status=PackStatus.FAILED, reason=str(exc)
1000
+ )
1001
+
1002
+ try:
1003
+ service_roles = _read_service_roles(entry_point, distribution=distribution)
1004
+ except MalformedServiceRolesError as exc:
1005
+ return PackReport(
1006
+ pack=pack, distribution=distribution, status=PackStatus.FAILED, reason=str(exc)
1007
+ )
1008
+
1009
+ registrar = PackRegistrar(registry, distribution=distribution)
1010
+ try:
1011
+ settings = _resolve_settings(register_fn, pack=pack, raw=raw_settings)
1012
+ register_fn(registrar, settings)
1013
+ registrar.commit()
1014
+ except Exception as exc: # one broken pack must not stop the rest from loading
1015
+ return PackReport(
1016
+ pack=pack,
1017
+ distribution=distribution,
1018
+ status=PackStatus.FAILED,
1019
+ ambient=ambient,
1020
+ reason=str(exc),
1021
+ disclosure=disclosure,
1022
+ service_roles=service_roles,
1023
+ )
1024
+
1025
+ deprecations = registrar.deprecations
1026
+ warn_deprecated(deprecations)
1027
+
1028
+ unavailable = registrar.unavailable_surfaces
1029
+
1030
+ return PackReport(
1031
+ pack=pack,
1032
+ distribution=distribution,
1033
+ # Task **6.29**: a pack that declared any surface unavailable registered *part* of what it
1034
+ # offers, which is exactly what `02` §2 reserves `PARTIAL` for. `ACTIVE` would say it is
1035
+ # contributing everything it has, and `FAILED` would say it contributed nothing — both
1036
+ # false, and the reason the vocabulary has a third word.
1037
+ status=PackStatus.PARTIAL if unavailable else PackStatus.ACTIVE,
1038
+ ambient=ambient,
1039
+ contributed=registrar.contributed,
1040
+ disclosure=disclosure,
1041
+ pipeline_resources=registrar.pipeline_resources,
1042
+ deprecations=deprecations,
1043
+ unavailable=unavailable,
1044
+ ext_models=registrar.ext_models,
1045
+ contributions=registrar.contributions,
1046
+ renderers=registrar.renderers,
1047
+ service_roles=service_roles,
1048
+ )
1049
+
1050
+
1051
+ def _read_disclosure(entry_point: EntryPointLike, *, pack: str) -> Disclosure | None:
1052
+ """The pack's module-level `DISCLOSURE`, read after import, before `register()` runs.
1053
+
1054
+ `None` means genuinely absent — no `DISCLOSURE` attribute at all — which
1055
+ is the honest "not disclosed" `docs/02-extension-model.md` describes.
1056
+ Present but not a `Disclosure` instance is a different fact and is never
1057
+ collapsed into that same `None`: it raises `MalformedDisclosureError`,
1058
+ naming the pack and what the attribute must be, so the caller can
1059
+ fold it into a `FAILED` report instead of reporting a pack that tried to
1060
+ disclose something as one that disclosed nothing.
1061
+ """
1062
+ module = sys.modules.get(entry_point.module)
1063
+ value = getattr(module, "DISCLOSURE", None) if module is not None else None
1064
+ if value is None:
1065
+ return None
1066
+ if isinstance(value, Disclosure):
1067
+ return value
1068
+ raise MalformedDisclosureError(
1069
+ f"'{pack}' defines DISCLOSURE but it is not a "
1070
+ f"weft_kernel.discovery.Disclosure instance (found {type(value).__name__}). "
1071
+ f"DISCLOSURE must be built from Disclosure(network=..., filesystem=..., "
1072
+ f"subprocess=..., note=...)."
1073
+ )
1074
+
1075
+
1076
+ def _read_service_roles(
1077
+ entry_point: EntryPointLike, *, distribution: str
1078
+ ) -> tuple[ServiceRoleOffer, ...]:
1079
+ """The pack's module-level `SERVICE_ROLES`, read after import, before `register()` runs.
1080
+
1081
+ Ledger task **9.0**, and the timing is the whole point. Which `[services]` roles a pack
1082
+ declares is a **static fact about the pack** — which key selects which contract — not a
1083
+ product of its configuration, so it is read exactly where `DISCLOSURE` is read: after the
1084
+ module imports, before settings are validated and before `register()` runs. A pack whose
1085
+ settings fail therefore still tells an operator that its role key *exists*.
1086
+
1087
+ That is not a nicety. `weft-store`'s `[packs.store] dsn` is required, so on a machine with
1088
+ no `weft.toml` the `store` pack reports `FAILED` and registers nothing — and had this
1089
+ declaration been buffered through `register()`, `[services] store = "qdrant"` would then be
1090
+ refused as an *unknown key*, naming the wrong problem entirely, on exactly the machine where
1091
+ an operator is trying to configure their way out of it. The pre-9.0 behaviour — the key
1092
+ parses, and the plugin name fails later through `weft_cli.registry_bootstrap.require_plugin`,
1093
+ which names the pack and its reason — is the better error, and reading the declaration here
1094
+ is what preserves it.
1095
+
1096
+ Absent means absent: most packs declare no role. Present but not a tuple of `ServiceRole`
1097
+ is a different fact and is never collapsed into the same empty answer, on
1098
+ `_read_disclosure`'s own footing.
1099
+ """
1100
+ module = sys.modules.get(entry_point.module)
1101
+ raw: object = getattr(module, "SERVICE_ROLES", None) if module is not None else None
1102
+ if raw is None:
1103
+ return ()
1104
+ found = type(raw).__name__
1105
+ declared = cast("tuple[object, ...]", raw) if isinstance(raw, tuple) else ()
1106
+ if not isinstance(raw, tuple) or not all(isinstance(item, ServiceRole) for item in declared):
1107
+ raise MalformedServiceRolesError(
1108
+ f"'{entry_point.name}' defines SERVICE_ROLES but it is not a tuple of "
1109
+ f"weft_kernel.context.ServiceRole (found {found}). Declare it as "
1110
+ f"`SERVICE_ROLES = (MY_ROLE,)`, beside the contract the role selects for."
1111
+ )
1112
+ return tuple(
1113
+ ServiceRoleOffer(distribution=distribution, role=role)
1114
+ for role in declared
1115
+ if isinstance(role, ServiceRole)
1116
+ )
1117
+
1118
+
1119
+ def _resolve_settings(
1120
+ register_fn: Callable[..., None], *, pack: str, raw: Mapping[str, object]
1121
+ ) -> BaseModel:
1122
+ """The validated settings instance `register_fn`'s own model expects.
1123
+
1124
+ Introspects `register_fn`'s second parameter for a `pydantic.BaseModel`
1125
+ subclass annotation — `docs/02-extension-model.md`'s own shape,
1126
+ `register(registry: Registry, settings: Settings) -> None` — and
1127
+ validates `raw` against it before `register_fn` is ever called. A pack
1128
+ with nothing to configure still declares an (empty) model; there is no
1129
+ settings-less shape to special-case.
1130
+ """
1131
+ parameters = list(inspect.signature(register_fn).parameters.values())
1132
+ if len(parameters) != 2:
1133
+ raise PackSettingsError(
1134
+ f"'{pack}' declares register() with {len(parameters)} parameter(s); "
1135
+ f"docs/02-extension-model.md requires exactly (registrar, settings)."
1136
+ )
1137
+
1138
+ hints = get_type_hints(register_fn)
1139
+ model = hints.get(parameters[1].name)
1140
+ if not (isinstance(model, type) and issubclass(model, BaseModel)):
1141
+ raise PackSettingsError(
1142
+ f"'{pack}' does not annotate its settings parameter with a "
1143
+ f"pydantic.BaseModel subclass; register(registrar, settings: Settings) is required."
1144
+ )
1145
+
1146
+ try:
1147
+ return model.model_validate(raw)
1148
+ except ValidationError as exc:
1149
+ raise PackSettingsError(f"'{pack}' settings failed validation: {exc}") from exc
1150
+
1151
+
1152
+ def _distribution_name(entry_point: EntryPointLike) -> str:
1153
+ dist = entry_point.dist
1154
+ if dist is None:
1155
+ raise MissingDistributionMetadataError(
1156
+ f"entry point '{entry_point.name}' in group '{ENTRY_POINT_GROUP}' carries no "
1157
+ f"distribution metadata; weft cannot attribute it to a pack."
1158
+ )
1159
+ return dist.name
1160
+
1161
+
1162
+ def _as_settings_source(value: object) -> Mapping[str, Mapping[str, object]]:
1163
+ """Narrow `interpolate_env`'s `object` return back to the shape `discover()` needs.
1164
+
1165
+ `interpolate_env` is generic over any parsed config value, so its return
1166
+ type is `object`; `discover()` always feeds it a `dict[str, dict[str,
1167
+ object]]`, and gets exactly that back, since interpolation preserves
1168
+ container shape. This function states that fact once rather than
1169
+ scattering `cast` at every call site.
1170
+ """
1171
+ if not isinstance(value, dict):
1172
+ raise WeftError(f"pack settings must be a mapping; found {type(value).__name__}.")
1173
+ raw = cast("dict[str, object]", value)
1174
+ result: dict[str, Mapping[str, object]] = {}
1175
+ for key, item in raw.items():
1176
+ if not isinstance(item, Mapping):
1177
+ raise WeftError(
1178
+ f"pack settings for '{key}' must be a mapping; found {type(item).__name__}."
1179
+ )
1180
+ result[key] = cast("Mapping[str, object]", item)
1181
+ return result