weft-embed 1.0.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_embed-1.0.0/.gitignore +78 -0
- weft_embed-1.0.0/LICENSE +21 -0
- weft_embed-1.0.0/NOTICE +24 -0
- weft_embed-1.0.0/PKG-INFO +9 -0
- weft_embed-1.0.0/pyproject.toml +16 -0
- weft_embed-1.0.0/src/weft_embed/__init__.py +41 -0
- weft_embed-1.0.0/src/weft_embed/contract.py +66 -0
- weft_embed-1.0.0/src/weft_embed/hash_embedder.py +100 -0
- weft_embed-1.0.0/src/weft_embed/py.typed +0 -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
|
weft_embed-1.0.0/LICENSE
ADDED
|
@@ -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.
|
weft_embed-1.0.0/NOTICE
ADDED
|
@@ -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-embed
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: First-party embedding pack. Publishes the Embedder contract.
|
|
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,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "weft-embed"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "First-party embedding pack. Publishes the Embedder contract."
|
|
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
|
+
# The one entry point a pack declares (`docs/02-extension-model.md` section 2).
|
|
11
|
+
[project.entry-points."weft.packs"]
|
|
12
|
+
embed = "weft_embed:register"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""First-party embedding pack.
|
|
2
|
+
|
|
3
|
+
Publishes the `Embedder` contract, in `contract.py`, and registers
|
|
4
|
+
`HashEmbedder` (`hash_embedder.py`) — a deterministic local embedder that is
|
|
5
|
+
**not a quality component**, per `docs/06-phase-0-build.md` step 8. This
|
|
6
|
+
pack exists at all because of a forced conclusion, not a design choice: G4
|
|
7
|
+
forbids a `Store` from embedding, G2 has not placed the embed step, and a
|
|
8
|
+
walking skeleton must not need a model download or an API key to prove that
|
|
9
|
+
indexing produces stored nodes.
|
|
10
|
+
|
|
11
|
+
Registered through the same public `weft.packs` entry point any third-party
|
|
12
|
+
pack uses — fitness function 2 — with no shortcut for being first-party.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
|
|
17
|
+
from weft_embed.contract import EMBEDDER_CONTRACT_VERSION, Embedder
|
|
18
|
+
from weft_embed.hash_embedder import HashEmbedder, HashEmbedderConfig
|
|
19
|
+
from weft_kernel.discovery import PackRegistrar
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Settings(BaseModel):
|
|
23
|
+
"""`weft-embed` takes no pack settings — an empty model is still the required shape."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register(registrar: PackRegistrar, settings: Settings) -> None:
|
|
29
|
+
"""Register `HashEmbedder` as `"hash"` for `Embedder`. The only plugin this pack ships."""
|
|
30
|
+
del settings
|
|
31
|
+
registrar.add(Embedder, "hash", HashEmbedder)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"EMBEDDER_CONTRACT_VERSION",
|
|
36
|
+
"Embedder",
|
|
37
|
+
"HashEmbedder",
|
|
38
|
+
"HashEmbedderConfig",
|
|
39
|
+
"Settings",
|
|
40
|
+
"register",
|
|
41
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The `Embedder` contract — published here, never by the kernel.
|
|
2
|
+
|
|
3
|
+
Specified in `docs/06-phase-0-build.md` step 8. `weft-embed` is the fourth
|
|
4
|
+
pack Phase 0 needs and the plan did not originally anticipate — the reasoning
|
|
5
|
+
is forced, not a design flourish: G4 forbids a `Store` from embedding
|
|
6
|
+
(`docs/02-extension-model.md` → *The store contract family*, "stores never
|
|
7
|
+
embed"), G2 has not decided whether embedding is a pipeline stage or a step
|
|
8
|
+
inside another one, and a walking skeleton must not depend on a model
|
|
9
|
+
download or an API key. Phase 0 puts embedding in the pipeline list as its
|
|
10
|
+
own stage — `06`'s minimal, reversible choice for G2's first trap, not an
|
|
11
|
+
answer to G2 — which is exactly what forces this contract to exist somewhere
|
|
12
|
+
a `Store` cannot own it.
|
|
13
|
+
|
|
14
|
+
**`Embedder` needs no boundary type of its own**, for the same reason
|
|
15
|
+
`weft_chunk.contract.Chunker` does not: `docs/02-extension-model.md` →
|
|
16
|
+
*Composition is typed and checked at load* states the ingest path is
|
|
17
|
+
`Stage[Seq[Node], Seq[Node]]` throughout, and embedding a `Node` that already
|
|
18
|
+
has content but no vector yet is squarely inside that stretch — a `Node` in,
|
|
19
|
+
the same `Node` with `embedding` set (via `Node.with_embedding`) out.
|
|
20
|
+
|
|
21
|
+
**`Embedder` declares `Stage[Sequence[Node], Sequence[Node]]` as one of its
|
|
22
|
+
own bases**, per `weft_kernel.runner`'s documented convention — the linear
|
|
23
|
+
runner reads a pipeline stage's `In`/`Out` off the *contract* named in its
|
|
24
|
+
`StageSpec`, via `__orig_bases__`, never off the plugin implementing it.
|
|
25
|
+
`@runtime_checkable` makes capability checkable by `isinstance` rather than
|
|
26
|
+
by a declared flag, the same property every other Phase 0 contract shares.
|
|
27
|
+
`Embedder.__protocol_attrs__` is exactly `{'run'}` — `Stage` itself declares
|
|
28
|
+
nothing beyond `run` (see `weft_kernel.runner`'s module docstring), so a
|
|
29
|
+
plugin implementing only `run` satisfies `Embedder`, full stop.
|
|
30
|
+
|
|
31
|
+
`version` is readable off the class (`Embedder.version`) but is not part of
|
|
32
|
+
that `isinstance` membership — see `weft_extract.contract`'s module
|
|
33
|
+
docstring for the full reasoning behind the `if TYPE_CHECKING:` /
|
|
34
|
+
assign-after-the-class-body split, which applies here unchanged.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
|
|
39
|
+
|
|
40
|
+
from weft_kernel.context import Context
|
|
41
|
+
from weft_kernel.payload import Node, Outcome
|
|
42
|
+
from weft_kernel.runner import Stage
|
|
43
|
+
|
|
44
|
+
#: Fitness function 6's subject for this contract — see the module docstring.
|
|
45
|
+
EMBEDDER_CONTRACT_VERSION = "1.0.0"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class Embedder(Stage[Sequence[Node], Sequence[Node]], Protocol):
|
|
50
|
+
"""Attaches an embedding to each `Node` it is handed.
|
|
51
|
+
|
|
52
|
+
One method, domain types on both sides, exactly `Chunker`'s shape one
|
|
53
|
+
stage later in the pipeline. An embedder that finds nothing to embed
|
|
54
|
+
(an empty batch) answers `NothingToProduce`, not an empty `Produced([])`
|
|
55
|
+
— the same donor-trap fix every other Phase 0 contract documents.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
#: See the module docstring — declared only for a type checker, assigned for real
|
|
60
|
+
#: after the class body, so it never joins `__protocol_attrs__`.
|
|
61
|
+
version: ClassVar[str]
|
|
62
|
+
|
|
63
|
+
async def run(self, payload: Sequence[Node], ctx: Context) -> Outcome[Sequence[Node]]: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Embedder.version = EMBEDDER_CONTRACT_VERSION
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""`HashEmbedder` — a deterministic, content-hashed vector. **Not a quality embedder.**
|
|
2
|
+
|
|
3
|
+
Specified in `docs/06-phase-0-build.md` step 8: "Phase 0 ships a deterministic
|
|
4
|
+
local embedder (hashing to a fixed-dimension vector) whose only job is to be
|
|
5
|
+
a real vector produced by a real stage. It is not a quality component and
|
|
6
|
+
its docstring should say so." This is that docstring, said plainly: `HashEmbedder`
|
|
7
|
+
carries no semantic understanding of content whatsoever. Two documents about
|
|
8
|
+
unrelated topics that happen to hash to nearby components are not "similar"
|
|
9
|
+
in any sense a retrieval strategy should trust — this class exists only so
|
|
10
|
+
that Phase 0's ingest pipeline has a real `Vector`, produced by a real stage,
|
|
11
|
+
flowing into a real `NodeStore.search_vector` call, with no model download
|
|
12
|
+
and no API key standing between a clean checkout and a passing test. Replace
|
|
13
|
+
it with a real embedding model before trusting a single retrieval result
|
|
14
|
+
against it.
|
|
15
|
+
|
|
16
|
+
**Deterministic by construction, not by convention.** Every component is a
|
|
17
|
+
SHA-256 digest of the node's content concatenated with that component's own
|
|
18
|
+
index, so the same content always hashes to the same vector — required for
|
|
19
|
+
`weft index`'s re-index-is-idempotent property (`docs/02-extension-model.md`
|
|
20
|
+
→ *Identity is a content-addressed digest*) to hold all the way through
|
|
21
|
+
storage, not just at the `Node` layer. It is not, and must never become,
|
|
22
|
+
a hash-based *approximation* of semantic similarity: nothing here reads
|
|
23
|
+
tokens, n-grams or any structure in `content` at all.
|
|
24
|
+
|
|
25
|
+
**Configuration is `dimension`, and nothing else.** No model name, no API
|
|
26
|
+
endpoint, no batching knob — there is no model to name, and pretending
|
|
27
|
+
otherwise (a `model: "text-embedding-3-small"` field that is silently
|
|
28
|
+
ignored) would be exactly the kind of plausible-but-false configuration
|
|
29
|
+
surface the project's catch-specific-exceptions rule already refuses at the
|
|
30
|
+
error-handling layer. `dimension` is real: it is the length of the vector
|
|
31
|
+
this stage actually produces.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
from collections.abc import Sequence
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
38
|
+
|
|
39
|
+
from weft_kernel.context import Context
|
|
40
|
+
from weft_kernel.payload import Node, NothingToProduce, Outcome, Produced, Vector
|
|
41
|
+
|
|
42
|
+
#: Matches `weft-store`'s pgvector column, which is declared without a fixed
|
|
43
|
+
#: dimension — see `docs/06-phase-0-build.md` step 8 and `weft_store.pgvector_store`.
|
|
44
|
+
#: Any positive dimension works; this is only the default a `with:` block may omit.
|
|
45
|
+
_DEFAULT_DIMENSION = 64
|
|
46
|
+
|
|
47
|
+
#: The width of the slice of each SHA-256 digest this module turns into one component.
|
|
48
|
+
_UINT64_RANGE = 2**64
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class HashEmbedderConfig(BaseModel):
|
|
52
|
+
"""`HashEmbedder`'s `with:` config — one real field. See the module docstring for why."""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
55
|
+
|
|
56
|
+
dimension: int = _DEFAULT_DIMENSION
|
|
57
|
+
|
|
58
|
+
@model_validator(mode="after")
|
|
59
|
+
def _dimension_is_positive(self) -> "HashEmbedderConfig":
|
|
60
|
+
if self.dimension < 1:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"dimension must be at least 1 — a Vector cannot be empty (got {self.dimension})"
|
|
63
|
+
)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HashEmbedder:
|
|
68
|
+
"""Attaches a deterministic, content-hashed `Vector` to every node it is handed.
|
|
69
|
+
|
|
70
|
+
**Not a quality component** — see the module docstring. Satisfies
|
|
71
|
+
`weft_embed.contract.Embedder` structurally: this class never imports it,
|
|
72
|
+
the same path every third-party embedder pack is expected to take.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, config: HashEmbedderConfig | None = None) -> None:
|
|
76
|
+
self._config = config if config is not None else HashEmbedderConfig()
|
|
77
|
+
|
|
78
|
+
async def run(self, payload: Sequence[Node], ctx: Context) -> Outcome[Sequence[Node]]:
|
|
79
|
+
del ctx # no service or locale this stage needs
|
|
80
|
+
if not payload:
|
|
81
|
+
return NothingToProduce(reason="no nodes to embed")
|
|
82
|
+
embedded = [
|
|
83
|
+
node.with_embedding(_hash_vector(node.content, self._config.dimension))
|
|
84
|
+
for node in payload
|
|
85
|
+
]
|
|
86
|
+
return Produced(value=embedded)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _hash_vector(content: str, dimension: int) -> Vector:
|
|
90
|
+
"""`dimension` components, each a deterministic function of `content` and its own index."""
|
|
91
|
+
encoded = content.encode("utf-8")
|
|
92
|
+
components = tuple(_component(encoded, index) for index in range(dimension))
|
|
93
|
+
return Vector(values=components)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _component(encoded: bytes, index: int) -> float:
|
|
97
|
+
"""One component: a SHA-256 digest of `encoded` and `index`, mapped onto `[-1.0, 1.0)`."""
|
|
98
|
+
digest = hashlib.sha256(encoded + index.to_bytes(4, byteorder="big")).digest()
|
|
99
|
+
raw = int.from_bytes(digest[:8], byteorder="big")
|
|
100
|
+
return (raw / _UINT64_RANGE) * 2.0 - 1.0
|
|
File without changes
|