Skip to content
Back to skills

Scripts

BSecurity

Query a portable, embedding-free lexical knowledgebase bundled as a `.skill`. Use when the user references this KB or asks a question whose answer is in its corpus ({{SOURCE}}). Retrieval is BM25 over a precomputed inverted index — there is no embedding model, so YOU expand the query into search terms before searching. Bundle holds index.json + chunks.jsonl + search.js + search.py; pure stdlib, no install, no network.

  • 150 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 4, 2026
ai-agentspythonbashnode

Security analysis

B84/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 6 files and shows the line behind each finding

Scanned September 4, 2026

npx -y skills add oaustegard/claude-skills --skill scripts --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Scripts?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Scripts
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/oaustegard-scripts/badge)](https://www.skillsdirectory.com/skills/oaustegard-scripts)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: lexical-kb
description: Query a portable, embedding-free lexical knowledgebase bundled as a `.skill`. Use when the user references this KB or asks a question whose answer is in its corpus ({{SOURCE}}). Retrieval is BM25 over a precomputed inverted index — there is no embedding model, so YOU expand the query into search terms before searching. Bundle holds index.json + chunks.jsonl + search.js + search.py; pure stdlib, no install, no network.
---

# lexical-kb — query an embedding-free knowledgebase

This KB has **no semantic search and no embedding model**. Retrieval is pure
lexical BM25 over a precomputed inverted index. That design moves one job onto
you: bridging the gap between how the user phrases a question and how the corpus
phrases the answer. An embedding model would do this with a vector; here **you
are the semantic layer** — you expand the query into terms before searching.

Corpus: {{SOURCE}} ({{CHUNK_COUNT}} chunks).

## The retrieval protocol — follow every step

A raw user question fed straight to BM25 underperforms: it matches only the
exact words the user happened to use. The expansion step is what makes lexical
retrieval competitive with embeddings. Do not skip it.

1. **Read the question. Extract `core` terms** — the essential nouns, proper
   nouns, and identifiers the answer MUST contain. These carry full weight.

2. **Generate `expand` terms** — synonyms, morphological variants (plural/verb
   forms), acronym expansions and contractions, and adjacent concepts. These
   carry lower weight. This is the work the missing embedding model would have
   done. Be generous: 5–15 expansion terms is normal.

3. **Run the searcher.** It ships in this bundle in two equivalent runtimes —
   `node search.js` or `python3 search.py`, identical flags and identical
   results. Use whichever your environment has. Pass the user's original
   question via `--query` AND your term groups — expansion is **additive**, it
   never replaces the user's words:

   ```bash
   node search.js \
     --query "how does centered simhash differ from random projection?" \
     --core "simhash" --core "centered" \
     --expand "random projection" --expand "hyperplane" --expand "LSH" \
     --expand "binary quantization" --expand "hamming distance" \
     --k 5
   ```

   `--core`/`--expand` are repeatable; pass phrases, the searcher tokenizes
   them. The `--query` terms contribute at a low floor weight so a curated
   synonym can lift a result but can never drop a doc the literal question would
   have matched. Defaults: core 1.0, expand 0.4, query-floor 0.25, top-k 5.
   Keep expansion targeted — terms too generic ("system", "process") leak into
   unrelated chunks and blur the ranking. A precise word can mislead too if it
   is polysemous: prefer the disambiguating phrase as one `--core` term (e.g.
   `--core "centered simhash"`) over a bare ambiguous word (`--core "centered"`,
   which also matches "centered around …" in unrelated chunks). The passage
   extractor is lexical too, so it will highlight the wrong sense rather than
   correct it.

4. **Read the returned chunks. Answer from them, and cite chunk ids inline.**
   The chunks are the source of truth the user installed. When a chunk
   contradicts your prior knowledge, the chunk wins — say so. When the chunks
   do not contain the answer, say that plainly rather than filling the gap from
   memory.

## When you cannot expand — RM3 fallback

If the query is outside any domain you can expand confidently, pass it raw with
pseudo-relevance feedback. The searcher harvests expansion terms from the
corpus's own top hits — model-free, weaker than your expansion, and prone to
drift when the first pass is off-topic, so prefer real expansion when you can:

```bash
node search.js --query "the user's raw question" --rm3 --k 5
```

## Metadata filtering

Each chunk carries structured `meta` (e.g. `title`, `source_path`, `section`).
Filter on it with `--filter` (repeatable). Filtering narrows by attribute; it
does not rank — combine it with term search.

```bash
node search.js --core "factions" --filter "section=blog" --filter "date>=2025" --k 5
```

Operators: `=`, `!=`, `~` (substring), `>`, `>=`, `<`, `<=` (numeric when both
sides parse, else lexicographic — ISO dates sort correctly).

## Passage vs. full document

Ranking uses the whole chunk (best recall), but each hit's `text` is by default
the **query-densest passage** of that chunk (~1200 chars), not the entire chunk —
so your reasoning context is signal, not the surrounding noise of a long document.
Each matched sentence keeps its neighbours (`--context`, default 1 each side) so
it reads in context rather than as an orphaned fragment, and nearby matches merge
into contiguous passages; ' … ' marks elisions between them. When a hit is a
passage, the result carries `full_chars` (the chunk's full length). If you need
the complete document for a hit — broader context, a quote in a section the
passage elided — re-run with `--snippet 0`:

```bash
node search.js --query "…" --core "…" --snippet 0 --k 3
```

Tune with `--snippet 2000`/`600` (budget) and `--context 2`/`0` (neighbours).

## Output

The searcher prints JSON: `{"hits": [{id, score, text, meta, full_chars?}, ...]}`,
sorted by descending BM25 score. `text` is the focused passage (or the full chunk
if it was already short / `--snippet 0`); `full_chars` appears only when `text`
is a passage. Surface the top hits to the user with their ids, then answer using
them as authoritative context.

## Mechanics

- Pure stdlib — Node or Python. No `npm install` / `pip install`, no model
  download, no network.
- The bundle is self-contained: `search.js` + `search.py` (pick one),
  `index.json`, `chunks.jsonl`. Run the searcher from inside the bundle
  directory (it defaults `--index` to its own location) or pass
  `--index /path/to/bundle`.

Files in this skill

  • build_lexkb.js8.2 KB
  • bundle_SKILL.md5.8 KB
  • search.js13.5 KB
  • search.py16.1 KB
  • zipstore.js3.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.