PHPackages                             cybersafe-lab/saqr - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. cybersafe-lab/saqr

ActiveLibrary

cybersafe-lab/saqr
==================

Grounded retrieval-augmented Q&amp;A library for Saudi Arabia's cybersecurity and data-protection frameworks (PDPL, NCA-ECC, NCA-CCC, SAMA, ISO 27001).

v0.2.2(yesterday)10Apache-2.0PHPPHP ^8.2CI passing

Since May 22Pushed yesterdayCompare

[ Source](https://github.com/cybersafe-lab/saqr)[ Packagist](https://packagist.org/packages/cybersafe-lab/saqr)[ RSS](/packages/cybersafe-lab-saqr/feed)WikiDiscussions main Synced today

READMEChangelog (3)Dependencies (3)Versions (5)Used By (0)

Saqr
====

[](#saqr)

[![CI](https://github.com/cybersafe-lab/saqr/actions/workflows/ci.yml/badge.svg)](https://github.com/cybersafe-lab/saqr/actions/workflows/ci.yml)[![PHP](https://camo.githubusercontent.com/184ea983600c41146ee84f2391d4841a638528091c0262f5e51738e676897fad/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e322d373737626234)](https://camo.githubusercontent.com/184ea983600c41146ee84f2391d4841a638528091c0262f5e51738e676897fad/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e322d373737626234)[![Node](https://camo.githubusercontent.com/b7631307482dff71a4c76d96c639d73dc68b158ad9922a46835f7f4c9a38e853/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6e6f64652d25453225383925413532302d333339393333)](https://camo.githubusercontent.com/b7631307482dff71a4c76d96c639d73dc68b158ad9922a46835f7f4c9a38e853/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6e6f64652d25453225383925413532302d333339393333)[![License](https://camo.githubusercontent.com/35b94db23d3ed026343335f74d52ce31e74b77ad7dab4e4b89f49f2026e0937f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c7565)](LICENSE)

**Grounded retrieval-augmented Q&amp;A library for Saudi Arabia's cybersecurity and data-protection frameworks.**

Saqr (Arabic: *صقر*, "falcon") answers questions about Saudi cybersecurity and data-protection regimes: **NCA** (ECC, CCC, CSCC, DCC, TCC, OTCC, OSMACC, SCyWF), **SAMA** (CSF, ITGF, BCM), **CST** (CRF), **Aramco** (SACS-210), **SDAIA** (PDPL), and **ISO 27001**. It uses a curated practitioner corpus, deterministic keyword retrieval, and (optionally) an LLM for fluent answer synthesis.

The corpus is plain JSON. The retriever is plain PHP. The optional generator is a thin Anthropic Messages API caller. **No vendor lock-in, no embeddings service, no database.**

[![Saqr chatbot answering with cited sources](docs/img/chatbot-sources.gif)](docs/img/chatbot-sources.gif)

---

How it works
------------

[](#how-it-works)

 ```
flowchart LR
    subgraph clients [Surfaces]
        CLI[saqr-cli]
        MCP[MCP server - Claude Desktop and Cursor]
        WP[Website chatbot - same retrieval design]
    end
    CLI --> P
    MCP --> P
    subgraph library [Saqr library - PHP 8.2]
        P[Pipeline] --> RL[RateLimiter]
        P --> R[Retriever - deterministic keyword scoring plus Arabic normalization]
        R --> C[(Curated JSON corpus - 32 entries, frozen IDs, official-source refs)]
        P --> G[Generator - optional Anthropic API, strict sanitizer]
    end
    P --> OUT[answer plus cited sources]
```

      Loading Retrieval is deterministic: keywords are scored by byte length, which weights Arabic terms above their Latin equivalents, and the same input always produces the same ranking. Every answer carries the ids of the entries it was built from and their official-source refs (empty only for the two self-description entries, which cite nothing), so any factual claim can be traced to a regulator publication. The LLM layer is optional. With no API key configured, Saqr returns the top-ranked curated entry verbatim rather than degrading to a guess. (The website chatbot in the diagram is a separate WordPress deployment that reimplements this retrieval design; it does not consume the library.)

1. **Corpus**: a JSON file of 31 practitioner-written entries. Each has an `id`(frozen in `corpus/ids.lock`), a `title` (the label clients print above the answer), a `category`, a `framework` label, a list of `keywords` (English and transliterated terms), an `answer`, and `refs`: the official document title and URL behind it.
2. **Retriever**: lowercase the question, expand any Arabic phrases to their English keyword equivalents, then score each corpus entry by summing the byte lengths of its keywords that appear in the question. Return the top 3 by score. No embeddings; deterministic and explainable.
3. **Generator** (optional): if `SAQR_ANTHROPIC_KEY` (or `ANTHROPIC_API_KEY`) is set, the top entries are passed to Claude Haiku as `[SOURCE 1]…[SOURCE N]`context with a strict grounding system prompt. The model returns sanitized HTML using only ``, ``, ``; anything else is unwrapped and re-escaped. If no key is set, the retriever returns the top entry verbatim.
4. **Rate limiter**: a `RateLimiterInterface` is composed into `Pipeline`. The bundled `InMemoryRateLimiter` is fine for CLI and single-process use; plug in Redis or any other backend for production.

---

Engineering highlights
----------------------

[](#engineering-highlights)

- **Test rigor:** 122 Pest tests, 387 assertions, across Unit, Characterization (pins the byte-level Arabic ranking semantics that a reimplementation in another language would silently drift on), Snapshot (ranking regression), Integration (CLI JSON contract), Smoke, and Eval suites, plus 29 vitest tests for the MCP server. CI runs every suite: the unit through eval suites on PHP 8.2 and 8.3, the Integration suite on 8.3, the MCP suite on Node 20 and 22. One test, a WordPress handler smoke, skips unless `SAQR_WP_PATH` points at a local install.
- **Grounded by construction:** answers are assembled from a curated, frozen-ID corpus; every answer-bearing response carries the entries it was retrieved from and their official regulator references.
- **Bilingual evaluation:** 108 retrieval eval cases (66 English, 42 Arabic) with per-language hit@1 / hit@3 / MRR and a committed overall baseline that CI refuses to regress.
- **Corpus as code:** `php bin/corpus-lint` runs in CI and enforces the entry schema, frozen IDs, a title and framework label on every entry, at least one official-source ref on every non-META entry, the `{title, url}` shape of each ref, brand-voice style rules (no em-dashes or en-dashes, no puff words), and a prompt-injection blocklist. Both run on every string an entry sends to a client: the answer, the entry title, and each ref title. Ref URLs carry the injection blocklist as well, but not the style rules, since a URL is the publisher's string and not ours to reword. A separate `php bin/refs-check`confirms every ref URL still resolves, including a per-host soft-404 guard for `nca.gov.sa` and `cst.gov.sa`, which answer HTTP 200 for paths that do not exist. It hits the network, so it runs at curation time rather than in CI.
- **No infra:** plain JSON and plain PHP; no embeddings service, no vector DB, no framework.

---

Use Saqr from Claude Desktop / Cursor (MCP)
-------------------------------------------

[](#use-saqr-from-claude-desktop--cursor-mcp)

Saqr ships an MCP server so any MCP-compatible AI client can query the corpus as tools.

### Option A: via npm (requires PHP 8.2+ on the host)

[](#option-a-via-npm-requires-php-82-on-the-host)

Add to your `claude_desktop_config.json`:

```
{
  "mcpServers": {
    "saqr": {
      "command": "npx",
      "args": ["-y", "@cybersafe-lab/saqr-mcp"],
      "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
    }
  }
}
```

### Option B: via Docker (no PHP required)

[](#option-b-via-docker-no-php-required)

```
{
  "mcpServers": {
    "saqr": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "ANTHROPIC_API_KEY",
        "ghcr.io/cybersafe-lab/saqr-mcp:latest"
      ]
    }
  }
}
```

Verify the Docker image's signature (optional):

```
cosign verify ghcr.io/cybersafe-lab/saqr-mcp:latest \
  --certificate-identity-regexp 'https://github.com/cybersafe-lab/saqr/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
```

Tools: `saqr_search`, `saqr_compare_frameworks`, `saqr_explain_control`, `saqr_show_corpus`.

All three answer-bearing tools return official sources next to the answer, as a `refs` array of `{title, url}` objects:

ToolWhat `refs` holds`saqr_search`the refs of each returned entry, per result`saqr_explain_control`the refs of the entry that answered the lookup`saqr_compare_frameworks`the refs behind the answer returned: the union across every entry a synthesized comparison drew on, deduplicated by URL, or just the refs of the single entry served verbatim when no API key is configured`refs` is always present. It is empty for exactly two entries: the META self-descriptions (`about-assistant`, `frameworks-index`), which describe the assistant rather than a framework and so have nothing to cite. Those two are also the entries that answer what new users open with ("help", "what frameworks do you cover"), so an empty `refs` is a normal first response, not an edge case. Read `refs[0]` defensively.

[![Saqr MCP server answering a PDPL question in MCP Inspector, with official-source refs](docs/img/mcp-inspector.png)](docs/img/mcp-inspector.png)

See `docs/mcp.md` for the full integration guide.

---

Quickstart
----------

[](#quickstart)

The package is not on Packagist yet, so install it straight from GitHub:

```
composer config repositories.saqr vcs https://github.com/cybersafe-lab/saqr
composer require cybersafe-lab/saqr:dev-main
```

```
