@untangle-code/search 0.0.0-stage → 0.1.0
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.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +433 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/benchmark-integrity.d.ts +12 -0
- package/dist/benchmark-integrity.d.ts.map +1 -0
- package/dist/benchmark-integrity.js +10 -0
- package/dist/benchmark-integrity.js.map +1 -0
- package/dist/bm25.d.ts +46 -0
- package/dist/bm25.d.ts.map +1 -0
- package/dist/bm25.js +105 -0
- package/dist/bm25.js.map +1 -0
- package/dist/deadline.d.ts +4 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +54 -0
- package/dist/deadline.js.map +1 -0
- package/dist/dedupe.d.ts +48 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +58 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/diversify.d.ts +45 -0
- package/dist/diversify.d.ts.map +1 -0
- package/dist/diversify.js +30 -0
- package/dist/diversify.js.map +1 -0
- package/dist/engine.d.ts +157 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1498 -0
- package/dist/engine.js.map +1 -0
- package/dist/enrichment.d.ts +16 -0
- package/dist/enrichment.d.ts.map +1 -0
- package/dist/enrichment.js +92 -0
- package/dist/enrichment.js.map +1 -0
- package/dist/filter.d.ts +86 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +166 -0
- package/dist/filter.js.map +1 -0
- package/dist/findability.d.ts +144 -0
- package/dist/findability.d.ts.map +1 -0
- package/dist/findability.js +393 -0
- package/dist/findability.js.map +1 -0
- package/dist/format.d.ts +28 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +172 -0
- package/dist/format.js.map +1 -0
- package/dist/fusion.d.ts +47 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/fusion.js +84 -0
- package/dist/fusion.js.map +1 -0
- package/dist/glob.d.ts +50 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +144 -0
- package/dist/glob.js.map +1 -0
- package/dist/grep.d.ts +76 -0
- package/dist/grep.d.ts.map +1 -0
- package/dist/grep.js +475 -0
- package/dist/grep.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +45 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +39 -0
- package/dist/ledger.js.map +1 -0
- package/dist/payload.d.ts +108 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +263 -0
- package/dist/payload.js.map +1 -0
- package/dist/port.d.ts +112 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +2 -0
- package/dist/port.js.map +1 -0
- package/dist/product-complexity.d.ts +89 -0
- package/dist/product-complexity.d.ts.map +1 -0
- package/dist/product-complexity.js +1367 -0
- package/dist/product-complexity.js.map +1 -0
- package/dist/regex-safety.d.ts +75 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +902 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/relevance-prerequisites.d.ts +21 -0
- package/dist/relevance-prerequisites.d.ts.map +1 -0
- package/dist/relevance-prerequisites.js +47 -0
- package/dist/relevance-prerequisites.js.map +1 -0
- package/dist/relevance.d.ts +107 -0
- package/dist/relevance.d.ts.map +1 -0
- package/dist/relevance.js +199 -0
- package/dist/relevance.js.map +1 -0
- package/dist/retrieval.d.ts +46 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +182 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/scoring.d.ts +102 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +175 -0
- package/dist/scoring.js.map +1 -0
- package/dist/sort.d.ts +21 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +53 -0
- package/dist/sort.js.map +1 -0
- package/dist/symbol-match.d.ts +21 -0
- package/dist/symbol-match.d.ts.map +1 -0
- package/dist/symbol-match.js +72 -0
- package/dist/symbol-match.js.map +1 -0
- package/dist/tokenize.d.ts +41 -0
- package/dist/tokenize.d.ts.map +1 -0
- package/dist/tokenize.js +202 -0
- package/dist/tokenize.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +42 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -3
- package/src/benchmark-integrity.ts +25 -0
- package/src/bm25.ts +135 -0
- package/src/deadline.ts +59 -0
- package/src/dedupe.ts +99 -0
- package/src/diversify.ts +71 -0
- package/src/engine.ts +1957 -0
- package/src/enrichment.ts +115 -0
- package/src/filter.ts +267 -0
- package/src/findability.ts +580 -0
- package/src/format.ts +228 -0
- package/src/fusion.ts +121 -0
- package/src/glob.ts +151 -0
- package/src/grep.ts +618 -0
- package/src/index.ts +83 -0
- package/src/ledger.ts +72 -0
- package/src/payload.ts +382 -0
- package/src/port.ts +133 -0
- package/src/product-complexity.ts +2168 -0
- package/src/regex-safety.ts +1002 -0
- package/src/relevance-prerequisites.ts +83 -0
- package/src/relevance.ts +328 -0
- package/src/retrieval.ts +299 -0
- package/src/scoring.ts +321 -0
- package/src/sort.ts +86 -0
- package/src/symbol-match.ts +87 -0
- package/src/tokenize.ts +213 -0
- package/src/types.ts +206 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# NOTICE
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 Luke Bölling and contributors.
|
|
4
|
+
|
|
5
|
+
This attribution covers original Untangle work. Contributors retain copyright in
|
|
6
|
+
their contributions. Third-party packages, grammars and other materials retain
|
|
7
|
+
their own copyright holders, licenses and notices; this line does not replace them.
|
|
8
|
+
|
|
9
|
+
## License — binding decision
|
|
10
|
+
|
|
11
|
+
Untangle is licensed under the **GNU Affero General Public License, Version 3
|
|
12
|
+
(AGPL-3.0-only)**. See [`LICENSE`](./LICENSE).
|
|
13
|
+
|
|
14
|
+
This decision is **final, not provisional**, and was explicitly instructed by
|
|
15
|
+
the project owner. No contributor, agent or tool may change it without a new
|
|
16
|
+
explicit instruction.
|
|
17
|
+
|
|
18
|
+
The repository-wide license covers Untangle's first-party source code, scripts,
|
|
19
|
+
tests, fixtures, configuration, documentation, website and brand artwork, including
|
|
20
|
+
the logo and 3D yarn illustrations. Every workspace `package.json` declares
|
|
21
|
+
`"license": "AGPL-3.0-only"`. There is no separately licensed enterprise component
|
|
22
|
+
or separate permissive license for documentation or brand assets.
|
|
23
|
+
|
|
24
|
+
Third-party code, embedded grammars, dependency assets and their notices retain
|
|
25
|
+
their original licenses. Their inclusion does not relicense those works. The
|
|
26
|
+
repository license does not grant trademark rights or change the license of a
|
|
27
|
+
repository analyzed with Untangle. See the repository's `docs/LICENSING.md` for
|
|
28
|
+
scope and verification details.
|
|
29
|
+
|
|
30
|
+
This resolves the license question left open in `vision.md` §14 and completes
|
|
31
|
+
`product.md` §16, item 2.
|
|
32
|
+
|
|
33
|
+
## Rationale
|
|
34
|
+
|
|
35
|
+
- **Network copyleft fits the intended product model.** AGPL §13 sets source
|
|
36
|
+
availability requirements for modified versions used through a computer network.
|
|
37
|
+
The current local CLI/stdio-MCP entry and future shared-server modes share the
|
|
38
|
+
same license.
|
|
39
|
+
- **Local-first analysis has a clear boundary.** Running Untangle against a codebase
|
|
40
|
+
does not replace that codebase's own license with Untangle's license.
|
|
41
|
+
- **First-party components share one license.** Implemented components and future
|
|
42
|
+
official plugins use this repository's licensing policy. Planned integrations do
|
|
43
|
+
not establish implemented or published products.
|
|
44
|
+
|
|
45
|
+
## Deliberately accepted consequence
|
|
46
|
+
|
|
47
|
+
The project deliberately chooses AGPL rather than a permissive embedding license.
|
|
48
|
+
Redistribution, modifications and combined works must be assessed against the
|
|
49
|
+
actual terms in `LICENSE`, including applicable Corresponding Source requirements.
|
|
50
|
+
No permissive exception, commercial dual license or special contract-only package
|
|
51
|
+
license is granted or promised by this notice.
|
|
52
|
+
|
|
53
|
+
## Dependencies
|
|
54
|
+
|
|
55
|
+
Third-party runtime dependencies are checked against the allowlist in
|
|
56
|
+
`scripts/check-licenses.mjs`. Their licenses and copyright notices remain unchanged
|
|
57
|
+
and traceable through package `THIRD_PARTY_LICENSES.md` files, grammar provenance,
|
|
58
|
+
the lockfile and release SBOM. The allowlist includes MIT / Apache-2.0 / BSD / ISC
|
|
59
|
+
and other explicitly listed licenses; it is not a license grant for dependencies.
|
|
60
|
+
|
|
61
|
+
The allowlist in `scripts/check-licenses.mjs` applies to **dependencies**, not
|
|
62
|
+
workspace packages. Untangle's own packages carry AGPL-3.0-only and are excluded
|
|
63
|
+
from that check.
|
|
64
|
+
|
|
65
|
+
## Clean room
|
|
66
|
+
|
|
67
|
+
Untangle is an independent implementation with its own data models, APIs, tool
|
|
68
|
+
names, tests and sources. Feature ideas may be derived from publicly observable
|
|
69
|
+
behavior and generally known techniques. Third-party source code is not copied,
|
|
70
|
+
including from AGPL projects even where the license would permit it.
|
|
71
|
+
|
|
72
|
+
## Contribution terms
|
|
73
|
+
|
|
74
|
+
Pull requests are welcome under the repository's existing AGPL-3.0-only license.
|
|
75
|
+
No separate CLA, copyright assignment or DCO sign-off is required. Contributors
|
|
76
|
+
must have the right to submit their work under those terms. See CONTRIBUTING.md.
|
|
77
|
+
|
|
78
|
+
## Release decisions
|
|
79
|
+
|
|
80
|
+
- [x] Copyright attribution: Luke Bölling, with contributors retaining their rights.
|
|
81
|
+
- [x] Contributions use AGPL-3.0-only without an additional CLA or DCO gate.
|
|
82
|
+
|
|
83
|
+
## Open legal items before public beta
|
|
84
|
+
|
|
85
|
+
- [ ] Trademark search for the product name `Untangle`.
|
|
86
|
+
- [ ] Registration and legal review of the npm scope `@untangle-code`.
|
|
87
|
+
- [ ] Verify source availability and notices for each released network daemon/server
|
|
88
|
+
mode (WP-59). The current stdio MCP `untangle_about` already reports the license,
|
|
89
|
+
source repository and build commit when supplied; metadata alone does not prove
|
|
90
|
+
that a concrete deployed version's source is available.
|
package/README.md
CHANGED
|
@@ -1,3 +1,434 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @untangle-code/search
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Untangle's **retrieval core** (spec.md §13, architecture.md §18, vision.md §1b/§1d/§1e): hybrid
|
|
4
|
+
search, explainable score breakdowns, retrieval funnels, Search Journeys, complexity and
|
|
5
|
+
Findability filters, grep mode, and deterministic output formats. I/O goes exclusively through the
|
|
6
|
+
domain ports (`VectorStore`, `EmbeddingProvider`, `SearchJourneyStorePort`, `Clock`, `Random`,
|
|
7
|
+
`LoggerPort`); no Qdrant details, `Date.now()`, `Math.random()`, or `localeCompare`.
|
|
8
|
+
|
|
9
|
+
Normative sources: [`spec.md`](../../spec.md) §13 (takes precedence in a conflict),
|
|
10
|
+
[`architecture.md`](../../architecture.md) §18, [`vision.md`](../../vision.md) §1b/§1d/§1e.
|
|
11
|
+
|
|
12
|
+
<a id="wofür-das-paket-da-ist"></a>
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
|
|
16
|
+
> **Product complexity is the distance between intent and implementation.** Easy to find is
|
|
17
|
+
> evidence of a product that is well-defined, clear, and easy to understand. It is evidence, not
|
|
18
|
+
> proof: search effort is a sensor whose meaning depends on the retrieval context and an explicit
|
|
19
|
+
> target.
|
|
20
|
+
|
|
21
|
+
The user described the original goal as follows (translated):
|
|
22
|
+
|
|
23
|
+
> “When I search for ‘function X’, I should quickly reach the right place. If I do not, the code
|
|
24
|
+
> may be too complex or insufficiently documented.”
|
|
25
|
+
|
|
26
|
+
This currently produces three separate but connected capabilities:
|
|
27
|
+
|
|
28
|
+
- **Search**: dense + lexical + exact symbol name, fused with RRF.
|
|
29
|
+
- **Findability**: the same score breakdown used as a synthetic self-probe of a known symbol
|
|
30
|
+
(`findability.ts`: a symbol searches for itself; if it cannot find itself, the cause is named).
|
|
31
|
+
- **Dynamic Search Complexity**: several real search attempts within a task, up to an explicitly
|
|
32
|
+
confirmed target (`product-complexity.ts`), including retrieval funnels, reformulations,
|
|
33
|
+
candidate dispersion, and explicitly reported navigation. **Dynamic Product Complexity** emerges
|
|
34
|
+
only across several completed, comparable Journeys.
|
|
35
|
+
- **Feature Probes**: optionally tagged Journeys connecting a pseudonymized product capability to
|
|
36
|
+
confirmed code targets and reporting Search Complexity and Target-Code Complexity as two
|
|
37
|
+
separate evidence axes.
|
|
38
|
+
|
|
39
|
+
Static Code Complexity remains separate. It can be a filter (`--cognitive-min`), a visible score
|
|
40
|
+
component (`complexity_modifier`), or a query sort key. Dynamic Search Complexity instead assesses
|
|
41
|
+
the observed route from product intent to implementation location rather than a file's control
|
|
42
|
+
flow. Product Complexity aggregates these task-local vectors only within compatible cohorts.
|
|
43
|
+
|
|
44
|
+
<a id="modulübersicht"></a>
|
|
45
|
+
|
|
46
|
+
## Module overview
|
|
47
|
+
|
|
48
|
+
| Module | Contents | Normative source |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `types.ts` | `UntangleSearchRequestV1` (= `SearchRequestV1` + mode/grep/format/bias), `GrepRequestV1`, `GrepResultSetV1` | spec §13.1; request A4 |
|
|
51
|
+
| `port.ts` | `SearchPort` (`search`, `grep`), `ProfileTargetV1`, `SparseVectorizer`, `FindabilityLookup`, `GraphProximityLookup` | arch §18.1 |
|
|
52
|
+
| `engine.ts` | `HybridSearchEngine`: reference port implementation; returns retrieval diagnostics, an effective ranking-context fingerprint, and a profile-bound corpus revision when index context is complete | spec §13.2, §13.7 |
|
|
53
|
+
| `tokenize.ts` | identifier-aware tokenization (`getUserById` → `getuserbyid`, `get`, `user`, `by`, `id`) | spec §13.2 step 3 |
|
|
54
|
+
| `bm25.ts` | `Bm25Ranker`: a deterministic local Okapi BM25 over the candidate pool | spec §10.2, §13.2 |
|
|
55
|
+
| `symbol-match.ts` | four-tier name comparison and storage filter candidates | vision §1d |
|
|
56
|
+
| `fusion.ts` | weighted Reciprocal Rank Fusion, `k = 60`, normalized against the theoretical maximum | spec §13.4 |
|
|
57
|
+
| `scoring.ts` | `ScoreBreakdownV1`, including boost cap (`MAX_BOOST_SHARE`) | spec §13.3 |
|
|
58
|
+
| `relevance.ts` | profile-calibrated absolute/relative relevance decisions and absence statements | vision §1d; WP-231 |
|
|
59
|
+
| `filter.ts` | `SearchFiltersV1` → `PayloadFilter` **plus** a list of filters that cannot be pushed down | spec §13.2 step 7, §10.4 |
|
|
60
|
+
| `glob.ts` | deterministic glob matching for path inclusion/exclusion | request A4 |
|
|
61
|
+
| `dedupe.ts` | dedupe key `workspace + repo + ref + metrics_scope_id`, Related Fragments | arch §18.4; spec §13.5 |
|
|
62
|
+
| `diversify.ts` | saturating duplicate penalty per file/repository | arch §18.5 |
|
|
63
|
+
| `sort.ts` | sorting with code-point tie-breaking, rank assignment | request A4 |
|
|
64
|
+
| `grep.ts` | regex/literal search over indexed content, with context lines and budgets | request A4 |
|
|
65
|
+
| `format.ts` | Text / JSON / JSONL | request A4 |
|
|
66
|
+
| `findability.ts` | self-probes, dispersion, hops, named causes | vision §1d |
|
|
67
|
+
| `product-complexity.ts` | `SearchJourneyService`, task-based assessments, compatible workspace cohorts, and Feature→Code mappings separated by feature fingerprint and measurement context; incomplete evidence remains visible but never sufficient | spec §13.7; vision §1e; WP-248/WP-249/WP-253 |
|
|
68
|
+
| `payload.ts` | stored payload → candidate, with a named reason when unusable | spec §10.3 |
|
|
69
|
+
| `ledger.ts` | Skip Ledger collector | BUILD_PLAN E4 |
|
|
70
|
+
|
|
71
|
+
All public symbols are re-exported exclusively through `src/index.ts`.
|
|
72
|
+
|
|
73
|
+
## Dynamic Product Complexity
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
A Search Journey is an opt-in observation of real work, not a synthetic benchmark. It starts with a
|
|
77
|
+
task goal, records successful and failed searches, accepts explicitly reported file/symbol/graph
|
|
78
|
+
navigation, and can end as `completed` or `abandoned`. A completed journey requires at least one
|
|
79
|
+
search attempt and an explicit file, chunk, or symbol target. Until then, Untangle does not infer
|
|
80
|
+
that the agent found the right place.
|
|
81
|
+
|
|
82
|
+
An optional stable `featureId` turns that task into a repeatable Feature Probe. The service stores
|
|
83
|
+
it as a separate workspace-local HMAC fingerprint unless text retention is enabled. New search
|
|
84
|
+
observations preserve the hit's versioned `ComplexitySummaryV1` and payload-completeness evidence;
|
|
85
|
+
completion exposes this as observed target-code evidence only when the accepted candidate is
|
|
86
|
+
unambiguous. Missing, broad, or legacy evidence stays explicitly unavailable.
|
|
87
|
+
|
|
88
|
+
The assessment exposes components rather than one opaque number: searches and reformulations to
|
|
89
|
+
target, accepted rank and reciprocal rank when the target was returned, retrieval health, eligible
|
|
90
|
+
and exposed candidates, file dispersion, candidate churn, limit pressure, and observed navigation
|
|
91
|
+
hops. An externally found or profile-ambiguous target remains valid evidence but receives no
|
|
92
|
+
invented rank.
|
|
93
|
+
|
|
94
|
+
Reports keep search effort and target-code complexity side by side. Embeddings are retrieval
|
|
95
|
+
representations, not complexity metrics, and no combined score or causal relationship is emitted.
|
|
96
|
+
`featureMappings` include only explicitly tagged journeys and group by both feature fingerprint and
|
|
97
|
+
measurement context. Target metric variants keep their exact scope, metrics version, and index
|
|
98
|
+
generation instead of being averaged across incompatible evidence. The report-level
|
|
99
|
+
`sessionCounts.featureTaggedCompleted` counts exactly the completed, explicitly tagged journeys;
|
|
100
|
+
untagged completed journeys can contribute to workspace cohorts but not to feature mappings.
|
|
101
|
+
|
|
102
|
+
One completed journey has a caller-closed discovery record for that task and target. The wider
|
|
103
|
+
question — whether the product is valuable or the target is the right product decision — remains
|
|
104
|
+
feedback-open. Reports
|
|
105
|
+
therefore aggregate only journeys with the same effective ranker, ranking-context fingerprint,
|
|
106
|
+
result limit and candidate limit, with the embedding profile, generation set, and immutable corpus
|
|
107
|
+
revision bound together separately for every searched profile. At least three completed journeys
|
|
108
|
+
with a complete context are required before `sufficientEvidence` becomes true. Even then, the
|
|
109
|
+
cohort is a heuristic, timeless learning artifact, not a precision claim, temporal trend view, or
|
|
110
|
+
universal product score.
|
|
111
|
+
|
|
112
|
+
Text retention is deliberately narrow. By default, task and query text become workspace-local,
|
|
113
|
+
domain-separated HMAC-SHA-256 fingerprints; successful candidates retain locators,
|
|
114
|
+
profile/generation, rank, and score, never chunk or source content. `retainText: true` additionally
|
|
115
|
+
retains goal/query text, failed-search reasons, and free abandonment details. The HMAC key is
|
|
116
|
+
supplied by the storage adapter and kept outside the session records. This is pseudonymization, not
|
|
117
|
+
anonymity or encryption: an operator with the full local state and key can test guesses. Explicit
|
|
118
|
+
tool metadata and a controlled abandonment `reasonCode` may remain without `retainText` and should
|
|
119
|
+
not contain secrets.
|
|
120
|
+
|
|
121
|
+
Every retained goal/query, free reason, tool name, and individual locator component is capped at
|
|
122
|
+
4,096 UTF-16 code units; `featureId` remains capped at 256. These limits apply in the service and
|
|
123
|
+
persisted-domain validator as well as MCP, so a direct adapter call cannot bypass them. Successful
|
|
124
|
+
results are first reduced to the exact bounded profile/candidate projection that is both
|
|
125
|
+
fingerprinted and retained; every service-created Journey state is then domain-validated before
|
|
126
|
+
any `SearchJourneyStorePort.put`, including with a permissive alternate adapter. Confirmed and
|
|
127
|
+
navigated targets are normalized into a new bounded locator object before persistence.
|
|
128
|
+
|
|
129
|
+
Without an explicit `acceptedAttemptId`, every search through `complete` remains part of the effort
|
|
130
|
+
vector; rank and score margin are attributed to the last matching attempt with a visible caveat.
|
|
131
|
+
Supplying an earlier accepted attempt deliberately closes the search portion at that attempt. New
|
|
132
|
+
explicit completions retain that ID in `outcome.acceptedAttemptId`, so persisted and MCP-returned
|
|
133
|
+
assessments can be checked against the exact attempt even when a target repeats or numbered results
|
|
134
|
+
arrive out of order. Legacy outcomes without the additive field retain last-match reconstruction.
|
|
135
|
+
Partially unhealthy retrieval never qualifies as a complete measurement context and therefore
|
|
136
|
+
cannot make a cohort `sufficientEvidence: true`.
|
|
137
|
+
Callers can provide the locally known generation, corpus revision, and run outcome for each
|
|
138
|
+
repository/ref. Only a `completed` index context with its `corpusRevisionFingerprint` is
|
|
139
|
+
measurement-healthy. The immutable revision is derived from the complete scanned
|
|
140
|
+
File-ID/content-hash manifest after every completed index, including a zero-change incremental run.
|
|
141
|
+
Hits from a running, cancelled, failed, legacy, or otherwise unverifiable context remain usable,
|
|
142
|
+
while the result warning `INDEX_CONTEXT_INCOMPLETE` makes a linked Search Journey's retrieval
|
|
143
|
+
unhealthy and its measurement context incomplete.
|
|
144
|
+
A rejected candidate with reason `missing_index_entry` or the result warning `INCOMPLETE_PAYLOAD`
|
|
145
|
+
has the same effect: useful partial results remain available, but incomplete index evidence never
|
|
146
|
+
counts as healthy retrieval.
|
|
147
|
+
Completeness also requires retrieval diagnostics and one identical full attempt tuple across every
|
|
148
|
+
counted successful search: ranker, ranking-context fingerprint, effective result/candidate limits,
|
|
149
|
+
and each profile's bound embedding-profile, index-generation set, and corpus revision. New
|
|
150
|
+
observations mark this as measurement context v2. Legacy sessions remain readable and visible in
|
|
151
|
+
reports, but the report deliberately downgrades them to incomplete so they can never make a cohort
|
|
152
|
+
sufficient. Missing diagnostics or any tuple change has the same result.
|
|
153
|
+
|
|
154
|
+
A journey accepts at most 128 search attempts and 512 navigation events. Optional client steps are
|
|
155
|
+
1-based and make retries idempotent: the same step with the same observation is reused, while the
|
|
156
|
+
same step with different evidence conflicts. Numbered and unnumbered attempts cannot be mixed, and
|
|
157
|
+
a numbered journey cannot complete with gaps in its step sequence. Active and abandoned journeys
|
|
158
|
+
remain visible in report counts but never enter cohort metrics.
|
|
159
|
+
|
|
160
|
+
<a id="die-zentrale-bauentscheidung-abruf--rangfolge"></a>
|
|
161
|
+
|
|
162
|
+
## The central design decision: retrieval ≠ ranking
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
RETRIEVAL (from storage) RANKING (in this process)
|
|
166
|
+
──────────────────────── ──────────────────────────────
|
|
167
|
+
hybridQuery(dense) ─┐ dense rank (storage cosine)
|
|
168
|
+
hybridQuery(sparse) ─┼─► Pool ─► lexical rank (local BM25) ─► RRF ─► base_hybrid
|
|
169
|
+
scroll(symbol_name in …)─┘ symbol rank (name comparison)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The **search mode** selects retrieval and ranking: `semantic` queries dense only, `lexical` sparse
|
|
173
|
+
only, and `symbol` the exact-name filter only. Lexical and symbol search do not create a query
|
|
174
|
+
embedding. `hybrid` starts all three branches independently and fuses the branches retrieved
|
|
175
|
+
successfully. A branch failure leaves the other responses usable and is reported through
|
|
176
|
+
`SEARCH_BRANCH_UNAVAILABLE` (branch and original error code), a caveat, and the Skip Ledger. If every
|
|
177
|
+
requested branch of every profile fails, search throws a provider error; zero hits from a successful
|
|
178
|
+
search remain distinguishable. Cancellation signals are forwarded.
|
|
179
|
+
Hybrid branches have their own deadline, 10 seconds by default: a hanging branch is cancelled and
|
|
180
|
+
also reported through `SEARCH_BRANCH_TIMEOUT`, while successful branches respond. The deadline is
|
|
181
|
+
configurable through `branchTimeoutMs` in the engine port and belongs to the ranking context. Pure
|
|
182
|
+
semantic/lexical/symbol search uses the outer call deadline so a semantic model cold start gets its
|
|
183
|
+
regular time allowance.
|
|
184
|
+
|
|
185
|
+
A dense/sparse absence policy is explicitly not evaluated when calibrated branch evidence is
|
|
186
|
+
missing; an observed exact symbol name remains direct evidence. Partially failed hybrid retrieval
|
|
187
|
+
does not supply a complete Journey measurement context. The new retrieval strategy is versioned in
|
|
188
|
+
the ranking context, keeping older Journeys with the previous mode behavior separate.
|
|
189
|
+
|
|
190
|
+
<a id="die-score-zerlegung-ist-öffentliche-api"></a>
|
|
191
|
+
|
|
192
|
+
## The score breakdown is public API
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
final = base_hybrid
|
|
196
|
+
+ exact_symbol_boost + exact_path_boost + repo_priority_boost
|
|
197
|
+
+ graph_proximity_boost + recency_boost
|
|
198
|
+
+ complexity_modifier
|
|
199
|
+
- duplicate_penalty
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Each component carries `rawValue`, `weight`, `contribution = rawValue * weight`, and evidence
|
|
203
|
+
(`evidence`). The domain's `validateScoreBreakdown()` checks the identity and sum; this package's
|
|
204
|
+
tests call it on **every** generated hit.
|
|
205
|
+
|
|
206
|
+
Text output for a hit:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
1. 1.0743 billing@main src/payment/payment-service.ts:1-3 method charge
|
|
210
|
+
complexity: cognitive 22
|
|
211
|
+
why:
|
|
212
|
+
+ base_hybrid = 1.0000 (raw 1.0000 x weight 1.0000) [RRF over dense#1, lexical#1, symbol#1]
|
|
213
|
+
+ exact_symbol_boost = 0.1500 (raw 1.0000 x weight 0.1500) [symbol_name == "charge"]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
<a id="entscheidungen"></a>
|
|
217
|
+
|
|
218
|
+
## Decisions
|
|
219
|
+
|
|
220
|
+
Where spec.md/architecture.md leave questions open, the following reasoned decisions apply.
|
|
221
|
+
Downstream packages that need something else should extend **additively**.
|
|
222
|
+
|
|
223
|
+
1. **Lexical ranking happens here, not in storage.** The `VectorStore` sparse branch supplies
|
|
224
|
+
candidates (recall); `Bm25Ranker` computes their rank in this process. The score breakdown must be
|
|
225
|
+
reproducible, and Qdrant's IDF statistics are not reproducible outside Qdrant. The trade-off: IDF
|
|
226
|
+
applies to the candidate pool rather than the entire corpus (see “Known limits”).
|
|
227
|
+
|
|
228
|
+
2. **Sparse vectorization is injected (`SparseVectorizer`), not implemented here.** It must match
|
|
229
|
+
index-run tokenization bit for bit, so it belongs to the storage adapter. Without it, hybrid
|
|
230
|
+
loses sparse retrieval and reports `SPARSE_RECALL_UNAVAILABLE` and the failed branch evidence.
|
|
231
|
+
Pure lexical search then cannot execute any branch and reports an error. Semantic and symbol
|
|
232
|
+
search do not need a sparse vectorizer.
|
|
233
|
+
|
|
234
|
+
3. **`k = 60` in the RRF formula** comes from the original paper (Cormack/Clarke/Buettcher 2009) and
|
|
235
|
+
is the default in Qdrant's own RRF implementation. Branch weights are dense 1.0, lexical 1.0,
|
|
236
|
+
**symbol name 0.5**: half weight because an exact name match also receives `exact_symbol_boost`
|
|
237
|
+
and would otherwise count twice.
|
|
238
|
+
|
|
239
|
+
4. **Normalization uses the theoretical maximum, not the best hit in the list.** Otherwise the
|
|
240
|
+
best hit would always score 1.0, even when poor; Findability diagnosis needs that distinction.
|
|
241
|
+
|
|
242
|
+
5. **Boosts are capped at `MAX_BOOST_SHARE = 0.5` of the base amount** (spec §13.3: they “must not
|
|
243
|
+
completely override Dense/BM25”). The cap applies to the **weight**, not the contribution, keeping
|
|
244
|
+
`contribution = rawValue * weight` true and the raw signal visible. The cap is disclosed through
|
|
245
|
+
`clamped`/`clampReason`.
|
|
246
|
+
|
|
247
|
+
6. **The complexity modifier is OFF by default.** Complexity must not silently shift ranking.
|
|
248
|
+
Opting in (`complexityBias: "prefer-simple"`) adds a visible `complexity_modifier` to the
|
|
249
|
+
breakdown. For this, `SCORE_COMPONENT_KINDS` in `packages/domain` was extended **additively** with
|
|
250
|
+
`"complexity_modifier"`, as was `ScoreBreakdownV1.inputs` with optional fields `denseRank`,
|
|
251
|
+
`lexicalScore`, `lexicalRank`, `symbolRank`: without them, three-branch fusion could not be
|
|
252
|
+
reproduced. No existing signature changed.
|
|
253
|
+
|
|
254
|
+
7. **A component with raw value 0 is omitted rather than emitted as zero.** A breakdown should
|
|
255
|
+
explain what happened, not everything that could have happened, keeping output economical in
|
|
256
|
+
tokens (vision §1a, “For Agents”).
|
|
257
|
+
|
|
258
|
+
8. **`changedSince` is pushed into storage as a range condition on `changed_at_epoch_ms`.** Points
|
|
259
|
+
without this field fail the condition and are excluded. “Changed since X” cannot be answered
|
|
260
|
+
without a change date, and silently accepting such points would give a false answer.
|
|
261
|
+
|
|
262
|
+
9. **Scope is a REQUIRED PARAMETER, not a filter.** `buildPayloadFilter` requires a `SearchScopeV1`
|
|
263
|
+
(workspace, target repositories) and ALWAYS pushes `workspace_id` into the storage filter; the
|
|
264
|
+
target repositories are intersected with the request filter. If the request is entirely outside
|
|
265
|
+
the target, storage is not queried: an empty `in` list would not be a filter at all. The engine
|
|
266
|
+
also checks scope again on each hit. A store that ignores the filter then creates ledger entries
|
|
267
|
+
rather than out-of-scope hits. The same rule applies to grep mode.
|
|
268
|
+
|
|
269
|
+
10. **Path globs and Findability are post-filters.** The payload filter supports only exact values
|
|
270
|
+
and ranges. Any post-filtering is disclosed as a `caveat` on the result group; every rejected
|
|
271
|
+
candidate appears in the Skip Ledger. The same holds for grep mode, where rejections beyond
|
|
272
|
+
`GREP_GLOB_LEDGER_DETAIL_LIMIT` become an aggregate entry with the complete count.
|
|
273
|
+
|
|
274
|
+
11. **The dedupe key includes the ref.** The same code in two branches or worktrees represents two
|
|
275
|
+
answers (vision §1a). A request returning hits from several refs of one repository additionally
|
|
276
|
+
receives `MULTIPLE_REFS`.
|
|
277
|
+
|
|
278
|
+
12. **Diversification without content vectors.** `hybridQuery` returns payloads, not vectors;
|
|
279
|
+
content MMR would require a second storage round trip. Instead, a saturating penalty
|
|
280
|
+
`1 - 1/(1+n)` applies across file and repository, the dimension named first in arch §18.5.
|
|
281
|
+
|
|
282
|
+
13. **grep searches the index, not the disk.** Accordingly, `GrepResultSetV1` carries `scannedChunks`
|
|
283
|
+
and the Skip Ledger: “no hits” and “nothing searched” must not look alike.
|
|
284
|
+
|
|
285
|
+
14. **The port lives in this package, not `domain`.** `SearchPort` composes two existing domain
|
|
286
|
+
ports and the grep/format extensions; the domain does not need it yet. Its contract suite
|
|
287
|
+
therefore lives here under `src/testing/` and is not included in the build.
|
|
288
|
+
|
|
289
|
+
15. **A pattern without `/` is also checked against the basename**, so `*.ts` behaves as expected
|
|
290
|
+
without requiring `**/*.ts`.
|
|
291
|
+
|
|
292
|
+
16. **Absence is a profile- and corpus-bound conjunction, not a universal cosine value.** A policy
|
|
293
|
+
is found by `embeddingProfileId` and applies only when its `workspaceId` and physical
|
|
294
|
+
`collection` also match the search target. An empty policy map preserves the previous unfiltered
|
|
295
|
+
behavior without an additional warning. If a nonempty map lacks the profile key of an active
|
|
296
|
+
target, its hits stay unchanged, but `RELEVANCE_POLICY_NOT_EVALUATED` names
|
|
297
|
+
`policy_profile_mismatch`, the actual profile, and all configured keys. The supplied
|
|
298
|
+
`QWEN3_EMBEDDING_06B_RELEVANCE_POLICY` is calibrated exclusively against the existing Untangle
|
|
299
|
+
self-index. It accepts an exact symbol name directly; otherwise **all three** signals must
|
|
300
|
+
support the match: dense top value ≥ 0.45, gap between rank 1 and the actually observed rank 50
|
|
301
|
+
≥ 0.061, and IDF-weighted coverage of all normalized query terms within a single candidate
|
|
302
|
+
≥ 0.291. The calibrated evidence pool is the fixed union of the first 50 raw hits from each of
|
|
303
|
+
the dense and sparse retrieval branches. Residually invalid candidates are removed but never
|
|
304
|
+
replaced by deeper ranks. Symbol scrolling does not change IDF, and a higher `candidateLimit`
|
|
305
|
+
only enlarges the ranking pool. Thus the absence decision stays identical at 50, 80, 100, 150,
|
|
306
|
+
and 200 candidates. Both newly calibrated thresholds lie between measured counterexamples:
|
|
307
|
+
the gap between the reconciliation truth (0.06686) and clearly absent blockchain configuration
|
|
308
|
+
(0.05556); coverage between the configuration-array truth (0.29827) and clearly absent blockchain
|
|
309
|
+
transaction question (0.28482). The conjunction matters: the latter negative question itself
|
|
310
|
+
has a 0.06908 gap but fails coverage. In the measurement, this rule retained 22/22 main truths
|
|
311
|
+
plus seven separate German-prose truths and rejected 11/14 adversarial negative questions.
|
|
312
|
+
`NO_SUFFICIENT_MATCH` contains every measurement and threshold; each rejected hit appears in
|
|
313
|
+
the Skip Ledger. If fewer than 50 dense ranks were observed, the sparse retrieval branch is
|
|
314
|
+
missing, or the corpus binding mismatches, **no** absence decision is made:
|
|
315
|
+
`RELEVANCE_POLICY_NOT_EVALUATED` visibly names actual and expected values and leaves hits
|
|
316
|
+
unchanged. A search target whose declared `embeddingProfileId` differs from its embedder's
|
|
317
|
+
actual profile ID is rejected during wiring. Significant revisions of the same corpus require
|
|
318
|
+
new measurement and calibration.
|
|
319
|
+
|
|
320
|
+
17. **German natural-language questions retain only a densely corroborated local BM25 rank.**
|
|
321
|
+
Diagnostics from `benchmark/diagnose.mjs` show the measured conflict: for d3/d4/d6 the gold hit
|
|
322
|
+
ranks 3/10/3 in dense results, but is unranked or ranks 71/70 in local BM25. In hybrid mode, the
|
|
323
|
+
lexical branch therefore contributes exactly when its top candidate also appears in the dense
|
|
324
|
+
top 10. This retains the useful BM25 signal for d1 and the counterprobe asking where the
|
|
325
|
+
measured baseline is documented, while excluding the conflicting d3/d4/d6 branches. Sparse
|
|
326
|
+
**retrieval** always stays active; only an unconfirmed rank is excluded from fusion. An explicit
|
|
327
|
+
`mode: "lexical"` is unchanged. Narrow question detection also covers German phrasings meaning
|
|
328
|
+
“In which file”, “What is/happens”, “When”, “Who”, and “What for”; BM25 removes a small documented
|
|
329
|
+
set of German and English question function words.
|
|
330
|
+
|
|
331
|
+
18. **An exact-character symbol query expresses precision intent within the RRF base.** In this
|
|
332
|
+
case, the symbol branch contains only exact-character matches and receives enough computed
|
|
333
|
+
weight to stay ahead of non-exact candidates even between the two permitted boost-clamp
|
|
334
|
+
extremes. `exact_symbol_boost` and `MAX_BOOST_SHARE` are neither bypassed nor increased. This
|
|
335
|
+
intent applies in `hybrid` and `symbol`. Residual filters run first, so an excluded exact
|
|
336
|
+
candidate affects neither intent, weight, nor pool IDF, and each exclusion receives exactly one
|
|
337
|
+
ledger entry. Equivalent definitions are ordered by visible provenance (repository, ref, path,
|
|
338
|
+
line), and only finally by opaque point ID. Effective branch weights appear in score evidence.
|
|
339
|
+
|
|
340
|
+
<a id="testbarkeit-ohne-qdrant"></a>
|
|
341
|
+
|
|
342
|
+
## Testability without Qdrant
|
|
343
|
+
|
|
344
|
+
`src/testing/in-memory-index.ts` builds a complete `InMemoryVectorStore` (testkit) from simple fixture
|
|
345
|
+
rows, embeds it with the deterministic embedder, and returns a fully wired `HybridSearchEngine`.
|
|
346
|
+
`src/testing/search-port.contract.ts` contains the port contract suite (`runSearchPortContract`);
|
|
347
|
+
`search-port.contract.self-test.test.ts` runs it against the in-memory reference.
|
|
348
|
+
|
|
349
|
+
**No test in this package touches 127.0.0.1:6333.** An unrelated Qdrant instance runs there and must
|
|
350
|
+
remain untouched under spec.md §3.2.
|
|
351
|
+
|
|
352
|
+
<a id="bekannte-grenzen"></a>
|
|
353
|
+
|
|
354
|
+
## Known limits
|
|
355
|
+
|
|
356
|
+
Named rather than hidden, following the same rule as the Skip Ledger:
|
|
357
|
+
|
|
358
|
+
- **BM25 IDF applies to the candidate pool**, not the entire corpus. That suffices for ordering
|
|
359
|
+
*within* the pool, and fusion uses only ordering; absolute `lexicalScore` values are not comparable
|
|
360
|
+
between queries with different pools.
|
|
361
|
+
- **An absence statement requires a calibrated profile and corpus policy.** Without a matching
|
|
362
|
+
`relevancePolicies` entry, hits are deliberately not suppressed automatically. This prevents raw
|
|
363
|
+
values specific to Qwen and Untangle from being applied to another embedder or project. The three
|
|
364
|
+
compositional negative diagnoses still accepted are also named: vector search demonstrates
|
|
365
|
+
thematic similarity but does not reliably establish the absence of an asserted relationship
|
|
366
|
+
between two genuinely present sub-concepts.
|
|
367
|
+
- **Without `SparseVectorizer`, sparse retrieval is missing.** A hit that only BM25 would have found
|
|
368
|
+
never enters the hybrid pool. Warning: `SPARSE_RECALL_UNAVAILABLE`; pure lexical search reports an
|
|
369
|
+
error. In this state, a configured absence policy explicitly is **not** evaluated for non-symbol
|
|
370
|
+
questions (`RELEVANCE_POLICY_NOT_EVALUATED`), because its calibrated evidence pool is incomplete.
|
|
371
|
+
- **Context lines in grep mode stop at chunk boundaries.** Neighboring lines reside in another
|
|
372
|
+
point and would have to be loaded separately.
|
|
373
|
+
- **No regex timeout; patterns are checked in advance instead.** A time limit would not help:
|
|
374
|
+
evaluating a regex is ONE synchronous call; while it runs, no timer fires and no `AbortSignal` is
|
|
375
|
+
checked. Measurement: a chunk containing a 31-character line and pattern `(a+)+$` blocked `grep()`
|
|
376
|
+
for more than 88 seconds despite `maxScannedChunks` being 1 and cancellation after 200 ms.
|
|
377
|
+
`assertSafeGrepPattern` (in `regex-safety.ts`) therefore rejects dangerous patterns BEFORE the
|
|
378
|
+
first storage access, deterministically, without a clock or thread. It checks **ambiguity under
|
|
379
|
+
an unbounded repetition**, no longer star height: `R+` is dangerous if `R` can match empty
|
|
380
|
+
(`(a*)*`), two alternatives within `R` can match the same word (`(a|a)+`, `(\s|\t)+`), or `R` can
|
|
381
|
+
consume unbounded characters at its boundary that could also begin or end `R` (`(a+)+`,
|
|
382
|
+
`(\w+\s?)+`, `(\w+a)+`). The criterion is more precise in both directions than the old star-height
|
|
383
|
+
check: `(\s|\t)+$`, a search for trailing whitespace, previously ran indefinitely and is now
|
|
384
|
+
rejected, while `(a+b)+` and `(\w+\.)+` were previously rejected and now run (the final separator
|
|
385
|
+
makes decomposition unambiguous).
|
|
386
|
+
**Limit:** this remains a decidable approximation of an undecidable problem, deliberately biased
|
|
387
|
+
toward rejection. Backreferences under repetition, `\p{…}` treated as “any character”, and
|
|
388
|
+
alternatives of different fixed lengths sharing a prefix (`(a|ab)+`) are rejected even when
|
|
389
|
+
harmless in a particular case. Only the EXPONENTIAL family is covered; an accepted pattern may
|
|
390
|
+
still be polynomially expensive (`\w+\d+`). Full protection would need a dedicated matcher
|
|
391
|
+
without backtracking or evaluation in a killable process. Neither is built, deliberately.
|
|
392
|
+
Cancellation is now also checked per chunk (previously only per scroll page).
|
|
393
|
+
- **`includeContext` and `includeLinked` are accepted but NOT executed** (step 11 of the spec §13.2
|
|
394
|
+
pipeline is missing). Results now disclose this through `GRAPH_CONTEXT_UNAVAILABLE` and
|
|
395
|
+
`LINKED_ARTIFACTS_UNAVAILABLE`. `timings` remains undefined: measuring each phase would sacrifice
|
|
396
|
+
byte-for-byte result repeatability, which takes priority here.
|
|
397
|
+
- **Missing payload fields are substituted and NAMED.** `readCodeChunkPayload` returns
|
|
398
|
+
`substitutedFields`, hits carry them as `incompletePayloadFields`, and results warn once with
|
|
399
|
+
`INCOMPLETE_PAYLOAD`. A stored range ending before its start is rejected rather than adjusted.
|
|
400
|
+
- **`candidateLimit` is capped at `searchConfig.maxCandidateLimit`** (warning
|
|
401
|
+
`CANDIDATE_LIMIT_CLAMPED`); the returned `query` shows the value actually used.
|
|
402
|
+
- **No MMR based on content similarity** (see decision 12).
|
|
403
|
+
- **`graph_proximity_boost` and the Findability filter need injected lookups.** Without them, the
|
|
404
|
+
component is omitted or the filter unapplied. The latter produces
|
|
405
|
+
`FINDABILITY_FILTER_UNAVAILABLE`, never silent omission.
|
|
406
|
+
- **`measureFindability` cannot determine hops without a symbol graph.** In that case, `hops.hops`
|
|
407
|
+
is `null` **with** `hopsMissingReason`: the missing value is visible, not omitted.
|
|
408
|
+
- **`measureFindability` returns TWO possible outcomes** (`FindabilityOutcomeV1`, vision §1d): a
|
|
409
|
+
`FindabilityV1` measurement if search ran; otherwise a `FindabilityUnavailableV1` status record.
|
|
410
|
+
The record carries neither score nor diagnosis: a failed search is a finding about us, not the
|
|
411
|
+
code. Failure is also detected when the engine exposes it only as a warning rather than throwing,
|
|
412
|
+
per spec §25.2 (`detectRetrievalFailure`); `findabilitySkipEntry` puts the record in the Skip Ledger
|
|
413
|
+
(E4).
|
|
414
|
+
- **`diagnose` no longer accepts loose probes**, only a `FindabilityDiagnosisBasisV1` from
|
|
415
|
+
`openFindabilityDiagnosisBasis`, which does not exist for a failed probe.
|
|
416
|
+
- **Rank normalization across profile groups is not implemented** (arch §18.3 describes it as
|
|
417
|
+
opt-in). `rankNormalized` is always `false`; groups remain separate.
|
|
418
|
+
|
|
419
|
+
<a id="lizenz"></a>
|
|
420
|
+
|
|
421
|
+
## License
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
**AGPL-3.0-only.** This package includes the complete license in [`LICENSE`](./LICENSE), the
|
|
425
|
+
license-choice rationale in [`NOTICE`](./NOTICE), and licenses for all third-party production
|
|
426
|
+
dependencies in [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md). All three ship in the npm
|
|
427
|
+
tarball: AGPL-3.0-only §4 and §5 require recipients to receive the license; a README alone is not
|
|
428
|
+
a copy of the license.
|
|
429
|
+
|
|
430
|
+
Local use alone does not trigger an obligation. Copyleft obligations apply to redistribution and
|
|
431
|
+
providing a network service (AGPL §13).
|
|
432
|
+
|
|
433
|
+
Corresponding TypeScript source ships under `src/` in the same tarball; the complete repository
|
|
434
|
+
is available at <https://github.com/lboel/untangle>.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Third-party licenses — @untangle-code/search
|
|
2
|
+
|
|
3
|
+
Generated by `pnpm third-party-licenses` from this package's installed
|
|
4
|
+
production dependency tree: transitive `dependencies`, across
|
|
5
|
+
workspace edges, with no `devDependencies` included. The generator reads
|
|
6
|
+
each resolved package's `package.json` in `node_modules`; nothing is maintained by hand
|
|
7
|
+
or estimated.
|
|
8
|
+
|
|
9
|
+
Do not edit manually: `pnpm third-party-licenses --check` fails on drift,
|
|
10
|
+
and `pnpm check-publishable` rejects packages without a current file.
|
|
11
|
+
|
|
12
|
+
This file exists because `docs/BUILD_PLAN.md`, risk R6, makes
|
|
13
|
+
per-package `THIRD_PARTY_LICENSES.md` a release gate for npm publication.
|
|
14
|
+
|
|
15
|
+
Untangle itself is **AGPL-3.0-only** (see `LICENSE` and `NOTICE` in
|
|
16
|
+
this package). The packages below are third-party works; their own
|
|
17
|
+
licenses and copyright notices continue to apply unchanged. Workspace
|
|
18
|
+
packages (`@untangle-code/*`) are not listed here; they belong to the same
|
|
19
|
+
work and use the same AGPL-3.0-only license.
|
|
20
|
+
|
|
21
|
+
## Production dependencies (transitive, 1)
|
|
22
|
+
|
|
23
|
+
| Package | Version | License | Project |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| `smol-toml` | 1.8.0 | BSD-3-Clause | `github:squirrelchat/smol-toml` |
|
|
26
|
+
|
|
27
|
+
The allowlist (MIT / Apache-2.0 / BSD / ISC / …) is checked by
|
|
28
|
+
`scripts/check-licenses.mjs` against this exact tree on every run;
|
|
29
|
+
the GPL family is excluded (architecture.md §2.4, BUILD_PLAN R6).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harte Ehrlichkeitsgrenze fuer den Live-Benchmark.
|
|
3
|
+
*
|
|
4
|
+
* Der Search-Port degradiert ausgefallene Ziele absichtlich zu Warnungen,
|
|
5
|
+
* damit eine interaktive Suche andere Profile weiter anzeigen kann. Ein
|
|
6
|
+
* Benchmark darf diese Semantik nicht als Messwert missverstehen: ohne
|
|
7
|
+
* ausgefuehrten Abruf sind null Treffer weder Akzeptanz noch Abstention.
|
|
8
|
+
*/
|
|
9
|
+
import type { SearchResultSetV1 } from "@untangle-code/domain";
|
|
10
|
+
/** Wirft, bevor ein Benchmark eine nicht ausgefuehrte Suche serialisieren kann. */
|
|
11
|
+
export declare function assertBenchmarkRetrievalCompleted(questionId: string, result: SearchResultSetV1): void;
|
|
12
|
+
//# sourceMappingURL=benchmark-integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmark-integrity.d.ts","sourceRoot":"","sources":["../src/benchmark-integrity.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAI/D,mFAAmF;AACnF,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,iBAAiB,GACxB,IAAI,CAQN"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { detectRetrievalFailure } from "./findability.js";
|
|
2
|
+
/** Wirft, bevor ein Benchmark eine nicht ausgefuehrte Suche serialisieren kann. */
|
|
3
|
+
export function assertBenchmarkRetrievalCompleted(questionId, result) {
|
|
4
|
+
const failure = detectRetrievalFailure(result);
|
|
5
|
+
if (failure === undefined) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
throw new Error(`benchmark row "${questionId}" is invalid because retrieval did not complete (${failure.reason}): ${failure.detail}`);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=benchmark-integrity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmark-integrity.js","sourceRoot":"","sources":["../src/benchmark-integrity.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,mFAAmF;AACnF,MAAM,UAAU,iCAAiC,CAC/C,UAAkB,EAClB,MAAyB;IAEzB,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;IACT,CAAC;IACD,MAAM,IAAI,KAAK,CACb,kBAAkB,UAAU,oDAAoD,OAAO,CAAC,MAAM,MAAM,OAAO,CAAC,MAAM,EAAE,CACrH,CAAC;AACJ,CAAC"}
|
package/dist/bm25.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Saettigungskonstante der Termhaeufigkeit (Standardwert). */
|
|
2
|
+
export declare const BM25_K1 = 1.2;
|
|
3
|
+
/** Laengennormalisierung (Standardwert). */
|
|
4
|
+
export declare const BM25_B = 0.75;
|
|
5
|
+
/** Ein Dokument des lexikalischen Index. */
|
|
6
|
+
export interface LexicalDocument {
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/** Ein Rangeintrag des lexikalischen Zweigs. */
|
|
11
|
+
export interface LexicalRanking {
|
|
12
|
+
id: string;
|
|
13
|
+
score: number;
|
|
14
|
+
/** 1-basierter Rang. */
|
|
15
|
+
rank: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* BM25 ueber eine feste Dokumentmenge. Die Statistiken werden EINMAL im
|
|
19
|
+
* Konstruktor gebildet; danach ist `score()` eine reine Funktion.
|
|
20
|
+
*/
|
|
21
|
+
export declare class Bm25Ranker {
|
|
22
|
+
private readonly k1;
|
|
23
|
+
private readonly b;
|
|
24
|
+
private readonly documents;
|
|
25
|
+
private readonly documentFrequency;
|
|
26
|
+
private readonly averageLength;
|
|
27
|
+
constructor(documents: readonly LexicalDocument[], k1?: number, b?: number);
|
|
28
|
+
get documentCount(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Inverse Dokumenthaeufigkeit in der geglaetteten Form
|
|
31
|
+
* `ln(1 + (N - df + 0.5) / (df + 0.5))` — immer positiv, damit ein in jedem
|
|
32
|
+
* Dokument vorkommender Term den Score nicht negativ macht.
|
|
33
|
+
*/
|
|
34
|
+
private inverseDocumentFrequency;
|
|
35
|
+
private scoreDocument;
|
|
36
|
+
/**
|
|
37
|
+
* Rangliste zu einem Suchtext. Dokumente mit Score 0 (kein Term getroffen)
|
|
38
|
+
* fallen heraus — sie haben lexikalisch nichts beigetragen und duerfen die
|
|
39
|
+
* Fusion nicht mit einem Scheinrang beeinflussen.
|
|
40
|
+
*
|
|
41
|
+
* Gleichstand wird deterministisch nach Codepoints der ID aufgeloest, nie
|
|
42
|
+
* nach Einfuegereihenfolge und nie ueber `localeCompare`.
|
|
43
|
+
*/
|
|
44
|
+
rank(queryText: string): LexicalRanking[];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=bm25.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bm25.d.ts","sourceRoot":"","sources":["../src/bm25.ts"],"names":[],"mappings":"AA8BA,+DAA+D;AAC/D,eAAO,MAAM,OAAO,MAAM,CAAC;AAC3B,4CAA4C;AAC5C,eAAO,MAAM,MAAM,OAAO,CAAC;AAE3B,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAQD;;;GAGG;AACH,qBAAa,UAAU;IAOnB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,CAAC;IAPpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAGrC,SAAS,EAAE,SAAS,eAAe,EAAE,EACpB,EAAE,GAAE,MAAgB,EACpB,CAAC,GAAE,MAAe;IAerC,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,aAAa;IAerB;;;;;;;OAOG;IACI,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE;CAcjD"}
|