weft-kernel 0.1.0__tar.gz → 0.2.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_kernel-0.1.0 → weft_kernel-0.2.0}/.gitignore +7 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/PKG-INFO +1 -1
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/pyproject.toml +1 -1
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/context.py +18 -7
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/discovery.py +46 -7
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/__init__.py +2 -1
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/ext.py +1 -1
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/node.py +28 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/pipeline.py +2 -2
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/resolution.py +1 -1
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/seam.py +77 -10
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/LICENSE +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/NOTICE +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/README.md +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/__init__.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/blocking.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/errors.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/fallback.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/applicability.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/ids.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/lineage.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/media_type.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/outcome.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/property.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/payload/vector.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/py.typed +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/registry.py +0 -0
- {weft_kernel-0.1.0 → weft_kernel-0.2.0}/src/weft_kernel/runner.py +0 -0
|
@@ -83,3 +83,10 @@ build/
|
|
|
83
83
|
/docs/_external-reading/
|
|
84
84
|
|
|
85
85
|
/tmp/
|
|
86
|
+
|
|
87
|
+
# The internal process record — how this project is worked, not what it ships. The build ledger,
|
|
88
|
+
# the lessons queue and its archive, the grilling sessions, the product direction, the roadmap past
|
|
89
|
+
# Phase 11, the control file that holds project state, and the owner's own list. Untracked
|
|
90
|
+
# 2026-09-11 at the owner's decision. The numbered reference documents stay tracked: 650 citations
|
|
91
|
+
# from `packages/` point at them and those pointers ship inside the wheel.
|
|
92
|
+
/docs/internal/
|
|
@@ -38,7 +38,7 @@ language axis it already has. A locale-keyed message store with one locale is
|
|
|
38
38
|
a dict with a constant key, so the catalogue, `Context.messages`, `t()` and
|
|
39
39
|
the three error classes they brought (`UnknownMessageError`,
|
|
40
40
|
`DuplicateMessageError`, `MessageFormatError`) are gone, taking this kernel
|
|
41
|
-
from 33 error classes to 30. `docs/05-grilling-sessions.md` → G11 holds the
|
|
41
|
+
from 33 error classes to 30. `docs/internal/05-grilling-sessions.md` → G11 holds the
|
|
42
42
|
session; `docs/02-extension-model.md` §1 owns what replaced it — an English
|
|
43
43
|
literal at the raise site, whose explanation surface is
|
|
44
44
|
`manual/troubleshooting.md`'s coverage ratchet, and whose *quality* is
|
|
@@ -70,7 +70,7 @@ import asyncio
|
|
|
70
70
|
from dataclasses import dataclass, field
|
|
71
71
|
from typing import cast
|
|
72
72
|
|
|
73
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
73
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
74
74
|
|
|
75
75
|
from weft_kernel.errors import UnresolvedNameError, WeftError
|
|
76
76
|
|
|
@@ -108,11 +108,11 @@ class ServiceRole(BaseModel):
|
|
|
108
108
|
"""A pack's declaration, beside the contract it publishes, that `[services].<key>`
|
|
109
109
|
selects an implementation of that contract for one run.
|
|
110
110
|
|
|
111
|
-
Ledger task **9.0**, closing the hole `docs/02-extension-model.md` §1 named in its own
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
Ledger task **9.0**, closing the hole `docs/02-extension-model.md` §1 named in its own Phase 0
|
|
112
|
+
narrowing: a service is populated into a `ServiceRegistry` by whatever assembles a run, but
|
|
113
|
+
nothing let a pack *name* which of its contracts is selectable that way, or under what
|
|
114
|
+
`[services]` key. `ServiceRole` is that declaration — "one constant beside the Protocol"
|
|
115
|
+
(`docs/internal/build-ledger.md:5054 'exists becaus'`, `:5370`), never a member on the Protocol
|
|
116
116
|
itself.
|
|
117
117
|
|
|
118
118
|
It is a plain constant rather than a `ClassVar` written into the contract's own body,
|
|
@@ -138,6 +138,17 @@ class ServiceRole(BaseModel):
|
|
|
138
138
|
key: str = Field(min_length=1)
|
|
139
139
|
contract: type[object]
|
|
140
140
|
|
|
141
|
+
@field_serializer("contract")
|
|
142
|
+
def _contract_as_name(self, contract: type[object]) -> str:
|
|
143
|
+
"""The contract's qualified name — ledger task **24.5**.
|
|
144
|
+
|
|
145
|
+
A role names a live Protocol because `ctx.require(...)` resolves against the class
|
|
146
|
+
itself; what a reader of `weft --json plugins list` can act on is which contract, which
|
|
147
|
+
is what a qualified name says. Same rule as `PackReport.ext_models` and
|
|
148
|
+
`RendererOffer.result_type`: what leaves is an identity, never the object.
|
|
149
|
+
"""
|
|
150
|
+
return f"{contract.__module__}.{contract.__qualname__}"
|
|
151
|
+
|
|
141
152
|
|
|
142
153
|
class ServiceRegistry:
|
|
143
154
|
"""Per-run map of `contract -> the one resolved instance a stage gets back.`
|
|
@@ -85,7 +85,7 @@ about stores: it is inert data until something that *does* know what a store
|
|
|
85
85
|
is reads `PackReport.ext_models` back off every report and registers each
|
|
86
86
|
class with the namespace-to-class registry that actually rehydrates one —
|
|
87
87
|
`weft_store.rehydrate.register_from_reports`, called once, generically, by
|
|
88
|
-
whatever already calls `discover()` (`
|
|
88
|
+
whatever already calls `discover()` (`weft_engine.registry_bootstrap.
|
|
89
89
|
build_dependencies`). No pack-specific knowledge sits in that call site: it
|
|
90
90
|
walks whatever `PackReport.ext_models` any report carries, so a future pack
|
|
91
91
|
shipping a new `ExtModel` needs no edit here and no edit in `weft-cli` at all.
|
|
@@ -100,7 +100,7 @@ value the kernel already owns (`weft_kernel.resolution` is this same distributio
|
|
|
100
100
|
not a capability, so this teaches the kernel nothing new either: it stops at "this
|
|
101
101
|
pack offered this contribution," and `PackReport.contributions` is read back off
|
|
102
102
|
every report by whatever assembles a `resolve()` call's own `contributions=` tuple —
|
|
103
|
-
`
|
|
103
|
+
`weft_engine.registry_bootstrap.build_dependencies`, the same caller `Contribution`'s
|
|
104
104
|
own docstring names, now real. No pack-specific knowledge sits there either: it
|
|
105
105
|
concatenates whatever `PackReport.contributions` every report carries, so a future
|
|
106
106
|
pack contributing into a slot needs no edit here and no edit in `weft-cli` at all.
|
|
@@ -130,7 +130,7 @@ from enum import StrEnum
|
|
|
130
130
|
from importlib import metadata
|
|
131
131
|
from typing import Protocol, cast, get_type_hints
|
|
132
132
|
|
|
133
|
-
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
133
|
+
from pydantic import BaseModel, ConfigDict, ValidationError, field_serializer
|
|
134
134
|
|
|
135
135
|
from weft_kernel.context import ServiceRole
|
|
136
136
|
from weft_kernel.errors import UnresolvedNameError, WeftError
|
|
@@ -223,6 +223,26 @@ class RendererOffer(BaseModel):
|
|
|
223
223
|
result_type: type[object]
|
|
224
224
|
render: Callable[[object], object]
|
|
225
225
|
|
|
226
|
+
@field_serializer("result_type")
|
|
227
|
+
def _result_type_as_name(self, result_type: type[object]) -> str:
|
|
228
|
+
"""The type's qualified name — ledger task **24.5**, and the same rule as
|
|
229
|
+
`PackReport.ext_models`: what leaves is an identity, never the live object.
|
|
230
|
+
|
|
231
|
+
This field holds a real class because `weft_cli.render`'s dispatch keys on it, and a
|
|
232
|
+
string could not do that. `weft --json plugins list` asked it to serialise and pydantic
|
|
233
|
+
refused the whole report, so the command exited 1 printing nothing — which is how a
|
|
234
|
+
field nobody had ever written down announces itself.
|
|
235
|
+
"""
|
|
236
|
+
return f"{result_type.__module__}.{result_type.__qualname__}"
|
|
237
|
+
|
|
238
|
+
@field_serializer("render")
|
|
239
|
+
def _render_as_name(self, render: Callable[[object], object]) -> str:
|
|
240
|
+
"""Likewise, for the callable. A function has no wire form; what a reader of the JSON
|
|
241
|
+
can act on is which one it is, which is what a qualified name says."""
|
|
242
|
+
module = getattr(render, "__module__", "?")
|
|
243
|
+
name = getattr(render, "__qualname__", repr(render))
|
|
244
|
+
return f"{module}.{name}"
|
|
245
|
+
|
|
226
246
|
|
|
227
247
|
class ServiceRoleOffer(BaseModel):
|
|
228
248
|
"""One `ServiceRole` a pack declared, attributed to the pack that declared it.
|
|
@@ -288,7 +308,7 @@ class PackReport(BaseModel):
|
|
|
288
308
|
|
|
289
309
|
`contributions` — task 5.3a — is every `weft_kernel.resolution.Contribution` the pack
|
|
290
310
|
buffered through `PackRegistrar.add_contribution`, empty for any pack that offers no
|
|
291
|
-
slot contribution (most packs). `
|
|
311
|
+
slot contribution (most packs). `weft_engine.registry_bootstrap.build_dependencies` is the
|
|
292
312
|
one place every report's own tuple is concatenated into the `contributions=` argument
|
|
293
313
|
every `weft_kernel.resolution.resolve` call site now passes — `02` §3 → *Slots*: "a pack
|
|
294
314
|
may... contribute into a slot a pipeline opted into."
|
|
@@ -321,6 +341,25 @@ class PackReport(BaseModel):
|
|
|
321
341
|
deprecations: tuple[Deprecation, ...] = ()
|
|
322
342
|
unavailable: tuple[Unavailable, ...] = ()
|
|
323
343
|
ext_models: tuple[type[ExtModel], ...] = ()
|
|
344
|
+
|
|
345
|
+
@field_serializer("ext_models")
|
|
346
|
+
def _ext_models_as_namespaces(self, models: tuple[type[ExtModel], ...]) -> tuple[str, ...]:
|
|
347
|
+
"""The namespaces, not the classes — ledger task **24.5**.
|
|
348
|
+
|
|
349
|
+
This field holds live classes on purpose: `weft_store.rehydrate.register_from_reports`
|
|
350
|
+
reads them back to teach a store how to rehydrate a pack's own `ext` namespaces, and a
|
|
351
|
+
string could not do that. What a class cannot do is *serialise*, and until 24.5 nothing
|
|
352
|
+
ever asked it to — `weft --json plugins list` then did, and pydantic refused the whole
|
|
353
|
+
report with `PydanticSerializationError: Unable to serialize unknown type:
|
|
354
|
+
ModelMetaclass`, taking the command down with it. Found by running the binary; the 2,749
|
|
355
|
+
tests could not see it, because none of them dumps a `PackReport`.
|
|
356
|
+
|
|
357
|
+
`__namespace__` is the identity a reader of the JSON wants and the one `02` § *The
|
|
358
|
+
payload model* says is collision-free by construction. The in-memory value is untouched:
|
|
359
|
+
a serializer decides what leaves, never what the field holds.
|
|
360
|
+
"""
|
|
361
|
+
return tuple(model.__namespace__ for model in models)
|
|
362
|
+
|
|
324
363
|
contributions: tuple[Contribution, ...] = ()
|
|
325
364
|
renderers: tuple[RendererOffer, ...] = ()
|
|
326
365
|
service_roles: tuple[ServiceRoleOffer, ...] = ()
|
|
@@ -371,7 +410,7 @@ class InertPluginPinError(WeftError):
|
|
|
371
410
|
parameter is what a diagnostic caller sets `False` to see every
|
|
372
411
|
`PackReport` anyway; `weft_cli.cli.dispatch` does this for `plugins
|
|
373
412
|
list`/`plugins doctor` and no other command, on the same reasoning
|
|
374
|
-
`
|
|
413
|
+
`weft_engine.registry_bootstrap`'s own module docstring already gives for
|
|
375
414
|
`WEFT_DATABASE_URL`: "a bare crash on every registry-needing command —
|
|
376
415
|
including `plugins doctor`, the one command meant to diagnose exactly
|
|
377
416
|
this — would be worse than" a report.
|
|
@@ -969,7 +1008,7 @@ def _activate(
|
|
|
969
1008
|
identical terms, for the identical reason: a pack that raises must never look like it
|
|
970
1009
|
offered a slot contribution it never actually committed. This function does nothing else
|
|
971
1010
|
with it either — assembling every report's own tuple into one `contributions=` argument
|
|
972
|
-
for `weft_kernel.resolution.resolve` is `
|
|
1011
|
+
for `weft_kernel.resolution.resolve` is `weft_engine.registry_bootstrap.build_dependencies`'s
|
|
973
1012
|
job, the one caller `weft_kernel.resolution.Contribution`'s own docstring names.
|
|
974
1013
|
|
|
975
1014
|
**Task 6.20** — `registrar.renderers` reaches `PackReport.renderers` on the identical
|
|
@@ -1089,7 +1128,7 @@ def _read_service_roles(
|
|
|
1089
1128
|
declaration been buffered through `register()`, `[services] store = "qdrant"` would then be
|
|
1090
1129
|
refused as an *unknown key*, naming the wrong problem entirely, on exactly the machine where
|
|
1091
1130
|
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 `
|
|
1131
|
+
parses, and the plugin name fails later through `weft_engine.registry_bootstrap.require_plugin`,
|
|
1093
1132
|
which names the pack and its reason — is the better error, and reading the declaration here
|
|
1094
1133
|
is what preserves it.
|
|
1095
1134
|
|
|
@@ -17,7 +17,7 @@ from weft_kernel.payload.ext import SCHEMA_VERSION_KEY, ExtMap, ExtModel, Schema
|
|
|
17
17
|
from weft_kernel.payload.ids import NodeId, SourceId
|
|
18
18
|
from weft_kernel.payload.lineage import Lineage
|
|
19
19
|
from weft_kernel.payload.media_type import MediaType
|
|
20
|
-
from weft_kernel.payload.node import Node, SyntheticOrigin
|
|
20
|
+
from weft_kernel.payload.node import Node, SyntheticOrigin, carry_forward
|
|
21
21
|
from weft_kernel.payload.outcome import Failed, NothingToProduce, Outcome, Produced
|
|
22
22
|
from weft_kernel.payload.property import Property
|
|
23
23
|
from weft_kernel.payload.vector import Vector
|
|
@@ -40,4 +40,5 @@ __all__ = [
|
|
|
40
40
|
"SourceId",
|
|
41
41
|
"SyntheticOrigin",
|
|
42
42
|
"Vector",
|
|
43
|
+
"carry_forward",
|
|
43
44
|
]
|
|
@@ -15,7 +15,7 @@ call site.
|
|
|
15
15
|
|
|
16
16
|
**Built in Phase 5 task 5.2c.** `__schema_version__` is a second mandatory
|
|
17
17
|
declaration, checked in the same `__pydantic_init_subclass__` seam as
|
|
18
|
-
`__namespace__` and for the identical reason — G9's ruling (`docs/README.md`
|
|
18
|
+
`__namespace__` and for the identical reason — G9's ruling (`docs/internal/README.md`
|
|
19
19
|
decision log, `docs/02-extension-model.md` §1): a contract version cannot
|
|
20
20
|
stand in for it because it is not available at the read site, so an
|
|
21
21
|
`ExtModel` carries its own version, and a missing one fails loudly at class
|
|
@@ -235,6 +235,34 @@ class Node(BaseModel):
|
|
|
235
235
|
return self._replace(embedding=embedding)
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
def carry_forward(child: Node, *, parent: Node) -> Node:
|
|
239
|
+
"""`child`, plus every namespace `parent.ext` carries except its root-origin marker.
|
|
240
|
+
|
|
241
|
+
`SyntheticOrigin` is excluded by name: it states that a node has no real lineage, and
|
|
242
|
+
`child` was just given one — by `Node.derive`, whichever stage called this — so copying
|
|
243
|
+
it forward would attach a claim about `child` that is false the moment it is read.
|
|
244
|
+
Every other namespace is copied verbatim, last-write-wins is never a concern here since
|
|
245
|
+
`child` carries no `ext` of its own yet (`derive` starts it empty); a caller that wants
|
|
246
|
+
its own fact to win over a carried-forward one — `weft_chunk.table_rows` does, for its
|
|
247
|
+
row's own `TableGrid` — attaches it with `with_ext` *after* calling this.
|
|
248
|
+
|
|
249
|
+
**G17, settled 2026-09-12.** Moved here from `weft_chunk.carry` — with a byte-identical
|
|
250
|
+
private copy in `weft_vision.describe_figure` whose own docstring cited
|
|
251
|
+
`weft_chunk.fixed_size._carry_forward`, a function that has not existed since task `9.14`
|
|
252
|
+
lifted it — because G17 adds a third consumer: every `weft_clean` cleaner rebuilds its
|
|
253
|
+
node with `Node.derive`, which drops `ext`, and `R9.1` is the property that a `TEXT`
|
|
254
|
+
node's extraction-time facts survive that. `weft_clean`, `weft_chunk` and `weft_vision`
|
|
255
|
+
import none of each other, so the only module all three already depend on is the kernel,
|
|
256
|
+
and the function belongs there on `01`'s own test: it names no capability. It is an
|
|
257
|
+
operation on `Node.ext` and `SyntheticOrigin`, both of them kernel types.
|
|
258
|
+
"""
|
|
259
|
+
for namespace, model in parent.ext.items():
|
|
260
|
+
if namespace == SyntheticOrigin.__namespace__:
|
|
261
|
+
continue
|
|
262
|
+
child = child.with_ext(model)
|
|
263
|
+
return child
|
|
264
|
+
|
|
265
|
+
|
|
238
266
|
def _content_digest(
|
|
239
267
|
*, media_type: MediaType, content: str, parent_ids: Sequence[NodeId], ordinal: int
|
|
240
268
|
) -> NodeId:
|
|
@@ -79,7 +79,7 @@ already preserves call order into the `**data` dict `BaseModel.__init__`
|
|
|
79
79
|
builds, which is the same dict a document's loader hands to
|
|
80
80
|
`model_validate`, so there is exactly one code path computing "the order",
|
|
81
81
|
not one per direction. This is what task 1.4 settles from the note 1.1 left
|
|
82
|
-
open in `docs/build-ledger.md`.
|
|
82
|
+
open in `docs/internal/build-ledger.md`.
|
|
83
83
|
|
|
84
84
|
**`fallback` is data here too.** `02` §1 gives the kernel a fallback combinator
|
|
85
85
|
over any contract, and `11` §4 keeps `fallback:` a per-stage list "tried in
|
|
@@ -241,7 +241,7 @@ PIPELINE_OPERATOR_MARK: Final[str] = "pipeline_operator"
|
|
|
241
241
|
ratchet, and `01` -> *Fitness functions* item 11(a) requires its "actual" side to be
|
|
242
242
|
"derived from the code (the actual operator fields on the model), never a hand-written
|
|
243
243
|
list" — a second tuple of the same four strings, sitting in the test file with no
|
|
244
|
-
structural link back to this class, is exactly the drift `docs/README.md` opens by
|
|
244
|
+
structural link back to this class, is exactly the drift `docs/internal/README.md` opens by
|
|
245
245
|
describing. Each of the four operator fields below carries `Field(...,
|
|
246
246
|
json_schema_extra={PIPELINE_OPERATOR_MARK: True})` for exactly that reason: it is the
|
|
247
247
|
one place in the tree that *decides* a field is an operator block, so the ratchet reads
|
|
@@ -112,7 +112,7 @@ so a leaf's override reaches a var referenced in a `with:` block the root itself
|
|
|
112
112
|
**Operators apply against the *running* stage list, never against the original root.**
|
|
113
113
|
`02` §3: "Operators apply in written order, each validated against the running result."
|
|
114
114
|
That is what makes `remove` followed by `insert` on one id a move rather than a
|
|
115
|
-
collision — task 1.4 settles the question `docs/build-ledger.md` left open after 1.1:
|
|
115
|
+
collision — task 1.4 settles the question `docs/internal/build-ledger.md` left open after 1.1:
|
|
116
116
|
"the order in which those keys appear in the document is the order they apply."
|
|
117
117
|
`weft_kernel.pipeline.Pipeline.operator_order` is read off the document (or the call)
|
|
118
118
|
that built the pipeline, never assumed from field order, and `_apply_operators` below
|
|
@@ -43,7 +43,7 @@ The four concerns:
|
|
|
43
43
|
— is out of scope by construction, not by an exclusion list.
|
|
44
44
|
5. **The NUL-byte sanitiser** — see `_sanitize_control_bytes` below, riding
|
|
45
45
|
the same `Produced` → `Node` / `tuple` / `list` walk `_strip_transient`
|
|
46
|
-
already performs, immediately after it. `docs/build-ledger.md` → **2.34**
|
|
46
|
+
already performs, immediately after it. `docs/internal/build-ledger.md` → **2.34**
|
|
47
47
|
settles where this lives, against two alternatives, with evidence:
|
|
48
48
|
|
|
49
49
|
- **Not in an extractor pack.** Eight sites across `packages/` build a
|
|
@@ -51,7 +51,7 @@ The four concerns:
|
|
|
51
51
|
text.py:80 'Node.syn'`, `weft_pdf/document.py:205 'rows: tu'`,
|
|
52
52
|
`weft_chunk/fixed_size.py:117 'destroys'
|
|
53
53
|
'destroys'`,
|
|
54
|
-
`weft_clean/dictionary_spacing.py:
|
|
54
|
+
`weft_clean/dictionary_spacing.py:116-117 'intact: '`,
|
|
55
55
|
`weft_clean/hyphenation.py:71-72 'intact: '
|
|
56
56
|
'intact:'`,
|
|
57
57
|
`weft_clean/whitespace.py:64-65 'intact: t'`, `weft_clean/table_linearizer.py:79 'destroys:'`,
|
|
@@ -74,11 +74,17 @@ The four concerns:
|
|
|
74
74
|
structural here, read off the span's own attributes, rather than four
|
|
75
75
|
keyword arguments an author has to remember to pass at every call site.
|
|
76
76
|
|
|
77
|
-
**NUL becomes a space, never a deletion**, because
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
**NUL becomes a space, never a deletion**, because this seam rewrites a
|
|
78
|
+
node's content *after* whatever produced it has already described it, and
|
|
79
|
+
a length-changing edit silently invalidates any description that indexes
|
|
80
|
+
into that content by position. A space is one character for one
|
|
81
|
+
character, so every such fact stays correct. **The reason is structural
|
|
82
|
+
rather than live as of 2026-09-12**: `weft_chunk.payload.ChunkOffset` was
|
|
83
|
+
the one shipped `ExtModel` recording a character offset, and repair
|
|
84
|
+
`R17.1` withdrew it once **G17** left it with no reader. Nothing in the
|
|
85
|
+
tree indexes content by position today — which is an argument for
|
|
86
|
+
preserving length cheaply while that is true, not for spending the
|
|
87
|
+
invariant.
|
|
82
88
|
|
|
83
89
|
**Scope is `Node.content` and the `str`-typed fields of whatever
|
|
84
90
|
`ExtModel`s `Node.ext` carries** — `weft_store/pgvector_store.py`'s
|
|
@@ -129,14 +135,14 @@ three concerns still apply: a span, the blocking-call guard, and, for a bare
|
|
|
129
135
|
gives `run()`.
|
|
130
136
|
|
|
131
137
|
**`guard_blocking_calls`, added by `weft-cli` task 3.4 for a caller outside
|
|
132
|
-
`Runner`'s own reach.** `docs/build-ledger.md` 3.2 tried running a
|
|
138
|
+
`Runner`'s own reach.** `docs/internal/build-ledger.md` 3.2 tried running a
|
|
133
139
|
`weft_command.contract.Command` invocation through this function unchanged
|
|
134
140
|
and reverted: `weft index`'s synchronous filesystem walk tripped concern 4,
|
|
135
141
|
which exists because a blocking `Stage` starves an event loop *other stages
|
|
136
142
|
share* — a `Command` is CLI orchestration invoked once per invocation or
|
|
137
143
|
REPL turn, with nothing else scheduled on that loop to starve, so the guard
|
|
138
144
|
was a false positive for it rather than a caught defect. 3.4's own analysis
|
|
139
|
-
(recorded in full in its `docs/build-ledger.md` entry) rejected two other
|
|
145
|
+
(recorded in full in its `docs/internal/build-ledger.md` entry) rejected two other
|
|
140
146
|
shapes for this fact: a second, hand-written span-and-attribution wrapper in
|
|
141
147
|
`weft_cli` (a second implementation of a concern this module already owns,
|
|
142
148
|
free to drift from it) and applying the guard unconditionally (the reverted
|
|
@@ -677,7 +683,7 @@ def _sanitize_ext_model(model: ExtModel) -> tuple[ExtModel, int]:
|
|
|
677
683
|
|
|
678
684
|
Scoped to a field whose *runtime value* is a `str` — not `tuple[str, ...]`
|
|
679
685
|
or `list[str]`, checked by `isinstance` rather than by the field's
|
|
680
|
-
annotation. `docs/build-ledger.md` → 2.34 scopes this task to "`Node.content`
|
|
686
|
+
annotation. `docs/internal/build-ledger.md` → 2.34 scopes this task to "`Node.content`
|
|
681
687
|
and the `str`-typed fields of the `ExtModel`s in `Node.ext`" literally; no
|
|
682
688
|
shipped `ExtModel` as of this task holds a string collection built from
|
|
683
689
|
verbatim extractor text, so widening to collections has no motivating case
|
|
@@ -723,3 +729,64 @@ def _clean_str(value: str) -> tuple[str, int]:
|
|
|
723
729
|
if "\x00" not in value:
|
|
724
730
|
return value, 0
|
|
725
731
|
return value.replace("\x00", " "), value.count("\x00")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
async def aclose(
|
|
735
|
+
instance: object,
|
|
736
|
+
*,
|
|
737
|
+
distribution: str,
|
|
738
|
+
contract: str,
|
|
739
|
+
plugin: str,
|
|
740
|
+
stage: str | None = None,
|
|
741
|
+
) -> None:
|
|
742
|
+
"""Close `instance` if it has an `aclose`, with the same span, guard and attribution
|
|
743
|
+
`wrap_flush` gives `flush`.
|
|
744
|
+
|
|
745
|
+
`aclose` is a fact read off the instance, never a method any contract publishes — an
|
|
746
|
+
in-memory store and every third-party pack that keeps no socket has nothing to close, and
|
|
747
|
+
requiring the method would be a line each of them has to write in order to be reaped. So
|
|
748
|
+
the defensive read and the call are one function rather than two: a caller that still had
|
|
749
|
+
to ask *is there one?* before calling a separate helper would still be the caller deciding,
|
|
750
|
+
and three call sites deciding it independently is exactly the copy this replaces.
|
|
751
|
+
|
|
752
|
+
This is a function callers reach for, never something the runner does on their behalf:
|
|
753
|
+
the instantiator owns the lifetime it opened, and a `Lifetime.PROCESS` instance outlives
|
|
754
|
+
any one run, so closing it is not the runner's to do.
|
|
755
|
+
|
|
756
|
+
`stage` is optional, defaulted to `f"{contract}:{plugin}"` exactly as `wrap` defaults it for
|
|
757
|
+
a caller with no pipeline concept — two of this function's three callers hold no pipeline
|
|
758
|
+
position at all, unlike `wrap_flush`'s one caller, which always has a resolved `StageSpec.id`.
|
|
759
|
+
|
|
760
|
+
No `_strip_transient` and no `_sanitize_control_bytes` — like `flush`, `aclose` returns
|
|
761
|
+
nothing an `Outcome` could decide.
|
|
762
|
+
"""
|
|
763
|
+
found = getattr(instance, "aclose", None)
|
|
764
|
+
if found is None or not callable(found):
|
|
765
|
+
return
|
|
766
|
+
close = cast("Callable[[], Awaitable[None]]", found)
|
|
767
|
+
|
|
768
|
+
label = stage if stage is not None else f"{contract}:{plugin}"
|
|
769
|
+
with _tracer.start_as_current_span(f"{label}:aclose", kind=SpanKind.INTERNAL) as span:
|
|
770
|
+
span.set_attribute("weft.pack", distribution)
|
|
771
|
+
span.set_attribute("weft.contract", contract)
|
|
772
|
+
span.set_attribute("weft.plugin", plugin)
|
|
773
|
+
with blocking.guard(f"{label}:aclose"):
|
|
774
|
+
try:
|
|
775
|
+
await close()
|
|
776
|
+
except WeftError as exc:
|
|
777
|
+
_attribute(
|
|
778
|
+
exc,
|
|
779
|
+
distribution=distribution,
|
|
780
|
+
contract=contract,
|
|
781
|
+
plugin=plugin,
|
|
782
|
+
stage=label,
|
|
783
|
+
)
|
|
784
|
+
raise
|
|
785
|
+
except Exception as exc:
|
|
786
|
+
raise WeftError(
|
|
787
|
+
f"'{label}' close failed: {exc}",
|
|
788
|
+
pack=distribution,
|
|
789
|
+
contract=contract,
|
|
790
|
+
plugin=plugin,
|
|
791
|
+
stage=label,
|
|
792
|
+
) from exc
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|