diff --git a/README.md b/README.md index f1addeb..e826ed3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Scale regression: Qarinah also passed **380 / 380 file-specific exact and typo-t

Website ·  + Features ·  + Install ·  Documentation ·  White paper ·  Research benchmark ·  @@ -139,7 +141,7 @@ npx qarinah query "release provenance" \ --format markdown ``` -Start with the [five-minute guide](docs/GETTING-STARTED.md), then use the [cross-agent handoff guide](docs/CROSS-AGENT-HANDOFFS.md), [dashboard guide](docs/DASHBOARD.md), [team-memory guide](docs/TEAM-MEMORY.md), [CLI reference](docs/CLI-REFERENCE.md), [JavaScript API reference](docs/API-REFERENCE.md), [MCP guide](docs/MCP-GUIDE.md), [task recipes](docs/RECIPES.md), or [troubleshooting guide](docs/TROUBLESHOOTING.md). +Start with the [feature map](docs/FEATURES.md) and [five-minute installation guide](docs/GETTING-STARTED.md), then use the [cross-agent handoff guide](docs/CROSS-AGENT-HANDOFFS.md), [dashboard guide](docs/DASHBOARD.md), [team-memory guide](docs/TEAM-MEMORY.md), [CLI reference](docs/CLI-REFERENCE.md), [JavaScript API reference](docs/API-REFERENCE.md), [MCP guide](docs/MCP-GUIDE.md), [task recipes](docs/RECIPES.md), or [troubleshooting guide](docs/TROUBLESHOOTING.md). Your project already contains the decisions and evidence behind its changes. Qarinah lets the next agent query that record and receive a bounded, cited pack selected for the current task. The same local memory can support Codex, Claude Code, CLI workflows, and compatible MCP clients instead of locking project context to one editor. diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..a1c55e1 --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,45 @@ +# Qarinah features + +Qarinah is evidence-linked project memory for coding agents. It keeps an inspectable record beside a software project and compiles bounded, cited context packs for Codex, Claude Code, Cursor, CLI workflows, and compatible MCP clients. + +## Project-owned memory + +- A canonical, append-only JSONL event record stored with the project. +- Stable event identities, content hashes, and chain verification for detectable edits, deletions, truncation, duplicates, and broken continuity. +- Explicit workspace initialization, machine-local trust, and metadata-only capture by default. +- Configurable content capture, redaction, retention, size, repository, time, and disclosure boundaries. + +## Cited context compilation + +- Bounded lexical, typo-tolerant, graph, and optional semantic retrieval. +- Complete selected records with event IDs, hashes, retrieval manifests, and evidence-coverage diagnostics. +- Token and character budgets with reserved output space and explicit abstention controls. +- Temporal validity, freshness, supersession, conflict, repository, and authority filtering before context is admitted. +- Deterministic Markdown and JSON context packs that can be inspected before they reach a model. + +## Project structure and derived views + +- Bounded project scanning with typed file, directory, import, link, and unresolved-reference relationships. +- A rebuildable typed graph and retrieval index derived from the authoritative event record. +- An optional SQLite WAL and FTS5 read model that remains disposable derived state. +- A local read-only dashboard for current and superseded decisions, conflicts, citations, permitted activity, affected files, and caller-supplied context measurements. + +## Coding-agent integrations + +- Reviewed Codex and Claude Code integrations that use the same explicitly trusted project record. +- Cursor setup, terminal workflows, and project-level instructions. +- A native MCP stdio server with diagnostic-only defaults and explicitly authorized, zero-write context retrieval. +- Cross-agent handoff capsules for continuing a task without replaying the complete retained history. + +## Team memory and portability + +- Multi-repository context packs that preserve separate repository and authority boundaries. +- Freshness diagnostics, encrypted sync bundles, signed checkpoints, and explicit membership records. +- Deterministic Markdown, JSON, typed graph, and Google Open Knowledge Format exports. +- Schemas and adapters for Cockroach Crawler evidence, Cockroach Browser memory records, ProductLoop runtime events, and optional Maqam authority scopes. + +## Verify the boundary + +Qarinah is not an autonomous agent runtime, a hosted personalization service, or a guarantee that a model answer is correct. It supplies inspectable project memory to a caller that remains responsible for model choice, current source files, tools, and execution authority. + +Continue with [installation and setup](GETTING-STARTED.md), [host integrations](HOST-INTEGRATIONS.md), the [CLI reference](CLI-REFERENCE.md), or the [JavaScript and TypeScript API](API-REFERENCE.md). diff --git a/package.json b/package.json index 90c7f4a..2d28f2f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "schemas/", "docs/ARCHITECTURE.md", "docs/GETTING-STARTED.md", + "docs/FEATURES.md", "docs/CROSS-AGENT-HANDOFFS.md", "docs/CROSS-SESSION-CONTINUATION-BENCHMARK.md", "docs/CROSS-AGENT-VIDEO-PROTOCOL.md", diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs index b2ecd6a..20ce31f 100644 --- a/scripts/build-site.mjs +++ b/scripts/build-site.mjs @@ -23,6 +23,19 @@ const packageJson = JSON.parse(await readFile(path.join(root, "package.json"), " const productVersion = packageJson.version; const productPositioning = "Evidence-linked project memory for coding agents."; const productExplanation = "Qarinah keeps one compact, cited project memory beside your code, so Codex, Claude Code, Cursor, and compatible tools can continue from verified context instead of starting from zero."; +const qarinahFeatures = [ + "Verified handoffs between coding agents", + "Local append-only project memory", + "Evidence-linked cited context packs", + "Typed project and provenance graph", + "Budgeted hybrid retrieval", + "Codex and Claude Code integrations", + "Consent-gated MCP context retrieval", + "Multi-repository memory with separate authority", + "Freshness checks and a visual memory dashboard", + "Encrypted team bundles and signed checkpoints", + "Deterministic Markdown, JSON, graph, and OKF exports" +]; const answerEngineQuestions = [ { name: "What is Qarinah?", @@ -313,6 +326,14 @@ const docPages = [ section: "Start", aliases: ["install qarinah", "project setup", "first query", "coding agent memory quickstart"] }, + { + route: "docs/features", + source: "docs/FEATURES.md", + title: "Features", + description: "Explore Qarinah project memory, cited context compilation, agent integrations, local dashboards, team memory, exports, and operating boundaries.", + section: "Start", + aliases: ["qarinah features", "project memory features", "coding agent memory capabilities", "context compiler capabilities"] + }, { route: "docs/cli", source: "docs/CLI-REFERENCE.md", @@ -615,7 +636,9 @@ function rewritePublicationLink(markdown, source) { function nav(active = "") { const items = [ - ["Product", "/", "home"], + ["Overview", "/", "home"], + ["Features", "/docs/features/", "features"], + ["Install", "/docs/getting-started/", "install"], ["Docs", "/docs/", "docs"], ["Answers", "/docs/faq/", "answers"], ["Compare", "/alternatives/", "alternatives"], @@ -652,7 +675,8 @@ function footer() {

Build - Getting started + Features + Install and get started CLI reference JavaScript API Integrations @@ -731,15 +755,7 @@ function structuredData({ title, description, canonical, kind = "doc" }) { name: "Qarinah", description, inLanguage: "en", - publisher: { "@id": person["@id"] }, - potentialAction: { - "@type": "SearchAction", - target: { - "@type": "EntryPoint", - urlTemplate: `${siteOrigin}/search/?q={search_term_string}` - }, - "query-input": "required name=search_term_string" - } + publisher: { "@id": person["@id"] } }, { "@type": "SoftwareApplication", @@ -758,19 +774,7 @@ function structuredData({ title, description, canonical, kind = "doc" }) { installUrl: `${siteOrigin}/docs/getting-started/`, codeRepository: github, releaseNotes: `${github}/releases/tag/v${productVersion}`, - featureList: [ - "Verified handoffs between coding agents", - "Local append-only project memory", - "Evidence-linked cited context packs", - "Typed project and provenance graph", - "Budgeted hybrid retrieval", - "Codex and Claude Code integrations", - "Consent-gated MCP context retrieval", - "Multi-repository memory with separate authority", - "Freshness checks and a visual memory dashboard", - "Encrypted team bundles and signed checkpoints", - "Deterministic Markdown, JSON, graph, and OKF exports" - ], + featureList: qarinahFeatures, offers: { "@type": "Offer", price: "0", @@ -919,6 +923,32 @@ function structuredData({ title, description, canonical, kind = "doc" }) { })) } ); + } else if (kind === "features") { + graph.push( + { + "@type": "CollectionPage", + "@id": `${url}#features`, + name: title, + description, + url, + inLanguage: "en", + dateModified: releaseDate, + author: { "@id": person["@id"] }, + about: { "@id": `${siteOrigin}/#software` } + }, + { + "@type": "ItemList", + "@id": `${url}#capability-list`, + name: "Qarinah product capabilities", + itemListOrder: "https://schema.org/ItemListUnordered", + numberOfItems: qarinahFeatures.length, + itemListElement: qarinahFeatures.map((name, index) => ({ + "@type": "ListItem", + position: index + 1, + name + })) + } + ); } else if (kind === "benchmark") { graph.push( { @@ -1023,6 +1053,8 @@ function layout({ title, description, body, active = "", canonical = "/", kind = + + @@ -1794,7 +1826,11 @@ async function markdownPage(page) { ? "benchmarks" : page.route === "docs/faq" ? "answers" - : "docs"; + : page.route === "docs/features" + ? "features" + : page.route === "docs/getting-started" + ? "install" + : "docs"; const publicationLink = page.route === "paper" ? `Published v1.4 DOI: 10.5281/zenodo.21850747 · Paper series DOI · Published v1.3` : ""; @@ -1810,6 +1846,8 @@ async function markdownPage(page) { ? "benchmark" : page.route === "docs/faq" ? "faq" + : page.route === "docs/features" + ? "features" : page.route === "docs/getting-started" || page.route === "docs/cross-agent-handoffs" ? "howto" : page.route === "docs/api" || page.route === "docs/cli" || page.route === "docs/mcp" @@ -1864,6 +1902,14 @@ const sitemapRoutes = [ "/search/", ...docPages.map((page) => `/${page.route}/`) ]; +const staticRedirects = (await readFile(path.join(root, "website", "static", "_redirects"), "utf8")).trim(); +const canonicalDirectoryRedirects = sitemapRoutes + .filter((route) => route !== "/") + .map((route) => `${route.slice(0, -1)} ${route} 308`); +await writeFile( + path.join(output, "_redirects"), + `${canonicalDirectoryRedirects.join("\n")}\n${staticRedirects}\n` +); await writeFile( path.join(output, "sitemap.xml"), `\n\n${sitemapRoutes.map((route) => ` ${siteOrigin}${route}${releaseDate}`).join("\n")}\n\n` diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index 88bb79b..b103523 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -11,6 +11,7 @@ const required = [ "docs/index.html", "docs/cross-agent-handoffs/index.html", "docs/getting-started/index.html", + "docs/features/index.html", "docs/cli/index.html", "docs/api/index.html", "docs/integrations/index.html", @@ -67,6 +68,11 @@ const availableRoutes = new Set(htmlFiles.map((file) => { const relative = path.relative(output, file).replaceAll("\\", "/"); return relative === "index.html" ? "/" : `/${relative.replace(/index\.html$/, "")}`; })); +const slashlessDirectoryAliases = new Map( + [...availableRoutes] + .filter((route) => route !== "/") + .map((route) => [route.slice(0, -1), route]) +); const errors = []; const titles = new Map(); @@ -118,6 +124,12 @@ for (const file of htmlFiles) { canonicals.set(canonical, fileLabel); } + for (const language of ["en", "x-default"]) { + const alternate = html.match(new RegExp(``))?.[1]; + const expected = `https://qarinah.io${route}`; + if (alternate !== expected) errors.push(`${fileLabel} ${language} alternate ${alternate ?? "is missing"}; expected ${expected}`); + } + if ((html.match(/)/g) || []).length !== 1) { errors.push(`${fileLabel} must contain exactly one h1`); } @@ -155,6 +167,13 @@ for (const file of htmlFiles) { errors.push(`${fileLabel} links to missing route ${linkedRoute}`); } } + + for (const match of html.matchAll(/href="(\/[^"#?]*)(?:[?#][^"]*)?"/g)) { + const linkedPath = match[1]; + if (slashlessDirectoryAliases.has(linkedPath)) { + errors.push(`${fileLabel} links to slashless directory alias ${linkedPath}; use ${slashlessDirectoryAliases.get(linkedPath)}`); + } + } } for (const [sourceRoute, html] of htmlByRoute) { @@ -176,10 +195,42 @@ for (const route of availableRoutes) { } for (const route of sitemapRoutes) { if (!availableRoutes.has(route)) errors.push(`sitemap.xml contains unknown route ${route}`); + if (route !== "/" && !route.endsWith("/")) errors.push(`sitemap.xml contains non-canonical slashless route ${route}`); +} + +const redirects = await readFile(path.join(output, "_redirects"), "utf8"); +const redirectRules = redirects + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .map((line) => line.split(/\s+/)); +const redirectSources = new Set(); +for (const [source, destination, status] of redirectRules) { + if (redirectSources.has(source)) errors.push(`_redirects contains duplicate source ${source}`); + redirectSources.add(source); + if (slashlessDirectoryAliases.has(source)) { + const expected = slashlessDirectoryAliases.get(source); + if (destination !== expected || !["301", "308"].includes(status)) { + errors.push(`_redirects must permanently canonicalize ${source} to ${expected}`); + } + } +} +for (const [source, destination] of slashlessDirectoryAliases) { + if (!redirectRules.some(([candidateSource, candidateDestination, status]) => + candidateSource === source && candidateDestination === destination && ["301", "308"].includes(status) + )) { + errors.push(`_redirects is missing permanent canonicalization ${source} -> ${destination}`); + } } const searchIndex = JSON.parse(await readFile(path.join(output, "search-index.json"), "utf8")); const indexedRoutes = new Set(searchIndex.map((entry) => entry.route)); +for (const entry of searchIndex) { + if (entry.route !== "/" && !entry.route.endsWith("/")) { + errors.push(`search-index.json contains non-canonical slashless route ${entry.route}`); + } + if (!availableRoutes.has(entry.route)) errors.push(`search-index.json contains unknown route ${entry.route}`); +} for (const route of availableRoutes) { if (route.startsWith("/docs/") && route !== "/docs/" && !indexedRoutes.has(route)) { errors.push(`search-index.json is missing ${route}`); @@ -209,6 +260,7 @@ for (const crawler of ["Googlebot", "Bingbot", "OAI-SearchBot", "Claude-SearchBo const llms = await readFile(path.join(output, "llms.txt"), "utf8"); for (const canonicalResource of [ "https://qarinah.io/docs/", + "https://qarinah.io/docs/features/", "https://qarinah.io/docs/faq/", "https://qarinah.io/alternatives/", "https://qarinah.io/articles/open-source-governed-agent-toolkit/", @@ -224,6 +276,10 @@ const alternatives = await readFile(path.join(output, "alternatives", "index.htm const toolkit = await readFile(path.join(output, "articles", "open-source-governed-agent-toolkit", "index.html"), "utf8"); const paper = await readFile(path.join(output, "paper", "index.html"), "utf8"); const faq = await readFile(path.join(output, "docs", "faq", "index.html"), "utf8"); +const features = await readFile(path.join(output, "docs", "features", "index.html"), "utf8"); +if (home.includes('"@type":"SearchAction"') || home.includes("search_term_string")) { + errors.push("Homepage must not emit the retired sitelinks-search SearchAction or its crawlable URL template."); +} if (!home.includes("98.71%") || !home.includes("the evaluated full-history input was 77.81 times larger")) { errors.push("Homepage is missing the plain-language benchmark proof."); } @@ -242,6 +298,28 @@ if (home.indexOf('
') > home.indexOf('
Features', + 'href="/docs/getting-started/">Install', + 'href="/docs/">Docs' +]) { + if (!home.includes(primaryDestination)) errors.push(`Homepage navigation is missing ${primaryDestination}`); +} +for (const capability of [ + "Project-owned memory", + "Cited context compilation", + "Project structure and derived views", + "Coding-agent integrations", + "Team memory and portability", + "Verify the boundary" +]) { + if (!features.includes(capability)) errors.push(`Features page is missing ${capability}`); +} +if (!features.includes('"@type":"CollectionPage"') + || !features.includes('"@type":"ItemList"') + || !features.includes('"numberOfItems":11')) { + errors.push("Features page is missing its visible capability collection structured data."); +} for (const launchDirectoryMarkup of [ 'href="https://startupbase.io/products/qarinah?utm_source=startupbase&utm_medium=badge&utm_campaign=launch-badge-dark"', 'src="https://statics.startupbase.io/site/badges/launched-on-sb-dark.svg"', diff --git a/test/deploy-workflow.test.js b/test/deploy-workflow.test.js index ade1d28..ea5e1ae 100644 --- a/test/deploy-workflow.test.js +++ b/test/deploy-workflow.test.js @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import path from "node:path"; import test from "node:test"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -14,8 +14,11 @@ test("website deployment is bound to exact published Qarinah assets", async () = const config = JSON.parse(configSource); assert.equal(config.name, "qarinah"); + assert.equal(config.main, "./website/worker.mjs"); assert.equal(config.assets.directory, "./site-dist"); + assert.equal(config.assets.binding, "ASSETS"); assert.equal(config.assets.html_handling, "auto-trailing-slash"); + assert.equal(config.assets.run_worker_first, true); assert.equal(config.compatibility_date, "2026-08-08"); assert.doesNotMatch(workflow, /^\s+workflow_run:/m); @@ -48,6 +51,30 @@ test("website deployment is bound to exact published Qarinah assets", async () = assert.doesNotMatch(workflow, /gitHubToken:/); }); +test("website worker permanently upgrades HTTP before delegating canonical HTTPS assets", async () => { + const workerPath = path.join(root, "website", "worker.mjs"); + const worker = (await import(pathToFileURL(workerPath))).default; + let delegated = 0; + const env = { + ASSETS: { + async fetch(request) { + delegated += 1; + return new Response(request.url, { status: 200 }); + } + } + }; + + const redirect = await worker.fetch(new Request("http://qarinah.io/docs?source=search-console"), env); + assert.equal(redirect.status, 308); + assert.equal(redirect.headers.get("location"), "https://qarinah.io/docs?source=search-console"); + assert.equal(delegated, 0); + + const served = await worker.fetch(new Request("https://qarinah.io/docs/"), env); + assert.equal(served.status, 200); + assert.equal(await served.text(), "https://qarinah.io/docs/"); + assert.equal(delegated, 1); +}); + test("trusted npm publishing retries eventual-consistency signature checks", async () => { const workflow = await readFile( path.join(root, ".github", "workflows", "publish-npm.yml"), diff --git a/website/static/llms.txt b/website/static/llms.txt index 1ab2582..d87fce7 100644 --- a/website/static/llms.txt +++ b/website/static/llms.txt @@ -14,6 +14,7 @@ This file is a convenience map for tools that choose to read it. It is not a ran - [Qarinah alternatives](https://qarinah.io/alternatives/): Product-boundary comparison with Mem0, Letta, LangMem, LangGraph memory, Graphiti, Zep, and native coding-host memory. - [Open-source governed-agent toolkit](https://qarinah.io/articles/open-source-governed-agent-toolkit/): How Qarinah, Maqam, Cockroach Browser, and Cockroach Crawler can compose with established agent, browser, web-data, and document tools. - [Getting started](https://qarinah.io/docs/getting-started/): Install Qarinah, opt in one workspace, connect a supported host, and compile a cited context pack. +- [Features](https://qarinah.io/docs/features/): Complete product capability map for project-owned memory, cited retrieval, integrations, dashboards, team memory, portability, and boundaries. - [Documentation index](https://qarinah.io/docs/): Documentation by task. - [npm package](https://www.npmjs.com/package/qarinah): Published `qarinah` package and registry metadata. - [GitHub repository](https://github.com/AjnasNB/qarinah): Source, releases, issues, and contribution history. @@ -34,6 +35,7 @@ This file is a convenience map for tools that choose to read it. It is not a ran ## Technical documentation +- [Features](https://qarinah.io/docs/features/): Shipped project-memory, context-compilation, integration, team, export, and verification capabilities. - [CLI reference](https://qarinah.io/docs/cli/): Every command, option, stdin mode, exit code, and machine-readable output. - [JavaScript API reference](https://qarinah.io/docs/api/): Public exports, types, functions, and bounded programmatic workflows. - [Host integrations](https://qarinah.io/docs/integrations/): Codex, Claude Code, Cursor, CLI, and consent-gated MCP integration guidance. diff --git a/website/worker.mjs b/website/worker.mjs new file mode 100644 index 0000000..116b034 --- /dev/null +++ b/website/worker.mjs @@ -0,0 +1,10 @@ +export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.protocol === "http:") { + url.protocol = "https:"; + return Response.redirect(url.toString(), 308); + } + return env.ASSETS.fetch(request); + } +}; diff --git a/wrangler.jsonc b/wrangler.jsonc index b31be03..3e3f12a 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,9 +1,12 @@ { "$schema": "./node_modules/wrangler/config-schema.json", "name": "qarinah", + "main": "./website/worker.mjs", "compatibility_date": "2026-08-08", "assets": { "directory": "./site-dist", - "html_handling": "auto-trailing-slash" + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "run_worker_first": true } }