Problem statement
src/contextweaver/_utils.py:tokenize (the single source of truth per docs/agent-context/invariants.md) is regex-driven on word boundaries. A query like "search invoices by customer" against tool ID billing.invoices.search matches via tags and description, but not via the canonical name token — billing.invoices.search lexes as one token. The gold set IDs are uniformly dotted (admin.audit.export, crm.deals.search, analytics.dashboards.list, …).
Splitting at dots/underscores/hyphens within tokens — while keeping the original token — is a small, deterministic change that likely lifts R@5 substantially on this corpus. The lift should be measurable on multiple backends.
Proposed solution
In tokenize(), after normalisation, additionally split each token at ., _, -, / boundaries and emit both the original token and its sub-tokens. Stay deterministic, stay stdlib. Add tests asserting tokenize("crm.deals.search") contains {"crm.deals.search", "crm", "deals", "search"}. Measure impact via the per-backend matrix on the current v0.3.0 baseline.
Round 2 Q2=C strict-regression-direction-only-lift posture: regression bounds are strict (no cell may drop more than 1pp); lift target is direction-only (≥1 cell must improve, magnitude not pre-specified).
Alternatives considered
- Light stemming (Porter, snowball). Rejected — adds nontrivial complexity for a corpus where the high-leverage split is mechanical (delimiter-based), not morphological.
- Tokenizer protocol abstraction. Rejected — the single-source-of-truth invariant exists for a reason; pluggability is a future concern. Keep the change minimal.
- Change
STOPWORDS instead. Rejected — the issue isn't stopword filtering; the issue is that compound IDs never get split.
Affected modules
src/contextweaver/_utils.py (the tokenize function only), tests/test_utils.py
Estimated effort
S (1–2 days)
Baseline
Current R@5 on 50-query gold (TF-IDF): 0.74 / 0.61 / 0.31 at sizes 50 / 83 / 1000 (benchmarks/README.md:60-65).
After the per-backend matrix issue lands, baseline expands to 9 cells (3 backends × 3 sizes).
Success metric
- Direction-only lift target: R@5 must improve on at least one matrix cell (any backend, any size).
- Strict regression bound: no matrix cell may drop more than 1pp on R@5.
- Strict latency bound: no matrix cell may exceed
baseline_p99 × 1.30 (Round 2 Q5=C).
Evaluation plan
Run make benchmark-matrix before and after; record both latest.jsons. The scorecard delta (from the regression-PR-comment issue) is the primary evidence. Paste the before/after table into the PR description (Round 2 Q3=B Reproducibility subsection).
Scope
src/contextweaver/_utils.py (tokenize only) + tests/test_utils.py + acceptance evidence in PR description.
Non-goals
- No stemming.
- No language detection.
- No tokenizer protocol abstraction.
- No router changes.
- No new public API.
- No change to
STOPWORDS.
Design constraints
- Deterministic. Same input → same set across N=100 calls.
- Sub-tokens emitted in addition to (not in place of) the original token.
STOPWORDS still applied to all tokens (original + sub-tokens).
- Single source of truth invariant (
docs/agent-context/invariants.md) preserved.
- No new dependencies.
Depends on
- Per-backend × per-size benchmark matrix (needed to measure impact across backends).
- Soft regression PR comment (makes the PR review trivial — paste the comment into the PR body).
Suggested order
After the matrix + regression-comment issues land. Cannot ship without the matrix to prove no per-backend regression.
Acceptance Criteria (STRICT)
Validation
# Before merging, on the PR branch:
make benchmark-matrix
make scorecard
git diff benchmarks/scorecard.md # paste into PR body
The regression PR comment will fire automatically on push.
Files likely touched
src/contextweaver/_utils.py, tests/test_utils.py
Risk / rollback
Low — local change to one function. Revert is a single git revert; existing baselines preserved.
Security/privacy notes
None. Tokenizer is pure text transform with no I/O.
Agent-ready notes
- Starting points:
src/contextweaver/_utils.py:tokenize — single function to modify. tests/test_utils.py for test patterns.
- Constraints: do NOT introduce a
TokenizerProtocol here (out of scope). Do NOT change STOPWORDS. Determinism is a hard invariant — must be preserved.
- Review focus for Copilot code review:
- Determinism on edge cases (empty string, single dot, trailing punctuation, unicode).
- Sub-tokens go through
STOPWORDS filtering (don't accidentally leak filtered words back in).
- Performance: the additional splitting should not measurably affect p99 latency (verify via scorecard).
- The split must not change behavior for non-dotted tokens (existing 871 tests cover this).
Cross-references
- Composes with the per-backend × per-size matrix and the regression PR comment — both are prerequisites for measuring impact safely.
- First product feature in the cycle that's net-new (not previously tracked as a GitHub issue).
Size
S
Problem statement
src/contextweaver/_utils.py:tokenize(the single source of truth perdocs/agent-context/invariants.md) is regex-driven on word boundaries. A query like"search invoices by customer"against tool IDbilling.invoices.searchmatches via tags and description, but not via the canonical name token —billing.invoices.searchlexes as one token. The gold set IDs are uniformly dotted (admin.audit.export,crm.deals.search,analytics.dashboards.list, …).Splitting at dots/underscores/hyphens within tokens — while keeping the original token — is a small, deterministic change that likely lifts R@5 substantially on this corpus. The lift should be measurable on multiple backends.
Proposed solution
In
tokenize(), after normalisation, additionally split each token at.,_,-,/boundaries and emit both the original token and its sub-tokens. Stay deterministic, stay stdlib. Add tests assertingtokenize("crm.deals.search")contains{"crm.deals.search", "crm", "deals", "search"}. Measure impact via the per-backend matrix on the current v0.3.0 baseline.Round 2 Q2=C strict-regression-direction-only-lift posture: regression bounds are strict (no cell may drop more than 1pp); lift target is direction-only (≥1 cell must improve, magnitude not pre-specified).
Alternatives considered
STOPWORDSinstead. Rejected — the issue isn't stopword filtering; the issue is that compound IDs never get split.Affected modules
src/contextweaver/_utils.py(thetokenizefunction only),tests/test_utils.pyEstimated effort
S (1–2 days)
Baseline
Current R@5 on 50-query gold (TF-IDF): 0.74 / 0.61 / 0.31 at sizes 50 / 83 / 1000 (
benchmarks/README.md:60-65).After the per-backend matrix issue lands, baseline expands to 9 cells (3 backends × 3 sizes).
Success metric
baseline_p99 × 1.30(Round 2 Q5=C).Evaluation plan
Run
make benchmark-matrixbefore and after; record bothlatest.jsons. The scorecard delta (from the regression-PR-comment issue) is the primary evidence. Paste the before/after table into the PR description (Round 2 Q3=B Reproducibility subsection).Scope
src/contextweaver/_utils.py(tokenizeonly) +tests/test_utils.py+ acceptance evidence in PR description.Non-goals
STOPWORDS.Design constraints
STOPWORDSstill applied to all tokens (original + sub-tokens).docs/agent-context/invariants.md) preserved.Depends on
Suggested order
After the matrix + regression-comment issues land. Cannot ship without the matrix to prove no per-backend regression.
Acceptance Criteria (STRICT)
tokenize("crm.deals.search")returns a set containing{"crm", "deals", "search"}(plus the dotted form unless it falls in STOPWORDS — it won't)tokenize(x) == tokenize(x)across N=100 callsmake cipasses; all 871+ existing tests still greenValidation
The regression PR comment will fire automatically on push.
Files likely touched
src/contextweaver/_utils.py,tests/test_utils.pyRisk / rollback
Low — local change to one function. Revert is a single git revert; existing baselines preserved.
Security/privacy notes
None. Tokenizer is pure text transform with no I/O.
Agent-ready notes
src/contextweaver/_utils.py:tokenize— single function to modify.tests/test_utils.pyfor test patterns.TokenizerProtocolhere (out of scope). Do NOT changeSTOPWORDS. Determinism is a hard invariant — must be preserved.STOPWORDSfiltering (don't accidentally leak filtered words back in).Cross-references
Size
S