diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed5b3e1..52378b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,9 @@ jobs: - name: Typecheck run: npm run lint + - name: Validate layer catalog + run: npm run validate:catalog + - name: Test run: npm test diff --git a/PROVENANCE.md b/PROVENANCE.md new file mode 100644 index 0000000..e2a56a1 --- /dev/null +++ b/PROVENANCE.md @@ -0,0 +1,43 @@ +# Provenance & attribution + +This project is **MIT-licensed** (see per-file headers). This document records where +non-original ideas, data and dependencies come from, and the licensing boundaries we +respect — a discipline borrowed from [Marble's os-taxonomy](https://github.com/withmarbleapp/os-taxonomy), +which cleanly separates database vs. authored-content licenses. + +## Original to this repo + +- The deterministic **103-layer engine** (`brainsnn-r3f-app/src/lib/layerRouter.js` and the + layer modules): the Cognitive Firewall, Affective Decoder, TRIBE-style projection, business + metrics, receipts, and **Layer 103 — the 39 Hz soliton field** (`solitonLayer.js`). +- The **layer catalog** as a dependency-graphed, manifest-versioned dataset + (`src/lib/layerCatalog.js`, `data/layer-catalog.json`, `schema/layer-catalog.schema.json`). +- The **stdio MCP server** (`mcp-server/`) and the **neural decoder gateway** + (`src/lib/neuralInputGateway.js`, `/api/neural/*`). + +## Dependencies (bundled) + +| Component | Source | License | +| --- | --- | --- | +| App runtime | React 19, Vite 6, Express 4, Tailwind | MIT | +| WebGL 3D brain | `three`, `@react-three/fiber`, `@react-three/drei` (`src/features/brain3d/`) | MIT | +| Agent bridge | `@modelcontextprotocol/sdk` (`mcp-server/`) | MIT | +| Optional deep analysis | `@google/genai` (Gemini) — only when `GEMINI_API_KEY` is set | Apache-2.0 | + +## Inspirations & external boundaries (NOT bundled) + +- **Marble Skill Taxonomy — `withmarbleapp/os-taxonomy`** (database ODbL-1.0, content CC-BY-SA-4.0). + We borrowed the *approach* — a schema-validated graph with a prerequisite/dependency edge set, + a versioned manifest with a content hash, and a validator script. **No data was copied**; our + catalog and dependency edges are original to BrainSNN's own engine. +- **Meta Brain2Qwerty** (research code **CC-BY-NC-4.0**). Our decoder gateway analyzes *decoded + text* from such a model but **does not bundle any Brain2Qwerty code, weights or data**. A real + decoder is reached only over the network via `NEURAL_DECODER_URL` (see + `docs/DECODER_TRANSCRIPT_GATEWAY.md`), keeping the NC boundary clean. +- **Optional external services** — Stripe, Supabase, and a TRIBE projection service — are each + reached over the network behind an environment variable and are never required. + +## Data & measurement disclaimer + +BrainSNN outputs are AI-estimated content-response signals, **not** literal brain, biometric or +EEG measurements. The engine is deterministic and stores no source recordings. diff --git a/README.md b/README.md index 612e4e6..8b06a44 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,18 @@ This repo is a joint AI workspace coordinated through `.ai-memory/`. See `AGENTS - A new affect class in the decoder - A new soliton preset / sweep axis in `src/lib/solitonLayer.js` +## The layer catalog as data + +The 103-layer catalog is published as a validated dataset — nodes plus a data-flow **dependency +graph** and a versioned **manifest** (content-hashed) — inspired by +[os-taxonomy](https://github.com/withmarbleapp/os-taxonomy): + +- `brainsnn-r3f-app/data/layer-catalog.json` — the dataset (regenerate with `npm run export:catalog`) +- `brainsnn-r3f-app/schema/layer-catalog.schema.json` — its JSON Schema +- `npm run validate:catalog` — structural + referential + acyclicity check (also gated in CI) +- `GET /api/layers` now returns the `manifest` and `dependencies` alongside the layers + ## License -MIT — see per-file headers. +MIT — see per-file headers. Sources, dependencies and external licensing boundaries are recorded +in [PROVENANCE.md](PROVENANCE.md). diff --git a/brainsnn-r3f-app/data/layer-catalog.json b/brainsnn-r3f-app/data/layer-catalog.json new file mode 100644 index 0000000..8ba6412 --- /dev/null +++ b/brainsnn-r3f-app/data/layer-catalog.json @@ -0,0 +1,744 @@ +{ + "manifest": { + "schemaVersion": "brainsnn.layer-catalog.v1", + "version": "1.0.0", + "totalLayers": 103, + "coreLayerIds": [ + 3, + 4, + 5, + 29, + 36, + 39, + 40, + 46, + 48, + 63, + 70, + 101, + 102, + 103 + ], + "dependencyEdges": 6, + "groups": { + "view": 18, + "firewall": 45, + "share": 12, + "data": 14, + "backend": 9, + "progression": 5 + }, + "contentHash": "smjcw2" + }, + "groups": { + "view": { + "label": "3D & UX", + "color": "#00f5ff" + }, + "firewall": { + "label": "Cognitive Firewall", + "color": "#a855f7" + }, + "share": { + "label": "Share & Cards", + "color": "#f97316" + }, + "data": { + "label": "Data & State", + "color": "#22c55e" + }, + "backend": { + "label": "Backend & Agents", + "color": "#06b6d4" + }, + "progression": { + "label": "Progression", + "color": "#eab308" + } + }, + "layers": [ + { + "id": 1, + "name": "3D Brain Viewer", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 2, + "name": "Neural Flow Grid", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 3, + "name": "TRIBE v2 Frames", + "group": "backend", + "blurb": "TRIBE v2 and scenario projections mapped to the 7-region BrainSNN model." + }, + { + "id": 4, + "name": "Cognitive Firewall", + "group": "firewall", + "blurb": "Deterministic pressure scoring across urgency, outrage, certainty, fear and trust erosion." + }, + { + "id": 5, + "name": "Gemma 4 Engine", + "group": "firewall", + "blurb": "Gemma endpoint support for deep multimodal analysis when configured server-side." + }, + { + "id": 6, + "name": "Snapshots", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 7, + "name": "Analytics Dashboard", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 8, + "name": "Narrative Engine", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 9, + "name": "Toast Notifications", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 10, + "name": "Keyboard Shortcuts", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 11, + "name": "Share & Embed", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 12, + "name": "Onboarding Walkthrough", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 13, + "name": "Split Brain View", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 14, + "name": "AI Voice Narrator", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 15, + "name": "Plugin System", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 16, + "name": "Live Sync", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 17, + "name": "Heatmap Timeline", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 18, + "name": "Knowledge Brain", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 19, + "name": "MCP Bridge", + "group": "backend", + "blurb": "Backend & Agents layer preserved in the 103-layer engine stack." + }, + { + "id": 20, + "name": "Code-Aware Knowledge", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 21, + "name": "Brain Steward", + "group": "backend", + "blurb": "Backend & Agents layer preserved in the 103-layer engine stack." + }, + { + "id": 22, + "name": "Conversation Brain", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 23, + "name": "Cognitive Immunity Score", + "group": "progression", + "blurb": "Progression layer preserved in the 103-layer engine stack." + }, + { + "id": 24, + "name": "Real Embeddings", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 25, + "name": "Red Team Simulator", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 26, + "name": "Dream Mode", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 27, + "name": "Adversarial Training", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 28, + "name": "Neuro-RAG", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 29, + "name": "Affective Decoder", + "group": "firewall", + "blurb": "12-affect trigger decoding across threat, reward, social and cognitive clusters." + }, + { + "id": 30, + "name": "Neurochemistry Sandbox", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 31, + "name": "Brain Evolve", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 32, + "name": "Attack Evolve", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 33, + "name": "Multimodal RAG Router", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 34, + "name": "Vector-Graph Fusion", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 35, + "name": "Direct Content Insertion", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 36, + "name": "Autopsy Mode", + "group": "share", + "blurb": "Per-content autopsy and A/B battle rollups for variants and transcripts." + }, + { + "id": 37, + "name": "Cognitive Fragments", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 38, + "name": "Daily Firewall Challenge", + "group": "progression", + "blurb": "Progression layer preserved in the 103-layer engine stack." + }, + { + "id": 39, + "name": "Propaganda Templates", + "group": "firewall", + "blurb": "Named manipulation and persuasion templates, surfaced as concrete evidence." + }, + { + "id": 40, + "name": "Sentence Heatmap", + "group": "firewall", + "blurb": "Sentence-level heatmap annotation connected to recommendations." + }, + { + "id": 41, + "name": "Refutation Library", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 42, + "name": "Counter-Draft", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 43, + "name": "Time-Series Autopsy", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 44, + "name": "Inbox Mode", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 45, + "name": "Semantic Templates", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 46, + "name": "Firewall Receipts", + "group": "share", + "blurb": "Deterministic scan receipt for export, share links and audit trails." + }, + { + "id": 47, + "name": "Diff Mode", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 48, + "name": "Ad Transparency", + "group": "firewall", + "blurb": "Ad transparency archetypes such as FOMO, proof gap and coercive scarcity." + }, + { + "id": 49, + "name": "Scan Anywhere", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 50, + "name": "Weekly Recap", + "group": "progression", + "blurb": "Progression layer preserved in the 103-layer engine stack." + }, + { + "id": 51, + "name": "Style Fingerprint", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 52, + "name": "Multi-lingual Firewall", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 53, + "name": "Echo Detector", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 54, + "name": "Public Scoring API", + "group": "backend", + "blurb": "Backend & Agents layer preserved in the 103-layer engine stack." + }, + { + "id": 55, + "name": "Custom Rules Editor", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 56, + "name": "Badge System", + "group": "progression", + "blurb": "Progression layer preserved in the 103-layer engine stack." + }, + { + "id": 57, + "name": "Data Portability", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 58, + "name": "Image OCR Firewall", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 59, + "name": "Audio Firewall", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 60, + "name": "Firewall Macros", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 61, + "name": "Firewall Diagnostic", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 62, + "name": "Hypothesis Mode", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 63, + "name": "Context Memory", + "group": "data", + "blurb": "Entity and campaign memory for recurring context triggers." + }, + { + "id": 64, + "name": "Debate Mode", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 65, + "name": "Firewall Replay", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 66, + "name": "Coverage Heatmap", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 67, + "name": "Calendar Heatmap", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 68, + "name": "Tone Shifter", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 69, + "name": "Similarity Search", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 70, + "name": "Explanation Mode", + "group": "firewall", + "blurb": "Plain-English narration of what each engine layer contributed." + }, + { + "id": 71, + "name": "Neural Oscillations", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 72, + "name": "Layer Explorer", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 73, + "name": "Text Adventure Training", + "group": "progression", + "blurb": "Progression layer preserved in the 103-layer engine stack." + }, + { + "id": 74, + "name": "Firewall Comparator", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 75, + "name": "Region Drill-Down", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 76, + "name": "Streaming Scoring API", + "group": "backend", + "blurb": "Backend & Agents layer preserved in the 103-layer engine stack." + }, + { + "id": 77, + "name": "Session Rooms", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 78, + "name": "Vertical Card Variants", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 79, + "name": "Compliment Detector", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 80, + "name": "Decoy / Sarcasm Detector", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 81, + "name": "Browser Extension Generator", + "group": "share", + "blurb": "Share & Cards layer preserved in the 103-layer engine stack." + }, + { + "id": 82, + "name": "MCP Tool Expansion", + "group": "backend", + "blurb": "Backend & Agents layer preserved in the 103-layer engine stack." + }, + { + "id": 83, + "name": "Rule Pack Library", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 84, + "name": "Scan Archive", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 85, + "name": "Journalism Bulk Mode", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 86, + "name": "Privacy Budget", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 87, + "name": "Genre Classifier", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 88, + "name": "Persona Simulator", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 89, + "name": "Refutation Composer", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 90, + "name": "Personal Dictionary", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 91, + "name": "PWA Install", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 92, + "name": "Command Palette", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 93, + "name": "Feedback Calibration", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 94, + "name": "Role Tour", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 95, + "name": "Image Bbox Annotation", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 96, + "name": "Cross-device Sync", + "group": "data", + "blurb": "Data & State layer preserved in the 103-layer engine stack." + }, + { + "id": 97, + "name": "Hotkey Map", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 98, + "name": "Theme + A11y", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 99, + "name": "Federated Community Firewall", + "group": "firewall", + "blurb": "Cognitive Firewall layer preserved in the 103-layer engine stack." + }, + { + "id": 100, + "name": "Milestone Dashboard", + "group": "view", + "blurb": "3D & UX layer preserved in the 103-layer engine stack." + }, + { + "id": 101, + "name": "Gemini Deep Analysis", + "group": "backend", + "blurb": "Gemini/Gemma/OpenAI-style deep analysis and counter-draft guidance." + }, + { + "id": 102, + "name": "Veea Lobster Trap", + "group": "backend", + "blurb": "Prompt injection, PII and secret preflight before model calls." + }, + { + "id": 103, + "name": "39 Hz Soliton Field", + "group": "backend", + "blurb": "39 Hz microtubule soliton field: gamma-band synchrony and leapfrogging ionic solitons as a deterministic signal layer." + } + ], + "dependencies": [ + { + "id": 29, + "dependsOn": [ + 4 + ], + "reason": "Affect arousal is sharpened by the firewall's emotional-activation signal." + }, + { + "id": 3, + "dependsOn": [ + 4, + 29 + ], + "reason": "The 7-region projection is driven by firewall pressure and affect valence/arousal." + }, + { + "id": 48, + "dependsOn": [ + 4 + ], + "reason": "Business metrics (manipulation risk, hook strength) derive from firewall signals." + }, + { + "id": 103, + "dependsOn": [ + 4, + 29 + ], + "reason": "The soliton lattice is detuned by manipulation pressure and driven by affect arousal." + }, + { + "id": 70, + "dependsOn": [ + 4, + 29, + 3 + ], + "reason": "The narration summarizes what the firewall, affect and projection layers contributed." + }, + { + "id": 46, + "dependsOn": [ + 4, + 103 + ], + "reason": "The receipt hashes the firewall signals and the soliton field for reproducibility." + } + ], + "coreLayerIds": [ + 3, + 4, + 5, + 29, + 36, + 39, + 40, + 46, + 48, + 63, + 70, + 101, + 102, + 103 + ] +} diff --git a/brainsnn-r3f-app/package.json b/brainsnn-r3f-app/package.json index 22c0cad..26b70f7 100644 --- a/brainsnn-r3f-app/package.json +++ b/brainsnn-r3f-app/package.json @@ -12,7 +12,9 @@ "lint": "tsc --noEmit", "test": "node scripts/test-runner.mjs", "test:run": "node scripts/test-runner.mjs", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "validate:catalog": "node scripts/validate-catalog.mjs", + "export:catalog": "node scripts/export-catalog.mjs" }, "dependencies": { "@google/genai": "^2.4.0", diff --git a/brainsnn-r3f-app/schema/layer-catalog.schema.json b/brainsnn-r3f-app/schema/layer-catalog.schema.json new file mode 100644 index 0000000..dd22a96 --- /dev/null +++ b/brainsnn-r3f-app/schema/layer-catalog.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://brainsnn.com/schema/layer-catalog.v1.json", + "title": "BrainSNN Layer Catalog", + "description": "The BrainSNN engine's layer catalog published as data: layers (nodes), a data-flow dependency graph (edges), and a versioned manifest. Generated by scripts/export-catalog.mjs.", + "type": "object", + "required": ["manifest", "groups", "layers", "dependencies", "coreLayerIds"], + "additionalProperties": false, + "properties": { + "manifest": { + "type": "object", + "required": ["schemaVersion", "version", "totalLayers", "coreLayerIds", "dependencyEdges", "groups", "contentHash"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "type": "string", "const": "brainsnn.layer-catalog.v1" }, + "version": { "type": "string" }, + "totalLayers": { "type": "integer", "minimum": 1 }, + "coreLayerIds": { "type": "array", "items": { "type": "integer", "minimum": 1 } }, + "dependencyEdges": { "type": "integer", "minimum": 0 }, + "groups": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 } }, + "contentHash": { "type": "string", "description": "Deterministic FNV-1a hash over layers + core ids + dependency edges." } + } + }, + "groups": { + "type": "object", + "description": "Group id → { label, color }.", + "additionalProperties": { + "type": "object", + "required": ["label", "color"], + "properties": { + "label": { "type": "string" }, + "color": { "type": "string" } + } + } + }, + "layers": { + "type": "array", + "description": "The layers (graph nodes).", + "items": { + "type": "object", + "required": ["id", "name", "group", "blurb"], + "additionalProperties": false, + "properties": { + "id": { "type": "integer", "minimum": 1 }, + "name": { "type": "string" }, + "group": { "type": "string" }, + "blurb": { "type": "string", "minLength": 1 } + } + } + }, + "dependencies": { + "type": "array", + "description": "Data-flow edges: a layer depends on the layers whose output it consumes inside runLayerRouter. Must be acyclic.", + "items": { + "type": "object", + "required": ["id", "dependsOn", "reason"], + "additionalProperties": false, + "properties": { + "id": { "type": "integer", "minimum": 1 }, + "dependsOn": { "type": "array", "items": { "type": "integer", "minimum": 1 } }, + "reason": { "type": "string", "minLength": 1 } + } + } + }, + "coreLayerIds": { + "type": "array", + "description": "The layers evaluated on every scan.", + "items": { "type": "integer", "minimum": 1 } + } + } +} diff --git a/brainsnn-r3f-app/scripts/export-catalog.mjs b/brainsnn-r3f-app/scripts/export-catalog.mjs new file mode 100644 index 0000000..ccd600a --- /dev/null +++ b/brainsnn-r3f-app/scripts/export-catalog.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// Publishes the layer catalog as pure data (os-taxonomy style): a single JSON +// dataset consumable by agents/tools without importing the app's JS. Run after +// changing the catalog: `npm run export:catalog`. A test keeps the committed +// file in sync via the manifest content hash. +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { + LAYER_CATALOG, + LAYER_GROUPS, + LAYER_DEPENDENCIES, + CORE_LAYER_IDS, + getCatalogManifest, +} from '../src/lib/layerCatalog.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const outPath = join(here, '..', 'data', 'layer-catalog.json'); + +const dataset = { + manifest: getCatalogManifest(), + groups: LAYER_GROUPS, + layers: LAYER_CATALOG, + dependencies: LAYER_DEPENDENCIES, + coreLayerIds: CORE_LAYER_IDS, +}; + +writeFileSync(outPath, `${JSON.stringify(dataset, null, 2)}\n`); +console.log(`Wrote ${outPath} (hash ${dataset.manifest.contentHash})`); diff --git a/brainsnn-r3f-app/scripts/validate-catalog.mjs b/brainsnn-r3f-app/scripts/validate-catalog.mjs new file mode 100644 index 0000000..c3d699d --- /dev/null +++ b/brainsnn-r3f-app/scripts/validate-catalog.mjs @@ -0,0 +1,18 @@ +#!/usr/bin/env node +// Structural + referential integrity check for the layer catalog and its +// dependency graph. Inspired by os-taxonomy's scripts/validate.mjs. Exits +// non-zero on any error so CI can gate it. +import { validateCatalog, getCatalogManifest } from '../src/lib/layerCatalog.js'; + +const { ok, errors } = validateCatalog(); +const manifest = getCatalogManifest(); + +if (!ok) { + console.error(`✗ Layer catalog invalid — ${errors.length} error(s):`); + for (const error of errors) console.error(` - ${error}`); + process.exit(1); +} + +console.log( + `✓ Layer catalog valid — ${manifest.totalLayers} layers, ${manifest.dependencyEdges} dependency edges, hash ${manifest.contentHash}`, +); diff --git a/brainsnn-r3f-app/server.ts b/brainsnn-r3f-app/server.ts index 99ccc95..2b3ea75 100644 --- a/brainsnn-r3f-app/server.ts +++ b/brainsnn-r3f-app/server.ts @@ -10,7 +10,7 @@ import { getEngineStatusSnapshot, runLayerRouter } from "./src/lib/layerRouter.js"; -import { LAYER_CATALOG } from "./src/lib/layerCatalog.js"; +import { LAYER_CATALOG, LAYER_DEPENDENCIES, getCatalogManifest } from "./src/lib/layerCatalog.js"; import { SOLITON_PRESETS, computeSolitonPreset, exploreSolitonField } from "./src/lib/solitonLayer.js"; import { computeFirewall } from "./src/lib/firewallLayer.js"; import { computeAffect } from "./src/lib/affectLayer.js"; @@ -173,8 +173,10 @@ app.get("/api/layers", (_req, res) => { const status = getEngineStatusSnapshot(process.env); res.json({ totalLayers: LAYER_CATALOG.length, + manifest: (getCatalogManifest as any)(), coreLayers: status.coreLayers, layers: LAYER_CATALOG, + dependencies: LAYER_DEPENDENCIES, }); }); diff --git a/brainsnn-r3f-app/src/lib/layerCatalog.js b/brainsnn-r3f-app/src/lib/layerCatalog.js index 955d294..58aecb1 100644 --- a/brainsnn-r3f-app/src/lib/layerCatalog.js +++ b/brainsnn-r3f-app/src/lib/layerCatalog.js @@ -93,3 +93,116 @@ export function searchLayers(query = '', group = 'all') { || LAYER_GROUPS[layer.group]?.label.toLowerCase().includes(q); }); } + +// --------------------------------------------------------------------------- +// Layer dependency graph, manifest and validation +// --------------------------------------------------------------------------- +// Inspired by Marble's os-taxonomy (a schema-validated knowledge graph with a +// prerequisite dependency graph, a versioned manifest and a validator). Here +// the "prerequisites" are the real data-flow edges inside `runLayerRouter`: +// which core layer consumes another layer's output. + +export const CATALOG_SCHEMA_VERSION = 'brainsnn.layer-catalog.v1'; +export const CATALOG_VERSION = '1.0.0'; + +// { id, dependsOn: [ids], reason } — a DAG that mirrors runLayerRouter's flow. +export const LAYER_DEPENDENCIES = [ + { id: 29, dependsOn: [4], reason: 'Affect arousal is sharpened by the firewall\'s emotional-activation signal.' }, + { id: 3, dependsOn: [4, 29], reason: 'The 7-region projection is driven by firewall pressure and affect valence/arousal.' }, + { id: 48, dependsOn: [4], reason: 'Business metrics (manipulation risk, hook strength) derive from firewall signals.' }, + { id: 103, dependsOn: [4, 29], reason: 'The soliton lattice is detuned by manipulation pressure and driven by affect arousal.' }, + { id: 70, dependsOn: [4, 29, 3], reason: 'The narration summarizes what the firewall, affect and projection layers contributed.' }, + { id: 46, dependsOn: [4, 103], reason: 'The receipt hashes the firewall signals and the soliton field for reproducibility.' }, +]; + +export function dependenciesFor(id) { + return LAYER_DEPENDENCIES.find((edge) => edge.id === id)?.dependsOn ?? []; +} + +function catalogHash() { + const payload = JSON.stringify({ + layers: LAYER_CATALOG.map((layer) => [layer.id, layer.name, layer.group]), + core: CORE_LAYER_IDS, + deps: LAYER_DEPENDENCIES.map((edge) => [edge.id, edge.dependsOn]), + }); + let hash = 2166136261; + for (let i = 0; i < payload.length; i += 1) { + hash ^= payload.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return (hash >>> 0).toString(36); +} + +// Deterministic manifest (no timestamps) so the content hash is stable and can +// be cited by a scan receipt. +export function getCatalogManifest() { + const groups = {}; + for (const key of Object.keys(LAYER_GROUPS)) { + groups[key] = LAYER_CATALOG.filter((layer) => layer.group === key).length; + } + return { + schemaVersion: CATALOG_SCHEMA_VERSION, + version: CATALOG_VERSION, + totalLayers: LAYER_CATALOG.length, + coreLayerIds: [...CORE_LAYER_IDS], + dependencyEdges: LAYER_DEPENDENCIES.length, + groups, + contentHash: catalogHash(), + }; +} + +// Structural + referential integrity check. Returns { ok, errors }. +export function validateCatalog() { + const errors = []; + const ids = LAYER_CATALOG.map((layer) => layer.id); + const idSet = new Set(ids); + + if (idSet.size !== ids.length) errors.push('Duplicate layer ids.'); + ids.forEach((id, index) => { + if (id !== index + 1) errors.push(`Layer ids must be contiguous from 1; got ${id} at position ${index + 1}.`); + }); + + LAYER_CATALOG.forEach((layer) => { + if (!LAYER_GROUPS[layer.group]) errors.push(`Layer ${layer.id} has unknown group "${layer.group}".`); + if (!layer.blurb || !layer.blurb.trim()) errors.push(`Layer ${layer.id} has an empty blurb.`); + }); + + new Set(CORE_LAYER_IDS).forEach((id) => { + if (!idSet.has(id)) errors.push(`CORE_LAYER_IDS references missing layer ${id}.`); + }); + if (new Set(CORE_LAYER_IDS).size !== CORE_LAYER_IDS.length) errors.push('Duplicate ids in CORE_LAYER_IDS.'); + + const seenEdges = new Set(); + LAYER_DEPENDENCIES.forEach((edge) => { + if (!idSet.has(edge.id)) errors.push(`Dependency references missing layer ${edge.id}.`); + if (seenEdges.has(edge.id)) errors.push(`Duplicate dependency entry for layer ${edge.id}.`); + seenEdges.add(edge.id); + (edge.dependsOn || []).forEach((dep) => { + if (!idSet.has(dep)) errors.push(`Layer ${edge.id} depends on missing layer ${dep}.`); + if (dep === edge.id) errors.push(`Layer ${edge.id} depends on itself.`); + }); + if (!edge.reason || !edge.reason.trim()) errors.push(`Dependency ${edge.id} has no reason.`); + }); + + // Cycle detection (DFS over the dependency edges). + const adjacency = new Map(LAYER_DEPENDENCIES.map((edge) => [edge.id, edge.dependsOn || []])); + const state = new Map(); + const visit = (node) => { + if (state.get(node) === 'done') return false; + if (state.get(node) === 'active') return true; + state.set(node, 'active'); + for (const next of adjacency.get(node) || []) { + if (visit(next)) return true; + } + state.set(node, 'done'); + return false; + }; + for (const edge of LAYER_DEPENDENCIES) { + if (visit(edge.id)) { + errors.push(`Dependency cycle detected at layer ${edge.id}.`); + break; + } + } + + return { ok: errors.length === 0, errors }; +} diff --git a/brainsnn-r3f-app/src/lib/layerCatalog.test.js b/brainsnn-r3f-app/src/lib/layerCatalog.test.js new file mode 100644 index 0000000..3def24a --- /dev/null +++ b/brainsnn-r3f-app/src/lib/layerCatalog.test.js @@ -0,0 +1,46 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from '../test/tinyVitest.js'; +import { + LAYER_CATALOG, + LAYER_DEPENDENCIES, + CORE_LAYER_IDS, + dependenciesFor, + getCatalogManifest, + validateCatalog, +} from './layerCatalog.js'; + +describe('layer catalog', () => { + it('passes structural + referential validation', () => { + const { ok, errors } = validateCatalog(); + expect(errors).toHaveLength(0); + expect(ok).toBe(true); + }); + + it('produces a deterministic, self-consistent manifest', () => { + const manifest = getCatalogManifest(); + expect(manifest.schemaVersion).toBe('brainsnn.layer-catalog.v1'); + expect(manifest.totalLayers).toBe(LAYER_CATALOG.length); + expect(manifest.dependencyEdges).toBe(LAYER_DEPENDENCIES.length); + expect(getCatalogManifest().contentHash).toBe(manifest.contentHash); + const groupTotal = Object.values(manifest.groups).reduce((sum, count) => sum + count, 0); + expect(groupTotal).toBe(LAYER_CATALOG.length); + }); + + it('exposes an acyclic dependency graph that only references real core layers', () => { + const ids = new Set(LAYER_CATALOG.map((layer) => layer.id)); + LAYER_DEPENDENCIES.forEach((edge) => { + expect(ids.has(edge.id)).toBe(true); + edge.dependsOn.forEach((dep) => expect(ids.has(dep)).toBe(true)); + }); + // The soliton layer (103) depends on firewall (4) and affect (29). + expect(dependenciesFor(103)).toContain(4); + expect(dependenciesFor(103)).toContain(29); + }); + + it('keeps the published data/layer-catalog.json in sync (run npm run export:catalog)', () => { + const dataset = JSON.parse(readFileSync(new URL('../../data/layer-catalog.json', import.meta.url), 'utf8')); + expect(dataset.manifest.contentHash).toBe(getCatalogManifest().contentHash); + expect(dataset.layers).toHaveLength(LAYER_CATALOG.length); + expect(dataset.coreLayerIds).toHaveLength(CORE_LAYER_IDS.length); + }); +});