From ca9a850b1e340d5e7714bfff52b52cf960293919 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:23:33 +0530 Subject: [PATCH 01/26] Add evidence-first DISHA Brain spine --- .env.example | 2 +- .github/workflows/disha-brain.yml | 25 ++ README.md | 293 +++++------------- demos/demo_1_6_geospatial.json | 7 + demos/demo_2_6_sustainable_development.json | 7 + demos/demo_3_6_physical_interface.json | 7 + demos/demo_4_6_hse.json | 7 + demos/demo_5_6_national_audit.json | 7 + demos/demo_6_6_gap_closure.json | 7 + disha/brain/api/routes.py | 14 + disha/brain/audit/__init__.py | 7 + disha/brain/audit/event_schema.py | 17 + disha/brain/audit/exporter.py | 10 + disha/brain/audit/hash_chain.py | 8 + disha/brain/audit/ledger.py | 28 ++ disha/brain/config.py | 2 +- disha/brain/evidence/__init__.py | 13 + disha/brain/evidence/classifier.py | 83 +++++ disha/brain/evidence/evidence_bundle.py | 7 + disha/brain/evidence/models.py | 44 +++ disha/brain/evidence/provenance.py | 19 ++ disha/brain/evidence/source_registry.py | 25 ++ disha/brain/geospatial/__init__.py | 8 + disha/brain/geospatial/coordinates.py | 17 + disha/brain/geospatial/risk_map.py | 11 + disha/brain/geospatial/spatial_index.py | 15 + disha/brain/geospatial/tracker.py | 21 ++ disha/brain/governance/__init__.py | 14 + .../brain/governance/constitutional_mapper.py | 14 + disha/brain/governance/nyaya.py | 8 + disha/brain/governance/open_data_audit.py | 10 + disha/brain/governance/rti_parser.py | 9 + disha/brain/graph/__init__.py | 12 + disha/brain/graph/graph.py | 62 ++++ disha/brain/graph/nodes/__init__.py | 1 + disha/brain/graph/nodes/action_agent.py | 28 ++ disha/brain/graph/nodes/audit_agent.py | 25 ++ disha/brain/graph/nodes/context_agent.py | 12 + disha/brain/graph/nodes/evidence_agent.py | 15 + .../brain/graph/nodes/human_approval_agent.py | 16 + disha/brain/graph/nodes/intake_agent.py | 15 + .../brain/graph/nodes/memory_update_agent.py | 11 + disha/brain/graph/nodes/policy_guard_agent.py | 14 + disha/brain/graph/nodes/reasoning_agent.py | 11 + disha/brain/graph/nodes/vyuha_agent.py | 12 + disha/brain/graph/nodes/yudh_agent.py | 12 + disha/brain/graph/router.py | 19 ++ disha/brain/graph/state.py | 70 +++++ disha/brain/memory/__init__.py | 5 + disha/brain/memory/episodic.py | 18 ++ disha/brain/memory/memory_store.py | 22 ++ disha/brain/memory/semantic.py | 13 + disha/brain/memory/working.py | 12 + disha/brain/policy/__init__.py | 18 ++ disha/brain/policy/human_approval.py | 14 + disha/brain/policy/no_first_use.py | 89 ++++++ disha/brain/policy/permissions.py | 15 + disha/brain/policy/risk_policy.py | 14 + disha/brain/sectors/__init__.py | 1 + disha/brain/sectors/development/__init__.py | 8 + .../sectors/development/resilience_index.py | 11 + disha/brain/sectors/development/setu.py | 8 + disha/brain/sectors/development/varuna.py | 8 + disha/brain/sectors/hse/__init__.py | 8 + disha/brain/sectors/hse/education.py | 6 + disha/brain/sectors/hse/health.py | 6 + disha/brain/sectors/hse/social_welfare.py | 6 + disha/brain/versions/__init__.py | 21 ++ disha/brain/versions/common.py | 16 + .../versions/v1_6_geospatial_detection.py | 22 ++ .../versions/v2_6_sustainable_development.py | 16 + .../brain/versions/v3_6_physical_interface.py | 14 + disha/brain/versions/v4_6_hse.py | 17 + disha/brain/versions/v5_6_national_audit.py | 24 ++ disha/brain/versions/v6_6_gap_closure.py | 16 + disha/brain/vyuha/__init__.py | 7 + disha/brain/vyuha/formations.py | 143 +++++++++ disha/brain/vyuha/playbook.py | 22 ++ disha/brain/vyuha/selector.py | 18 ++ disha/brain/yudh/__init__.py | 7 + disha/brain/yudh/assessment.py | 25 ++ disha/brain/yudh/gap_model.py | 17 + disha/brain/yudh/threat_model.py | 14 + docs/ARCHITECTURE.md | 42 +++ docs/DEMOS.md | 26 ++ docs/DISHA_BRAIN.md | 14 + docs/EVIDENCE_MODEL.md | 18 ++ docs/LANGGRAPH_AGENTIC_FLOW.md | 14 + docs/NO_FIRST_USE.md | 10 + docs/PRODUCT_THESIS.md | 10 + docs/ROADMAP.md | 26 ++ docs/TRUST_MODEL.md | 14 + docs/VERSION_LADDER.md | 15 + docs/YUDH_VYUHA_DOCTRINE.md | 23 ++ tests/test_disha_brain_graph.py | 125 ++++++++ 95 files changed, 1896 insertions(+), 223 deletions(-) create mode 100644 .github/workflows/disha-brain.yml create mode 100644 demos/demo_1_6_geospatial.json create mode 100644 demos/demo_2_6_sustainable_development.json create mode 100644 demos/demo_3_6_physical_interface.json create mode 100644 demos/demo_4_6_hse.json create mode 100644 demos/demo_5_6_national_audit.json create mode 100644 demos/demo_6_6_gap_closure.json create mode 100644 disha/brain/audit/__init__.py create mode 100644 disha/brain/audit/event_schema.py create mode 100644 disha/brain/audit/exporter.py create mode 100644 disha/brain/audit/hash_chain.py create mode 100644 disha/brain/audit/ledger.py create mode 100644 disha/brain/evidence/__init__.py create mode 100644 disha/brain/evidence/classifier.py create mode 100644 disha/brain/evidence/evidence_bundle.py create mode 100644 disha/brain/evidence/models.py create mode 100644 disha/brain/evidence/provenance.py create mode 100644 disha/brain/evidence/source_registry.py create mode 100644 disha/brain/geospatial/__init__.py create mode 100644 disha/brain/geospatial/coordinates.py create mode 100644 disha/brain/geospatial/risk_map.py create mode 100644 disha/brain/geospatial/spatial_index.py create mode 100644 disha/brain/geospatial/tracker.py create mode 100644 disha/brain/governance/__init__.py create mode 100644 disha/brain/governance/constitutional_mapper.py create mode 100644 disha/brain/governance/nyaya.py create mode 100644 disha/brain/governance/open_data_audit.py create mode 100644 disha/brain/governance/rti_parser.py create mode 100644 disha/brain/graph/__init__.py create mode 100644 disha/brain/graph/graph.py create mode 100644 disha/brain/graph/nodes/__init__.py create mode 100644 disha/brain/graph/nodes/action_agent.py create mode 100644 disha/brain/graph/nodes/audit_agent.py create mode 100644 disha/brain/graph/nodes/context_agent.py create mode 100644 disha/brain/graph/nodes/evidence_agent.py create mode 100644 disha/brain/graph/nodes/human_approval_agent.py create mode 100644 disha/brain/graph/nodes/intake_agent.py create mode 100644 disha/brain/graph/nodes/memory_update_agent.py create mode 100644 disha/brain/graph/nodes/policy_guard_agent.py create mode 100644 disha/brain/graph/nodes/reasoning_agent.py create mode 100644 disha/brain/graph/nodes/vyuha_agent.py create mode 100644 disha/brain/graph/nodes/yudh_agent.py create mode 100644 disha/brain/graph/router.py create mode 100644 disha/brain/graph/state.py create mode 100644 disha/brain/memory/__init__.py create mode 100644 disha/brain/memory/episodic.py create mode 100644 disha/brain/memory/memory_store.py create mode 100644 disha/brain/memory/semantic.py create mode 100644 disha/brain/memory/working.py create mode 100644 disha/brain/policy/__init__.py create mode 100644 disha/brain/policy/human_approval.py create mode 100644 disha/brain/policy/no_first_use.py create mode 100644 disha/brain/policy/permissions.py create mode 100644 disha/brain/policy/risk_policy.py create mode 100644 disha/brain/sectors/__init__.py create mode 100644 disha/brain/sectors/development/__init__.py create mode 100644 disha/brain/sectors/development/resilience_index.py create mode 100644 disha/brain/sectors/development/setu.py create mode 100644 disha/brain/sectors/development/varuna.py create mode 100644 disha/brain/sectors/hse/__init__.py create mode 100644 disha/brain/sectors/hse/education.py create mode 100644 disha/brain/sectors/hse/health.py create mode 100644 disha/brain/sectors/hse/social_welfare.py create mode 100644 disha/brain/versions/__init__.py create mode 100644 disha/brain/versions/common.py create mode 100644 disha/brain/versions/v1_6_geospatial_detection.py create mode 100644 disha/brain/versions/v2_6_sustainable_development.py create mode 100644 disha/brain/versions/v3_6_physical_interface.py create mode 100644 disha/brain/versions/v4_6_hse.py create mode 100644 disha/brain/versions/v5_6_national_audit.py create mode 100644 disha/brain/versions/v6_6_gap_closure.py create mode 100644 disha/brain/vyuha/__init__.py create mode 100644 disha/brain/vyuha/formations.py create mode 100644 disha/brain/vyuha/playbook.py create mode 100644 disha/brain/vyuha/selector.py create mode 100644 disha/brain/yudh/__init__.py create mode 100644 disha/brain/yudh/assessment.py create mode 100644 disha/brain/yudh/gap_model.py create mode 100644 disha/brain/yudh/threat_model.py create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEMOS.md create mode 100644 docs/DISHA_BRAIN.md create mode 100644 docs/EVIDENCE_MODEL.md create mode 100644 docs/LANGGRAPH_AGENTIC_FLOW.md create mode 100644 docs/NO_FIRST_USE.md create mode 100644 docs/PRODUCT_THESIS.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/TRUST_MODEL.md create mode 100644 docs/VERSION_LADDER.md create mode 100644 docs/YUDH_VYUHA_DOCTRINE.md create mode 100644 tests/test_disha_brain_graph.py diff --git a/.env.example b/.env.example index 5e032f92..1965184c 100644 --- a/.env.example +++ b/.env.example @@ -35,7 +35,7 @@ PHYSICS_API_PORT=8002 # Port for physics backend # ── DISHA Brain (Unified Backend + Edge Agent) ───────────────────── # Brain backend (FastAPI) -DISHA_BRAIN_API_HOST=0.0.0.0 +DISHA_BRAIN_API_HOST=127.0.0.1 DISHA_BRAIN_API_PORT=8080 DISHA_BRAIN_API_TOKEN=change-me DISHA_BRAIN_DB_PATH=./data/disha_brain.db diff --git a/.github/workflows/disha-brain.yml b/.github/workflows/disha-brain.yml new file mode 100644 index 00000000..5e5440fa --- /dev/null +++ b/.github/workflows/disha-brain.yml @@ -0,0 +1,25 @@ +name: DISHA Brain + +on: + pull_request: + push: + branches: [main] + +jobs: + brain-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pydantic pytest ruff bandit + - name: Lint production brain spine + run: ruff check disha/brain tests + - name: Security scan production brain spine + run: bandit -q -r disha/brain + - name: Run graph tests + run: pytest tests/test_disha_brain_graph.py diff --git a/README.md b/README.md index 44ef45d7..e476ff82 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,74 @@ -# DISHA OS +# DISHA -![DISHA OS](docs/images/disha-os-hero.png) +DISHA is an evidence-first agentic intelligence architecture for national-scale reasoning, geospatial awareness, governance audit, cyber defence, public-sector accountability, and human-governed action. -DISHA OS is an AI Cyber Evidence Operating System built around DISHA Brain: a local-first, audit-first intelligence core for secure automation, cyber monitoring, evidence-grade logging, and operator control surfaces. +It is not a generic chatbot. The production direction is a defensive, audit-first system that classifies evidence, links sources, applies No-First-Use boundaries, routes work through versioned intelligence modules, and requires human approval when risk or ambiguity crosses policy thresholds. -Docs live in `docs/` (including a Wiki-ready structure under `docs/wiki/`). +## What Is Production-Oriented Now -## Deploy (Docker) +- `disha/brain/`: central reasoning spine, FastAPI backend, graph orchestration, policy gates, evidence model, memory, audit, Yudh/Vyuha logic, and six version modules. +- `web/`: hardened Next.js API surface with auth, audit, export/share, file, and agent endpoints. +- `src/`: TypeScript CLI/runtime hardening modules and MCP entrypoint. +- `demos/`: runnable JSON payloads for the six DISHA versions. +- `tests/test_disha_brain_graph.py`: focused tests for evidence classification, version routing, NFU, Vyuha, audit, memory, geospatial validation, HSE, national audit, and end-to-end graph invocation. -- Local/dev: `docker-compose.yml` -- Production-style: `docker-compose.prod.yml` (pulls images from GHCR) +Legacy and experimental surfaces remain under `legacy/`, `disha/services/`, `disha/ai/`, and integrations. They should be reused only after source review. -## OS Artifact +## Six DISHA Versions -CI can build a bootable ISO (`disha-os-amd64.iso`) via the `Release (Images + OS ISO)` workflow. +| Version | Module | Purpose | +| --- | --- | --- | +| 1.6 | Geospatial Detection Intelligence | Coordinates, sensor evidence, object tracking, public-safety evidence bundles. | +| 2.6 | Sustainable Development Geospatial Intelligence | Infrastructure monitoring, climate/resource signals, SETU/VARUNA resilience scoring. | +| 3.6 | Physical Interface Architecture | Edge telemetry, trusted device state, sensor-to-brain routing, operator control. | +| 4.6 | HSE Intelligence | Health, social welfare, education access, district-level service gap reports. | +| 5.6 | National Audit Intelligence | RTI, open data, constitutional references, public accountability, contradiction detection. | +| 6.6 | Gap Closure Intelligence | Gap identification, risk mitigation, Yudh assessment, Vyuha selection, lawful corrective action. | -Primary runtime surfaces: - -- `web/`: hardened Next.js application with authenticated API routes, RBAC, CSRF protection, audit logging, and export/share workflows. -- `src/`: TypeScript CLI/runtime surface focused on secure execution, MCP entrypoints, storage policy, and observability adapters. -- `disha/brain/`: unified FastAPI brain backend (reasoning/planning/execution, anomaly detection, risk scoring, decisioning, events, WebSocket alerts). -- `disha/edge_agent/`: desktop telemetry agent sending signed telemetry to the brain backend. - -Core use cases: -- Secure “AI ops” command center (audit-first, policy-gated actions) -- Cross-device assistant runtime (web + agent + brain) -- Telemetry-driven anomaly detection and explainable alerts -- Modular subsystem integration (MCP, cyber defense, strategy, quantum physics) - -Search keywords: -`secure ai agent`, `jarvis-like assistant`, `zero trust`, `audit logging`, `policy engine`, `anomaly detection`, `fastapi`, `nextjs`, `mcp` - -## Why DISHA - -- Secure by default web and CLI foundations. -- Explicit AI workflow boundaries instead of opaque prompt pipelines. -- Support for interactive web, CLI, and Python service surfaces in one repository. -- Operational focus on auditability, local development, and staged hardening. - -## Repository Map - -```text -. -|-- web/ Next.js app and secure API surface -|-- src/ TypeScript CLI/runtime hardening modules -|-- disha/ Brain backend, AI core modules, agents, and services -|-- docker/ Compose and observability assets -|-- docs/ Architecture, wiki, design, TDD, analysis -|-- legacy/ retired prototypes and legacy stacks -`-- .github/workflows/ CI, CodeQL, security, and module pipelines -``` - -## Architecture Summary - -![DISHA Brain Platform Architecture](docs/images/disha-brain-platform-premium.png) +## Agentic Flow ```mermaid -flowchart LR - U[User] --> W[Next.js Web] - U --> C[TypeScript CLI] - W --> B[DISHA Brain API] - W --> WS[Web Services Layer] - WS --> SEC[Auth RBAC CSRF Rate Limit Audit] - WS --> DB[(Postgres)] - WS --> REDIS[(Redis)] - WS --> EXT[Model or Backend Services] - C --> MCP[MCP Entrypoint] - MCP --> CSEC[Secure Storage Policy and Audit] - CORE[AI Core Modules] --> B -``` - -## Agent Runtime (Token-Efficient) - -DISHA Web includes a small “agent runtime” focused on reducing LLM token spend while keeping behavior auditable and deterministic for critical flows. - -- Token economy: automatic context compaction + Redis-backed response cache (`DISHA_AGENT_MODE`, `DISHA_AGENT_INPUT_BUDGET_TOKENS`) -- Memory graph: privacy-first entity graph updated from user messages (`GET /api/agent/memory-graph`) -- Workflow runner: n8n-like node pipeline with per-node + total timeouts (`POST /api/agent/workflows/run`) - -The production-ready platform path is `web/` + `src/` + `disha/brain/`. Other folders remain in the repo as legacy or experimental surfaces and are progressively converged or retired behind stable interfaces. - -## Tech Stack - -| Area | Stack | -| --- | --- | -| Web | Next.js, React, TypeScript, Zod | -| CLI | TypeScript, MCP, OpenTelemetry APIs | -| Data | Postgres, Redis | -| Python Services | FastAPI, Pydantic, PyTorch-adjacent research modules | -| Tooling | npm, Bun, Docker Compose, GitHub Actions, Ruff, mypy, Bandit, CodeQL | - -## Local Setup - -### Prerequisites - -- Node.js 20+ -- npm 10+ -- Python 3.11+ -- Docker and Docker Compose - -### 1. Clone - -```bash -git clone https://github.com/Tashima-Tarsh/Disha.git -cd Disha -``` - -### 2. Install web dependencies - -```bash -cd web -npm install -``` - -### 3. Configure environment - -Use [web/.env.example](web/.env.example) as the baseline. The minimum local values are: - -```bash -DISHA_AUTH_MODE=dev-jwt -DISHA_JWT_SECRET=<32+ random characters> -DISHA_DEV_PASSWORD= -DATABASE_URL=postgresql://disha:postgres@localhost:5432/disha -REDIS_URL=redis://localhost:6379 -DISHA_WORKSPACE_ROOT=.. -``` - -### 4. Start infrastructure - -```bash -cd docker -docker compose up postgres redis -d -``` - -### 5. Run the web app - -```bash -cd web -npm run dev +flowchart TD + A[Input sources] --> B[Intake Agent] + B --> C[Evidence Agent] + C --> D[Context Agent] + D --> E[DISHA Brain Reasoning Agent] + E --> F[Version Router] + F --> G[Version Module] + G --> H[Yudh Intelligence Agent] + H --> I[Vyuha Selector Agent] + I --> J[Policy Guard and NFU Gate] + J --> K{Human approval required?} + K -->|Yes| L[Human Approval Gate] + K -->|No| M[Action or Report Agent] + L --> M + M --> N[Audit Agent] + N --> O[Memory Update Agent] ``` -## Run With Docker Compose (Recommended) +Each result includes version, evidence class, source list, confidence level, risk score, reasoning summary, Yudh assessment, Vyuha recommendation, NFU policy status, human approval requirement, final recommendation, and audit event. -This runs the complete DISHA Brain platform: +## Quickstart ```bash -docker compose up --build +python -m pytest tests/test_disha_brain_graph.py ``` -Services: -- Web UI: `http://localhost:3000` -- Brain API: `http://localhost:8080/api/v1/health` - -Optional modules (AI Platform, MCP Server, Quantum Physics, OpenCanary): +Run a demo payload: ```bash -docker compose --profile full up --build +python - <<'PY' +import json +from pathlib import Path +from disha.brain.graph import DishaAgenticGraph, GraphInput + +payload = json.loads(Path("demos/demo_5_6_national_audit.json").read_text()) +result = DishaAgenticGraph().invoke(GraphInput(**payload)) +print(result.model_dump_json(indent=2)) +PY ``` -### 6. Run validation +Run the existing web checks: ```bash cd web @@ -167,89 +77,30 @@ npm run type-check npm run build ``` -## Deployment - -### Docker Compose baseline - -The repository includes [docker/docker-compose.yml](docker/docker-compose.yml) for: - -- `disha-web` -- `postgres` -- `redis` -- `otel-collector` - -Production deployment expects environment-managed secrets for: - -- `DISHA_JWT_SECRET` -- `POSTGRES_PASSWORD` -- `ANTHROPIC_API_KEY` -- OIDC variables when using federated authentication +## Trust Boundaries -### CI +DISHA never treats all text as truth. Inputs are classified as verified, official record, RTI record, open data, sensor signal, audit record, constitutional reference, inference, allegation, contradiction, or unresolved. Unsupported facts stay marked as requiring verification. -GitHub Actions covers: - -- quality gate linting and typing -- module-specific CI -- security scanning -- CodeQL analysis - -## Environment Variables - -Primary web variables are documented in [web/.env.example](web/.env.example). The most important ones are: - -| Variable | Purpose | -| --- | --- | -| `DISHA_AUTH_MODE` | `dev-jwt` or `oidc` | -| `DISHA_JWT_SECRET` | JWT signing key for dev JWT mode | -| `DISHA_DEV_PASSWORD` | bootstrap password for local login | -| `DISHA_OIDC_ISSUER` | OIDC issuer URL | -| `DISHA_OIDC_CLIENT_ID` | OIDC client id | -| `DISHA_OIDC_CLIENT_SECRET` | OIDC client secret | -| `DATABASE_URL` | Postgres connection string | -| `REDIS_URL` | Redis connection string | -| `DISHA_WORKSPACE_ROOT` | allowed filesystem root for web file operations | -| `DISHA_ALLOWED_ORIGINS` | allowed browser origins | - -## Screenshots - -Add screenshots to `docs/images/` and wire them here: - -- `docs/images/dashboard-overview.png` -- `docs/images/auth-flow.png` -- `docs/images/share-export.png` - -Recommended capture set: - -1. Dashboard or command center overview -2. Auth and policy-protected API interactions -3. File, export, and sharing workflows +No-First-Use is enforced in code under `disha/brain/policy/no_first_use.py`. Offensive, retaliatory, unauthorized, or destructive actions are blocked. Ambiguous actions require human approval. ## Documentation -- [Repository Analysis](docs/repository-analysis.md) -- [Technical Design Document](docs/TDD.md) -- [Architecture Diagrams](docs/architecture-diagrams.md) -- [Design System](docs/design-system.md) -- [DISHA Brain Architecture](docs/disha-brain-architecture.md) -- [DISHA Brain Enterprise Architecture](docs/disha-brain-enterprise-architecture.md) -- [Wiki Home](docs/HOME.md) - -## Current State - -What is production-oriented now: - -- `web/` hardening path -- `src/` CLI security and observability adapters -- `disha/brain/` unified brain backend and alerts pipeline -- Compose-based local infrastructure -- CI hardening across Python and TypeScript surfaces - -What remains in transition: - -- legacy surfaces under `backend/` and `disha-agi-brain/` -- uneven documentation quality outside the new docs set - -## License - -See [LICENSE](LICENSE). +- [Architecture](docs/ARCHITECTURE.md) +- [Product Thesis](docs/PRODUCT_THESIS.md) +- [Version Ladder](docs/VERSION_LADDER.md) +- [DISHA Brain](docs/DISHA_BRAIN.md) +- [Agentic Flow](docs/LANGGRAPH_AGENTIC_FLOW.md) +- [Yudh/Vyuha Doctrine](docs/YUDH_VYUHA_DOCTRINE.md) +- [No-First-Use](docs/NO_FIRST_USE.md) +- [Evidence Model](docs/EVIDENCE_MODEL.md) +- [Trust Model](docs/TRUST_MODEL.md) +- [Demos](docs/DEMOS.md) +- [Roadmap](docs/ROADMAP.md) + +## Remaining Gaps + +- External source verification connectors need production credentials, publisher allowlists, and retrieval audits. +- Domain models for HSE, geospatial, governance, and resilience need expert-reviewed datasets. +- Persistent graph memory and audit export need deployment-specific storage guarantees. +- Existing legacy integrations need security review before being promoted into the production spine. +- thenitishkr.in and the two DISHA Intelligence books are referenced by the product brief, but repository-verifiable source material is not present here. Their exact relationship is [VERIFY REQUIRED]. diff --git a/demos/demo_1_6_geospatial.json b/demos/demo_1_6_geospatial.json new file mode 100644 index 00000000..cc6455dc --- /dev/null +++ b/demos/demo_1_6_geospatial.json @@ -0,0 +1,7 @@ +{ + "input_text": "Sensor report with coordinates indicates repeated movement near a restricted public-safety perimeter.", + "source_type": "sensor", + "source_links": [], + "requested_actions": ["report", "preserve_evidence", "alert"], + "authorized_environment": true +} diff --git a/demos/demo_2_6_sustainable_development.json b/demos/demo_2_6_sustainable_development.json new file mode 100644 index 00000000..cc3cd856 --- /dev/null +++ b/demos/demo_2_6_sustainable_development.json @@ -0,0 +1,7 @@ +{ + "input_text": "Open data dataset about flood impact on bridge and road infrastructure requires resilience prioritisation.", + "source_type": "government_open_data", + "source_links": ["https://data.gov.in/"], + "requested_actions": ["report", "preserve_evidence"], + "authorized_environment": false +} diff --git a/demos/demo_3_6_physical_interface.json b/demos/demo_3_6_physical_interface.json new file mode 100644 index 00000000..aba134b7 --- /dev/null +++ b/demos/demo_3_6_physical_interface.json @@ -0,0 +1,7 @@ +{ + "input_text": "Edge device telemetry from an authorized machine state collector needs secure sensor-to-brain routing.", + "source_type": "sensor", + "source_links": [], + "requested_actions": ["monitor", "preserve_evidence"], + "authorized_environment": true +} diff --git a/demos/demo_4_6_hse.json b/demos/demo_4_6_hse.json new file mode 100644 index 00000000..930620ad --- /dev/null +++ b/demos/demo_4_6_hse.json @@ -0,0 +1,7 @@ +{ + "input_text": "District health, social welfare, and school access signals show a possible service gap.", + "source_type": "document", + "source_links": [], + "requested_actions": ["report", "preserve_evidence"], + "authorized_environment": false +} diff --git a/demos/demo_5_6_national_audit.json b/demos/demo_5_6_national_audit.json new file mode 100644 index 00000000..4093656a --- /dev/null +++ b/demos/demo_5_6_national_audit.json @@ -0,0 +1,7 @@ +{ + "input_text": "RTI response and Article 12 public authority material appear to conflict with an open data audit record.", + "source_type": "rti_record", + "source_links": [], + "requested_actions": ["report", "preserve_evidence"], + "authorized_environment": false +} diff --git a/demos/demo_6_6_gap_closure.json b/demos/demo_6_6_gap_closure.json new file mode 100644 index 00000000..91c1a90a --- /dev/null +++ b/demos/demo_6_6_gap_closure.json @@ -0,0 +1,7 @@ +{ + "input_text": "Gap closure request: missing audit trail and delayed corrective action require Yudh assessment and Vyuha selection.", + "source_type": "audit_report", + "source_links": [], + "requested_actions": ["report", "preserve_evidence", "recover"], + "authorized_environment": true +} diff --git a/disha/brain/api/routes.py b/disha/brain/api/routes.py index 6ff09f16..a1dcf71a 100644 --- a/disha/brain/api/routes.py +++ b/disha/brain/api/routes.py @@ -14,6 +14,7 @@ from ..brain.reasoning import ReasoningBrain from ..brain.risk_engine import RiskEngine from ..database.store import SQLiteStore +from ..graph import DishaAgenticGraph, DishaGraphResult, GraphInput from ..models.schemas import ( CommandResponse, DecisionAction, @@ -49,6 +50,7 @@ def __init__(self) -> None: self.anomaly = AnomalyDetector() self.risk_engine = RiskEngine() self.decision_engine = DecisionEngine() + self.graph = DishaAgenticGraph() self.monitoring: MonitoringService | None = None def module_health(self) -> dict[str, str]: @@ -63,6 +65,7 @@ def module_health(self) -> dict[str, str]: "anomaly_detector": "ok" if self.anomaly else "degraded", "risk_engine": "ok" if self.risk_engine else "degraded", "decision_engine": "ok" if self.decision_engine else "degraded", + "agentic_graph": "ok" if self.graph else "degraded", "monitoring": "ok" if self.monitoring else "degraded", } return modules @@ -108,6 +111,17 @@ async def health() -> HealthResponse: ) +@router.post( + "/graph/invoke", + response_model=DishaGraphResult, + dependencies=[Depends(require_api_token)], +) +async def invoke_graph( + payload: GraphInput, app: AppContext = Depends(get_context) +) -> DishaGraphResult: + return app.graph.invoke(payload) + + @router.post( "/internal/audit", dependencies=[Depends(require_api_token)], diff --git a/disha/brain/audit/__init__.py b/disha/brain/audit/__init__.py new file mode 100644 index 00000000..a970eddd --- /dev/null +++ b/disha/brain/audit/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .event_schema import AuditEvent +from .ledger import AuditLedger + +__all__ = ["AuditEvent", "AuditLedger"] + diff --git a/disha/brain/audit/event_schema.py b/disha/brain/audit/event_schema.py new file mode 100644 index 00000000..2e370aeb --- /dev/null +++ b/disha/brain/audit/event_schema.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from pydantic import BaseModel, Field + + +class AuditEvent(BaseModel): + event_id: str + action: str + actor: str = "disha_brain" + version: str | None = None + outcome: str + metadata: dict[str, Any] = Field(default_factory=dict) + created_at: str = Field(default_factory=lambda: datetime.now(UTC).isoformat()) + diff --git a/disha/brain/audit/exporter.py b/disha/brain/audit/exporter.py new file mode 100644 index 00000000..7eba1e74 --- /dev/null +++ b/disha/brain/audit/exporter.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import json + +from .ledger import AuditLedger + + +def export_ledger_json(ledger: AuditLedger) -> str: + return json.dumps(ledger.export(), indent=2, sort_keys=True) + diff --git a/disha/brain/audit/hash_chain.py b/disha/brain/audit/hash_chain.py new file mode 100644 index 00000000..52f7da70 --- /dev/null +++ b/disha/brain/audit/hash_chain.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from hashlib import sha256 + + +def chain_hash(previous_hash: str, payload: str) -> str: + return sha256(f"{previous_hash}:{payload}".encode()).hexdigest() + diff --git a/disha/brain/audit/ledger.py b/disha/brain/audit/ledger.py new file mode 100644 index 00000000..2904db3d --- /dev/null +++ b/disha/brain/audit/ledger.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json + +from .event_schema import AuditEvent +from .hash_chain import chain_hash + + +class AuditLedger: + def __init__(self) -> None: + self._events: list[tuple[AuditEvent, str]] = [] + self._latest_hash = "GENESIS" + + def append(self, event: AuditEvent) -> AuditEvent: + payload = json.dumps(event.model_dump(mode="json"), sort_keys=True) + self._latest_hash = chain_hash(self._latest_hash, payload) + self._events.append((event, self._latest_hash)) + return event + + def export(self) -> list[dict[str, object]]: + return [ + {"event": json.loads(event.model_dump_json()), "hash": event_hash} + for event, event_hash in self._events + ] + + @property + def latest_hash(self) -> str: + return self._latest_hash diff --git a/disha/brain/config.py b/disha/brain/config.py index f4129402..097475b5 100644 --- a/disha/brain/config.py +++ b/disha/brain/config.py @@ -12,7 +12,7 @@ class Settings: "DISHA_BRAIN_APP_NAME", os.getenv("JARVIS_X_APP_NAME", "DISHA Brain") ) api_host: str = os.getenv( - "DISHA_BRAIN_API_HOST", os.getenv("JARVIS_X_API_HOST", "0.0.0.0") + "DISHA_BRAIN_API_HOST", os.getenv("JARVIS_X_API_HOST", "127.0.0.1") ) api_port: int = int( os.getenv("DISHA_BRAIN_API_PORT", os.getenv("JARVIS_X_API_PORT", "8080")) diff --git a/disha/brain/evidence/__init__.py b/disha/brain/evidence/__init__.py new file mode 100644 index 00000000..ed5444ce --- /dev/null +++ b/disha/brain/evidence/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from .classifier import build_evidence_bundle, classify_evidence_text +from .models import ConfidenceLevel, EvidenceBundle, EvidenceClass, EvidenceItem + +__all__ = [ + "ConfidenceLevel", + "EvidenceBundle", + "EvidenceClass", + "EvidenceItem", + "build_evidence_bundle", + "classify_evidence_text", +] diff --git a/disha/brain/evidence/classifier.py b/disha/brain/evidence/classifier.py new file mode 100644 index 00000000..a47e0804 --- /dev/null +++ b/disha/brain/evidence/classifier.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from hashlib import sha256 + +from .models import ConfidenceLevel, EvidenceBundle, EvidenceClass, EvidenceItem + +CLASS_PRIORITY: dict[EvidenceClass, int] = { + EvidenceClass.verified: 100, + EvidenceClass.official_record: 95, + EvidenceClass.rti_record: 90, + EvidenceClass.audit_record: 85, + EvidenceClass.constitutional_reference: 80, + EvidenceClass.open_data: 70, + EvidenceClass.sensor_signal: 65, + EvidenceClass.contradiction: 45, + EvidenceClass.inference: 35, + EvidenceClass.allegation: 25, + EvidenceClass.unresolved: 10, +} + + +def classify_evidence_text(text: str, source_type: str = "operator") -> EvidenceClass: + normalized = f"{source_type} {text}".lower() + if any(token in normalized for token in ("gazette", "official order", "government notification", ".gov", "gov.in")): + return EvidenceClass.official_record + if "rti" in normalized or "right to information" in normalized: + return EvidenceClass.rti_record + if any(token in normalized for token in ("open data", "data.gov", "csv", "dataset")): + return EvidenceClass.open_data + if any(token in normalized for token in ("sensor", "telemetry", "iot", "gps", "coordinate")): + return EvidenceClass.sensor_signal + if any(token in normalized for token in ("audit", "cag", "inspection report")): + return EvidenceClass.audit_record + if any(token in normalized for token in ("constitution", "article ", "schedule ")): + return EvidenceClass.constitutional_reference + if any(token in normalized for token in ("contradicts", "conflicts with", "mismatch")): + return EvidenceClass.contradiction + if any(token in normalized for token in ("alleges", "allegation", "claimed", "unverified")): + return EvidenceClass.allegation + if any(token in normalized for token in ("infer", "likely", "may indicate", "pattern suggests")): + return EvidenceClass.inference + return EvidenceClass.unresolved + + +def confidence_for(evidence_class: EvidenceClass) -> ConfidenceLevel: + if CLASS_PRIORITY[evidence_class] >= 85: + return ConfidenceLevel.high + if CLASS_PRIORITY[evidence_class] >= 55: + return ConfidenceLevel.medium + return ConfidenceLevel.low + + +def build_evidence_bundle( + texts: list[str], + source_type: str = "operator", + source_links: list[str] | None = None, +) -> EvidenceBundle: + links = source_links or [] + items: list[EvidenceItem] = [] + for index, text in enumerate(texts): + evidence_class = classify_evidence_text(text, source_type) + items.append( + EvidenceItem( + id=sha256(f"{source_type}:{index}:{text}".encode()).hexdigest()[:16], + text=text, + evidence_class=evidence_class, + source_type=source_type, + source_link=links[index] if index < len(links) else None, + confidence=confidence_for(evidence_class), + ) + ) + dominant = max( + (item.evidence_class for item in items), + key=lambda cls: CLASS_PRIORITY[cls], + default=EvidenceClass.unresolved, + ) + return EvidenceBundle( + items=items, + dominant_class=dominant, + source_links=[link for link in links if link], + confidence=confidence_for(dominant), + ) + diff --git a/disha/brain/evidence/evidence_bundle.py b/disha/brain/evidence/evidence_bundle.py new file mode 100644 index 00000000..e50e0bae --- /dev/null +++ b/disha/brain/evidence/evidence_bundle.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .classifier import build_evidence_bundle +from .models import EvidenceBundle, EvidenceClass, EvidenceItem + +__all__ = ["EvidenceBundle", "EvidenceClass", "EvidenceItem", "build_evidence_bundle"] + diff --git a/disha/brain/evidence/models.py b/disha/brain/evidence/models.py new file mode 100644 index 00000000..5daf02c6 --- /dev/null +++ b/disha/brain/evidence/models.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field + + +class EvidenceClass(str, Enum): + verified = "verified" + official_record = "official_record" + rti_record = "rti_record" + open_data = "open_data" + sensor_signal = "sensor_signal" + audit_record = "audit_record" + constitutional_reference = "constitutional_reference" + inference = "inference" + allegation = "allegation" + contradiction = "contradiction" + unresolved = "unresolved" + + +class ConfidenceLevel(str, Enum): + low = "low" + medium = "medium" + high = "high" + + +class EvidenceItem(BaseModel): + id: str + text: str + evidence_class: EvidenceClass + source_type: str = "operator" + source_link: str | None = None + confidence: ConfidenceLevel = ConfidenceLevel.low + metadata: dict[str, Any] = Field(default_factory=dict) + + +class EvidenceBundle(BaseModel): + items: list[EvidenceItem] = Field(default_factory=list) + dominant_class: EvidenceClass = EvidenceClass.unresolved + source_links: list[str] = Field(default_factory=list) + confidence: ConfidenceLevel = ConfidenceLevel.low + diff --git a/disha/brain/evidence/provenance.py b/disha/brain/evidence/provenance.py new file mode 100644 index 00000000..c8b152ec --- /dev/null +++ b/disha/brain/evidence/provenance.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class SourceProvenance(BaseModel): + source_id: str + source_type: str + title: str + link: str | None = None + retrieved_at: str | None = None + verification_note: str = "[VERIFY REQUIRED]" + + +def verification_note(link: str | None, source_type: str) -> str: + if link and source_type in {"official_record", "rti_record", "open_data"}: + return "Source-linked; verify against the issuing authority before action." + return "[VERIFY REQUIRED]" + diff --git a/disha/brain/evidence/source_registry.py b/disha/brain/evidence/source_registry.py new file mode 100644 index 00000000..8b6031e4 --- /dev/null +++ b/disha/brain/evidence/source_registry.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from .provenance import SourceProvenance, verification_note + + +class SourceRegistry: + def __init__(self) -> None: + self._sources: dict[str, SourceProvenance] = {} + + def register( + self, source_id: str, source_type: str, title: str, link: str | None = None + ) -> SourceProvenance: + source = SourceProvenance( + source_id=source_id, + source_type=source_type, + title=title, + link=link, + verification_note=verification_note(link, source_type), + ) + self._sources[source_id] = source + return source + + def list_sources(self) -> list[SourceProvenance]: + return list(self._sources.values()) + diff --git a/disha/brain/geospatial/__init__.py b/disha/brain/geospatial/__init__.py new file mode 100644 index 00000000..4a7b8586 --- /dev/null +++ b/disha/brain/geospatial/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .coordinates import Coordinates +from .risk_map import public_safety_risk +from .tracker import TrackedObject + +__all__ = ["Coordinates", "TrackedObject", "public_safety_risk"] + diff --git a/disha/brain/geospatial/coordinates.py b/disha/brain/geospatial/coordinates.py new file mode 100644 index 00000000..f958762e --- /dev/null +++ b/disha/brain/geospatial/coordinates.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field, field_validator + + +class Coordinates(BaseModel): + latitude: float = Field(ge=-90.0, le=90.0) + longitude: float = Field(ge=-180.0, le=180.0) + accuracy_m: float | None = Field(default=None, ge=0.0) + + @field_validator("latitude", "longitude") + @classmethod + def finite_coordinate(cls, value: float) -> float: + if value != value: + raise ValueError("coordinate must be finite") + return value + diff --git a/disha/brain/geospatial/risk_map.py b/disha/brain/geospatial/risk_map.py new file mode 100644 index 00000000..b3c8ac88 --- /dev/null +++ b/disha/brain/geospatial/risk_map.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from .coordinates import Coordinates + + +def public_safety_risk(point: Coordinates, signal_count: int) -> float: + score = min(signal_count * 0.2, 0.8) + if point.accuracy_m is not None and point.accuracy_m <= 25: + score += 0.1 + return min(score, 1.0) + diff --git a/disha/brain/geospatial/spatial_index.py b/disha/brain/geospatial/spatial_index.py new file mode 100644 index 00000000..2815871b --- /dev/null +++ b/disha/brain/geospatial/spatial_index.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from .coordinates import Coordinates + + +class SpatialIndex: + def __init__(self) -> None: + self._points: list[Coordinates] = [] + + def add(self, point: Coordinates) -> None: + self._points.append(point) + + def all_points(self) -> list[Coordinates]: + return list(self._points) + diff --git a/disha/brain/geospatial/tracker.py b/disha/brain/geospatial/tracker.py new file mode 100644 index 00000000..8947552e --- /dev/null +++ b/disha/brain/geospatial/tracker.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field + +from .coordinates import Coordinates + + +class TrackedObject(BaseModel): + object_id: str + observations: list[Coordinates] = Field(default_factory=list) + + def add(self, coordinates: Coordinates) -> None: + self.observations.append(coordinates) + + def status(self) -> str: + if len(self.observations) >= 3: + return "pattern_observable" + if self.observations: + return "single_or_sparse_signal" + return "no_signal" + diff --git a/disha/brain/governance/__init__.py b/disha/brain/governance/__init__.py new file mode 100644 index 00000000..f087ce71 --- /dev/null +++ b/disha/brain/governance/__init__.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from .constitutional_mapper import map_constitutional_context +from .nyaya import nyaya_summary +from .open_data_audit import audit_open_data +from .rti_parser import parse_rti_signal + +__all__ = [ + "audit_open_data", + "map_constitutional_context", + "nyaya_summary", + "parse_rti_signal", +] + diff --git a/disha/brain/governance/constitutional_mapper.py b/disha/brain/governance/constitutional_mapper.py new file mode 100644 index 00000000..4b09ad13 --- /dev/null +++ b/disha/brain/governance/constitutional_mapper.py @@ -0,0 +1,14 @@ +from __future__ import annotations + + +def map_constitutional_context(text: str) -> list[str]: + normalized = text.lower() + references: list[str] = [] + if "article 12" in normalized: + references.append("Article 12: State definition [VERIFY REQUIRED]") + if "article 21" in normalized: + references.append("Article 21: life and personal liberty [VERIFY REQUIRED]") + if "constitution" in normalized and not references: + references.append("Constitutional reference present; exact provision [VERIFY REQUIRED]") + return references + diff --git a/disha/brain/governance/nyaya.py b/disha/brain/governance/nyaya.py new file mode 100644 index 00000000..cd2f6f67 --- /dev/null +++ b/disha/brain/governance/nyaya.py @@ -0,0 +1,8 @@ +from __future__ import annotations + + +def nyaya_summary(evidence_class: str, contradiction_count: int) -> str: + if contradiction_count: + return "NYAYA review: contradictions require source-by-source public accountability review." + return f"NYAYA review: proceed with {evidence_class} evidence and explicit verification notes." + diff --git a/disha/brain/governance/open_data_audit.py b/disha/brain/governance/open_data_audit.py new file mode 100644 index 00000000..532f3ead --- /dev/null +++ b/disha/brain/governance/open_data_audit.py @@ -0,0 +1,10 @@ +from __future__ import annotations + + +def audit_open_data(text: str, source_links: list[str]) -> dict[str, object]: + return { + "open_data_signal": "open data" in text.lower() or bool(source_links), + "source_count": len(source_links), + "verification": "Every dataset must be checked against publisher metadata.", + } + diff --git a/disha/brain/governance/rti_parser.py b/disha/brain/governance/rti_parser.py new file mode 100644 index 00000000..e4ae7dcb --- /dev/null +++ b/disha/brain/governance/rti_parser.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +def parse_rti_signal(text: str) -> dict[str, str | bool]: + return { + "mentions_rti": "rti" in text.lower() or "right to information" in text.lower(), + "status": "source-linked review required", + } + diff --git a/disha/brain/graph/__init__.py b/disha/brain/graph/__init__.py new file mode 100644 index 00000000..0dd91dba --- /dev/null +++ b/disha/brain/graph/__init__.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from .graph import DishaAgenticGraph, run_disha_graph +from .state import DishaGraphResult, DishaGraphState, GraphInput + +__all__ = [ + "DishaAgenticGraph", + "DishaGraphResult", + "DishaGraphState", + "GraphInput", + "run_disha_graph", +] diff --git a/disha/brain/graph/graph.py b/disha/brain/graph/graph.py new file mode 100644 index 00000000..2400fe0c --- /dev/null +++ b/disha/brain/graph/graph.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from ..audit import AuditLedger +from ..memory import MemoryStore +from .nodes import ( + action_agent, + audit_agent, + context_agent, + evidence_agent, + human_approval_agent, + intake_agent, + memory_update_agent, + policy_guard_agent, + reasoning_agent, + vyuha_agent, + yudh_agent, +) +from .router import route_version +from .state import DishaGraphResult, GraphInput + + +class DishaAgenticGraph: + def __init__( + self, ledger: AuditLedger | None = None, memory: MemoryStore | None = None + ) -> None: + self.ledger = ledger or AuditLedger() + self.memory = memory or MemoryStore() + + def invoke(self, payload: GraphInput) -> DishaGraphResult: + state = intake_agent.run(payload) + state = evidence_agent.run(state) + state = context_agent.run(state) + state = reasoning_agent.run(state) + state.version = route_version(state.input_text, state.source_type) + state = action_agent.run(state) + state = yudh_agent.run(state) + state = vyuha_agent.run(state) + state = policy_guard_agent.run(state) + state = human_approval_agent.run(state) + state = audit_agent.run(state, self.ledger) + state = memory_update_agent.run(state, self.memory) + if state.audit_record is None: + raise RuntimeError("audit event was not created") + return DishaGraphResult( + version=state.version or "unknown", + evidence_class=state.evidence_class, + source_list=state.source_links, + confidence_level=state.confidence_level, + risk_score=state.risk_score, + reasoning_summary=state.risk_reason, + yudh_assessment=state.yudh_assessment, + vyuha_recommendation=state.vyuha_recommendation, + nfu_policy_status=state.nfu_status, + human_approval_required=state.human_approval_required, + final_recommendation=state.final_answer, + audit_event=state.audit_record, + ) + + +def run_disha_graph(payload: GraphInput) -> DishaGraphResult: + return DishaAgenticGraph().invoke(payload) + diff --git a/disha/brain/graph/nodes/__init__.py b/disha/brain/graph/nodes/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/disha/brain/graph/nodes/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/disha/brain/graph/nodes/action_agent.py b/disha/brain/graph/nodes/action_agent.py new file mode 100644 index 00000000..4cfd9eb6 --- /dev/null +++ b/disha/brain/graph/nodes/action_agent.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from ...versions import ( + v1_6_geospatial_detection, + v2_6_sustainable_development, + v3_6_physical_interface, + v4_6_hse, + v5_6_national_audit, + v6_6_gap_closure, +) +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + handlers = { + "1.6": v1_6_geospatial_detection.run, + "2.6": v2_6_sustainable_development.run, + "3.6": v3_6_physical_interface.run, + "4.6": v4_6_hse.run, + "5.6": v5_6_national_audit.run, + "6.6": v6_6_gap_closure.run, + } + output = handlers[state.version or "5.6"](state.input_text) + state.sector_context = {"version_title": output.title, "signals": output.signals} + state.final_answer = output.recommendation + state.metadata["version_notes"] = output.notes + return state + diff --git a/disha/brain/graph/nodes/audit_agent.py b/disha/brain/graph/nodes/audit_agent.py new file mode 100644 index 00000000..4a5f4843 --- /dev/null +++ b/disha/brain/graph/nodes/audit_agent.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from hashlib import sha256 + +from ...audit import AuditEvent, AuditLedger +from ..state import DishaGraphState + + +def run(state: DishaGraphState, ledger: AuditLedger) -> DishaGraphState: + event = AuditEvent( + event_id=sha256(f"{state.input_text}:{state.version}".encode()).hexdigest()[:16], + action="disha_graph_run", + version=state.version, + outcome=state.nfu_status, + metadata={ + "evidence_class": state.evidence_class.value, + "risk_score": state.risk_score, + "human_approval_required": state.human_approval_required, + "source_count": len(state.source_links), + }, + ) + state.audit_record = ledger.append(event) + state.metadata["audit_hash"] = ledger.latest_hash + return state + diff --git a/disha/brain/graph/nodes/context_agent.py b/disha/brain/graph/nodes/context_agent.py new file mode 100644 index 00000000..8f14a195 --- /dev/null +++ b/disha/brain/graph/nodes/context_agent.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from ...governance import map_constitutional_context +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + state.constitutional_context = map_constitutional_context(state.input_text) + if state.source_type in {"sensor", "cyber_telemetry"}: + state.cyber_context = {"source_type": state.source_type, "scope": "authorized review required"} + return state + diff --git a/disha/brain/graph/nodes/evidence_agent.py b/disha/brain/graph/nodes/evidence_agent.py new file mode 100644 index 00000000..ef17eb56 --- /dev/null +++ b/disha/brain/graph/nodes/evidence_agent.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from ...evidence import build_evidence_bundle +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + bundle = build_evidence_bundle( + [state.input_text], state.source_type, state.source_links + ) + state.evidence_items = bundle.items + state.evidence_class = bundle.dominant_class + state.confidence_level = bundle.confidence + return state + diff --git a/disha/brain/graph/nodes/human_approval_agent.py b/disha/brain/graph/nodes/human_approval_agent.py new file mode 100644 index 00000000..b6cad9c2 --- /dev/null +++ b/disha/brain/graph/nodes/human_approval_agent.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from ...policy import approval_gate +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + approval = approval_gate( + state.human_approval_required, + "Risk or NFU status requires an authorized human decision." + if state.human_approval_required + else "No elevated approval threshold reached.", + ) + state.metadata["human_approval"] = approval.model_dump() + return state + diff --git a/disha/brain/graph/nodes/intake_agent.py b/disha/brain/graph/nodes/intake_agent.py new file mode 100644 index 00000000..af960e00 --- /dev/null +++ b/disha/brain/graph/nodes/intake_agent.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from ..state import DishaGraphState, GraphInput + + +def run(payload: GraphInput) -> DishaGraphState: + return DishaGraphState( + input_text=payload.input_text.strip(), + source_type=payload.source_type, + source_links=payload.source_links, + requested_actions=payload.requested_actions, + authorized_environment=payload.authorized_environment, + metadata=payload.metadata, + ) + diff --git a/disha/brain/graph/nodes/memory_update_agent.py b/disha/brain/graph/nodes/memory_update_agent.py new file mode 100644 index 00000000..fc648cbf --- /dev/null +++ b/disha/brain/graph/nodes/memory_update_agent.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from ...memory import MemoryStore +from ..state import DishaGraphState + + +def run(state: DishaGraphState, memory: MemoryStore) -> DishaGraphState: + memory.update_from_result(state.input_text, state.version or "unknown", state.final_answer) + state.memory_context = memory.working.context + return state + diff --git a/disha/brain/graph/nodes/policy_guard_agent.py b/disha/brain/graph/nodes/policy_guard_agent.py new file mode 100644 index 00000000..2257eef5 --- /dev/null +++ b/disha/brain/graph/nodes/policy_guard_agent.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from ...policy import evaluate_actions, requires_approval +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + result = evaluate_actions(state.requested_actions, state.authorized_environment) + state.nfu_status = result.status + state.human_approval_required = requires_approval(state.risk_score, result.status) + if result.decision.value == "forbidden": + state.human_approval_required = True + return state + diff --git a/disha/brain/graph/nodes/reasoning_agent.py b/disha/brain/graph/nodes/reasoning_agent.py new file mode 100644 index 00000000..e0fda453 --- /dev/null +++ b/disha/brain/graph/nodes/reasoning_agent.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from ...yudh import threat_score +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + state.risk_score = threat_score(state.input_text, state.evidence_class.value) + state.risk_reason = "Risk derived from evidence class and public-interest threat indicators." + return state + diff --git a/disha/brain/graph/nodes/vyuha_agent.py b/disha/brain/graph/nodes/vyuha_agent.py new file mode 100644 index 00000000..94242dd7 --- /dev/null +++ b/disha/brain/graph/nodes/vyuha_agent.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from ...vyuha import build_recommendation, select_vyuha +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + signals = [state.version or "", state.source_type, state.evidence_class.value, state.input_text] + formation = select_vyuha(signals, state.risk_score) + state.vyuha_recommendation = build_recommendation(formation).model_dump() + return state + diff --git a/disha/brain/graph/nodes/yudh_agent.py b/disha/brain/graph/nodes/yudh_agent.py new file mode 100644 index 00000000..e738fd44 --- /dev/null +++ b/disha/brain/graph/nodes/yudh_agent.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from ...yudh import assess_yudh +from ..state import DishaGraphState + + +def run(state: DishaGraphState) -> DishaGraphState: + state.yudh_assessment = assess_yudh( + state.risk_score, state.evidence_class.value + ).model_dump() + return state + diff --git a/disha/brain/graph/router.py b/disha/brain/graph/router.py new file mode 100644 index 00000000..711cac1a --- /dev/null +++ b/disha/brain/graph/router.py @@ -0,0 +1,19 @@ +from __future__ import annotations + + +def route_version(input_text: str, source_type: str = "operator") -> str: + normalized = f"{source_type} {input_text}".lower() + text_only = input_text.lower() + if any(token in normalized for token in ("climate", "infrastructure", "resource", "bridge", "flood", "development")): + return "2.6" + if any(token in text_only for token in ("edge", "device", "iot", "physical interface", "machine state")): + return "3.6" + if any(token in normalized for token in ("health", "welfare", "education", "school", "district service")): + return "4.6" + if any(token in text_only for token in ("gap", "mitigation", "corrective", "vyuha", "yudh")): + return "6.6" + if any(token in normalized for token in ("coordinate", "geospatial", "gps", "sensor", "object tracking")): + return "1.6" + if any(token in normalized for token in ("rti", "constitution", "article", "open data", "audit", "public authority")): + return "5.6" + return "5.6" diff --git a/disha/brain/graph/state.py b/disha/brain/graph/state.py new file mode 100644 index 00000000..f67cbb42 --- /dev/null +++ b/disha/brain/graph/state.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + +from ..audit.event_schema import AuditEvent +from ..evidence.models import ConfidenceLevel, EvidenceClass, EvidenceItem + +SourceType = Literal[ + "operator", + "sensor", + "geospatial", + "government_open_data", + "rti_record", + "audit_report", + "document", + "cyber_telemetry", +] + + +class GraphInput(BaseModel): + input_text: str + source_type: SourceType = "operator" + source_links: list[str] = Field(default_factory=list) + requested_actions: list[str] = Field(default_factory=lambda: ["report", "preserve_evidence"]) + authorized_environment: bool = False + metadata: dict[str, Any] = Field(default_factory=dict) + + +class DishaGraphState(BaseModel): + input_text: str + source_type: SourceType = "operator" + version: str | None = None + evidence_items: list[EvidenceItem] = Field(default_factory=list) + evidence_class: EvidenceClass = EvidenceClass.unresolved + source_links: list[str] = Field(default_factory=list) + confidence_level: ConfidenceLevel = ConfidenceLevel.low + geo_context: dict[str, Any] = Field(default_factory=dict) + sector_context: dict[str, Any] = Field(default_factory=dict) + constitutional_context: list[str] = Field(default_factory=list) + cyber_context: dict[str, Any] = Field(default_factory=dict) + memory_context: dict[str, Any] = Field(default_factory=dict) + risk_score: float = 0.0 + risk_reason: str = "" + yudh_assessment: dict[str, Any] = Field(default_factory=dict) + vyuha_recommendation: dict[str, Any] = Field(default_factory=dict) + nfu_status: str = "not_evaluated" + human_approval_required: bool = False + final_answer: str = "" + audit_record: AuditEvent | None = None + requested_actions: list[str] = Field(default_factory=list) + authorized_environment: bool = False + metadata: dict[str, Any] = Field(default_factory=dict) + + +class DishaGraphResult(BaseModel): + version: str + evidence_class: EvidenceClass + source_list: list[str] + confidence_level: ConfidenceLevel + risk_score: float + reasoning_summary: str + yudh_assessment: dict[str, Any] + vyuha_recommendation: dict[str, Any] + nfu_policy_status: str + human_approval_required: bool + final_recommendation: str + audit_event: AuditEvent + diff --git a/disha/brain/memory/__init__.py b/disha/brain/memory/__init__.py new file mode 100644 index 00000000..8411efed --- /dev/null +++ b/disha/brain/memory/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .memory_store import MemoryStore + +__all__ = ["MemoryStore"] diff --git a/disha/brain/memory/episodic.py b/disha/brain/memory/episodic.py new file mode 100644 index 00000000..abb1d26f --- /dev/null +++ b/disha/brain/memory/episodic.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class EpisodicMemoryItem(BaseModel): + input_text: str + version: str + final_answer: str + + +class EpisodicMemory: + def __init__(self) -> None: + self.items: list[EpisodicMemoryItem] = [] + + def add(self, item: EpisodicMemoryItem) -> None: + self.items.append(item) + diff --git a/disha/brain/memory/memory_store.py b/disha/brain/memory/memory_store.py new file mode 100644 index 00000000..637646eb --- /dev/null +++ b/disha/brain/memory/memory_store.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from .episodic import EpisodicMemory, EpisodicMemoryItem +from .semantic import SemanticMemory +from .working import WorkingMemory + + +class MemoryStore: + def __init__(self) -> None: + self.working = WorkingMemory() + self.episodic = EpisodicMemory() + self.semantic = SemanticMemory() + + def update_from_result(self, input_text: str, version: str, final_answer: str) -> None: + self.working.update({"last_version": version, "last_answer": final_answer}) + self.episodic.add( + EpisodicMemoryItem( + input_text=input_text, version=version, final_answer=final_answer + ) + ) + self.semantic.observe([version, *input_text.split()[:8]]) + diff --git a/disha/brain/memory/semantic.py b/disha/brain/memory/semantic.py new file mode 100644 index 00000000..7827e1ef --- /dev/null +++ b/disha/brain/memory/semantic.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +class SemanticMemory: + def __init__(self) -> None: + self.entities: dict[str, int] = {} + + def observe(self, terms: list[str]) -> None: + for term in terms: + normalized = term.strip().lower() + if normalized: + self.entities[normalized] = self.entities.get(normalized, 0) + 1 + diff --git a/disha/brain/memory/working.py b/disha/brain/memory/working.py new file mode 100644 index 00000000..da0b4098 --- /dev/null +++ b/disha/brain/memory/working.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import Any + + +class WorkingMemory: + def __init__(self) -> None: + self.context: dict[str, Any] = {} + + def update(self, values: dict[str, Any]) -> None: + self.context.update(values) + diff --git a/disha/brain/policy/__init__.py b/disha/brain/policy/__init__.py new file mode 100644 index 00000000..e14926ed --- /dev/null +++ b/disha/brain/policy/__init__.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from .human_approval import HumanApproval, approval_gate +from .no_first_use import NFUDecision, NFUResult, evaluate_actions +from .permissions import PermissionContext +from .risk_policy import requires_approval, risk_level + +__all__ = [ + "HumanApproval", + "NFUDecision", + "NFUResult", + "PermissionContext", + "approval_gate", + "evaluate_actions", + "requires_approval", + "risk_level", +] + diff --git a/disha/brain/policy/human_approval.py b/disha/brain/policy/human_approval.py new file mode 100644 index 00000000..35daafdb --- /dev/null +++ b/disha/brain/policy/human_approval.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class HumanApproval(BaseModel): + required: bool + reason: str + approver_role: str = "authorized_human_operator" + + +def approval_gate(required: bool, reason: str) -> HumanApproval: + return HumanApproval(required=required, reason=reason) + diff --git a/disha/brain/policy/no_first_use.py b/disha/brain/policy/no_first_use.py new file mode 100644 index 00000000..665b4a2b --- /dev/null +++ b/disha/brain/policy/no_first_use.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel, Field + + +class NFUDecision(str, Enum): + allowed = "allowed" + forbidden = "forbidden" + requires_human_approval = "requires_human_approval" + + +ALLOWED_ACTIONS = { + "monitor", + "block", + "rate_limit", + "contain", + "quarantine", + "terminate_local_process", + "revoke_session", + "rotate_key", + "preserve_evidence", + "report", + "alert", + "recover", + "owned_honeypot", +} + +FORBIDDEN_ACTIONS = { + "hack_back", + "exploit_attacker_system", + "credential_theft", + "deploy_malware", + "ddos", + "destructive_retaliation", + "unauthorized_scan", + "attack_third_party", + "unauthorized_access", + "brute_force", + "auth_bypass", + "self_propagate", +} + + +class NFUResult(BaseModel): + decision: NFUDecision + status: str + allowed_actions: list[str] = Field(default_factory=list) + forbidden_actions: list[str] = Field(default_factory=list) + reason: str + + +def evaluate_actions(actions: list[str], authorized_environment: bool = False) -> NFUResult: + normalized = {action.strip().lower().replace(" ", "_") for action in actions} + forbidden = sorted(normalized & FORBIDDEN_ACTIONS) + unknown = sorted(normalized - ALLOWED_ACTIONS - FORBIDDEN_ACTIONS) + allowed = sorted(normalized & ALLOWED_ACTIONS) + + if forbidden: + return NFUResult( + decision=NFUDecision.forbidden, + status="blocked_by_no_first_use", + allowed_actions=allowed, + forbidden_actions=forbidden, + reason="NFU forbids offensive, retaliatory, unauthorized, or destructive action.", + ) + if "owned_honeypot" in normalized and not authorized_environment: + return NFUResult( + decision=NFUDecision.forbidden, + status="blocked_unowned_honeypot", + allowed_actions=allowed, + reason="Honeypots are allowed only in owned or authorized environments.", + ) + if unknown: + return NFUResult( + decision=NFUDecision.requires_human_approval, + status="ambiguous_action_requires_approval", + allowed_actions=allowed, + forbidden_actions=unknown, + reason="Ambiguous actions are escalated before execution.", + ) + return NFUResult( + decision=NFUDecision.allowed, + status="nfu_compliant", + allowed_actions=allowed, + reason="All requested actions are defensive and evidence-preserving.", + ) + diff --git a/disha/brain/policy/permissions.py b/disha/brain/policy/permissions.py new file mode 100644 index 00000000..602a9c1f --- /dev/null +++ b/disha/brain/policy/permissions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class PermissionContext(BaseModel): + actor: str = "operator" + authorized_environment: bool = False + can_execute_actions: bool = False + can_view_sensitive_evidence: bool = False + + +def can_execute(context: PermissionContext) -> bool: + return context.authorized_environment and context.can_execute_actions + diff --git a/disha/brain/policy/risk_policy.py b/disha/brain/policy/risk_policy.py new file mode 100644 index 00000000..83e0d353 --- /dev/null +++ b/disha/brain/policy/risk_policy.py @@ -0,0 +1,14 @@ +from __future__ import annotations + + +def risk_level(score: float) -> str: + if score >= 0.75: + return "high" + if score >= 0.4: + return "medium" + return "low" + + +def requires_approval(score: float, nfu_status: str) -> bool: + return score >= 0.4 or nfu_status != "nfu_compliant" + diff --git a/disha/brain/sectors/__init__.py b/disha/brain/sectors/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/disha/brain/sectors/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/disha/brain/sectors/development/__init__.py b/disha/brain/sectors/development/__init__.py new file mode 100644 index 00000000..9911bc63 --- /dev/null +++ b/disha/brain/sectors/development/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .resilience_index import resilience_score +from .setu import setu_priority +from .varuna import varuna_signal + +__all__ = ["resilience_score", "setu_priority", "varuna_signal"] + diff --git a/disha/brain/sectors/development/resilience_index.py b/disha/brain/sectors/development/resilience_index.py new file mode 100644 index 00000000..f186157d --- /dev/null +++ b/disha/brain/sectors/development/resilience_index.py @@ -0,0 +1,11 @@ +from __future__ import annotations + + +def resilience_score(signals: list[str]) -> float: + base = 0.45 + if "public_asset_priority" in signals: + base += 0.15 + if "water_climate_resilience_signal" in signals: + base += 0.2 + return min(base, 1.0) + diff --git a/disha/brain/sectors/development/setu.py b/disha/brain/sectors/development/setu.py new file mode 100644 index 00000000..5f968827 --- /dev/null +++ b/disha/brain/sectors/development/setu.py @@ -0,0 +1,8 @@ +from __future__ import annotations + + +def setu_priority(text: str) -> str: + if any(token in text.lower() for token in ("bridge", "road", "asset", "infrastructure")): + return "public_asset_priority" + return "general_development_priority" + diff --git a/disha/brain/sectors/development/varuna.py b/disha/brain/sectors/development/varuna.py new file mode 100644 index 00000000..bb4cbdc5 --- /dev/null +++ b/disha/brain/sectors/development/varuna.py @@ -0,0 +1,8 @@ +from __future__ import annotations + + +def varuna_signal(text: str) -> str: + if any(token in text.lower() for token in ("flood", "water", "rain", "climate")): + return "water_climate_resilience_signal" + return "no_climate_signal" + diff --git a/disha/brain/sectors/hse/__init__.py b/disha/brain/sectors/hse/__init__.py new file mode 100644 index 00000000..f91fcd5f --- /dev/null +++ b/disha/brain/sectors/hse/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .education import education_signal +from .health import health_signal +from .social_welfare import welfare_signal + +__all__ = ["education_signal", "health_signal", "welfare_signal"] + diff --git a/disha/brain/sectors/hse/education.py b/disha/brain/sectors/hse/education.py new file mode 100644 index 00000000..c58ac830 --- /dev/null +++ b/disha/brain/sectors/hse/education.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +def education_signal(text: str) -> str: + return "education_access_gap" if "education" in text.lower() or "school" in text.lower() else "not_applicable" + diff --git a/disha/brain/sectors/hse/health.py b/disha/brain/sectors/hse/health.py new file mode 100644 index 00000000..7ca88857 --- /dev/null +++ b/disha/brain/sectors/hse/health.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +def health_signal(text: str) -> str: + return "health_service_gap" if "health" in text.lower() else "not_applicable" + diff --git a/disha/brain/sectors/hse/social_welfare.py b/disha/brain/sectors/hse/social_welfare.py new file mode 100644 index 00000000..fed17164 --- /dev/null +++ b/disha/brain/sectors/hse/social_welfare.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +def welfare_signal(text: str) -> str: + return "social_welfare_gap" if "welfare" in text.lower() else "not_applicable" + diff --git a/disha/brain/versions/__init__.py b/disha/brain/versions/__init__.py new file mode 100644 index 00000000..1dcf2afc --- /dev/null +++ b/disha/brain/versions/__init__.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from . import ( + v1_6_geospatial_detection, + v2_6_sustainable_development, + v3_6_physical_interface, + v4_6_hse, + v5_6_national_audit, + v6_6_gap_closure, +) +from .common import VersionOutput + +__all__ = [ + "VersionOutput", + "v1_6_geospatial_detection", + "v2_6_sustainable_development", + "v3_6_physical_interface", + "v4_6_hse", + "v5_6_national_audit", + "v6_6_gap_closure", +] diff --git a/disha/brain/versions/common.py b/disha/brain/versions/common.py new file mode 100644 index 00000000..e7d1cf51 --- /dev/null +++ b/disha/brain/versions/common.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class VersionOutput(BaseModel): + version: str + title: str + signals: list[str] = Field(default_factory=list) + recommendation: str + notes: list[str] = Field(default_factory=list) + + +def verification_note() -> str: + return "Operational claims require source review; unsupported facts remain [VERIFY REQUIRED]." + diff --git a/disha/brain/versions/v1_6_geospatial_detection.py b/disha/brain/versions/v1_6_geospatial_detection.py new file mode 100644 index 00000000..b9458928 --- /dev/null +++ b/disha/brain/versions/v1_6_geospatial_detection.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from ..geospatial import Coordinates, TrackedObject, public_safety_risk +from .common import VersionOutput, verification_note + + +def run(input_text: str, coordinates: Coordinates | None = None) -> VersionOutput: + tracker = TrackedObject(object_id="observed-signal") + if coordinates: + tracker.add(coordinates) + risk = public_safety_risk(coordinates, len(tracker.observations)) if coordinates else 0.2 + return VersionOutput( + version="1.6", + title="Geospatial Detection Intelligence", + signals=["geospatial", "sensor_signal", tracker.status()], + recommendation=( + "Create a public-safety evidence bundle and route suspicious patterns to authorized review. " + "Do not generate weaponization instructions." + ), + notes=[f"public_safety_risk={risk:.2f}", verification_note()], + ) + diff --git a/disha/brain/versions/v2_6_sustainable_development.py b/disha/brain/versions/v2_6_sustainable_development.py new file mode 100644 index 00000000..5713f61e --- /dev/null +++ b/disha/brain/versions/v2_6_sustainable_development.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from ..sectors.development import resilience_score, setu_priority, varuna_signal +from .common import VersionOutput, verification_note + + +def run(input_text: str) -> VersionOutput: + signals = [setu_priority(input_text), varuna_signal(input_text)] + return VersionOutput( + version="2.6", + title="Sustainable Development Geospatial Intelligence", + signals=["development", "resilience", *signals], + recommendation="Prioritize public assets by verifiable infrastructure, climate, and resource signals.", + notes=[f"resilience_score={resilience_score(signals):.2f}", verification_note()], + ) + diff --git a/disha/brain/versions/v3_6_physical_interface.py b/disha/brain/versions/v3_6_physical_interface.py new file mode 100644 index 00000000..a97eee5a --- /dev/null +++ b/disha/brain/versions/v3_6_physical_interface.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from .common import VersionOutput, verification_note + + +def run(input_text: str) -> VersionOutput: + return VersionOutput( + version="3.6", + title="Physical Interface Architecture", + signals=["physical_interface", "edge_telemetry", "operator_control"], + recommendation="Accept edge telemetry only from trusted devices, preserve local operator control, and keep sensor-to-brain flow auditable.", + notes=[verification_note()], + ) + diff --git a/disha/brain/versions/v4_6_hse.py b/disha/brain/versions/v4_6_hse.py new file mode 100644 index 00000000..2e54c9f8 --- /dev/null +++ b/disha/brain/versions/v4_6_hse.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from ..sectors.hse import education_signal, health_signal, welfare_signal +from .common import VersionOutput, verification_note + + +def run(input_text: str) -> VersionOutput: + signals = [health_signal(input_text), welfare_signal(input_text), education_signal(input_text)] + active = [signal for signal in signals if signal != "not_applicable"] + return VersionOutput( + version="4.6", + title="HSE Intelligence", + signals=["hse", "service_gap", *active], + recommendation="Produce a district-level service gap report with privacy protection and source-linked evidence.", + notes=[verification_note()], + ) + diff --git a/disha/brain/versions/v5_6_national_audit.py b/disha/brain/versions/v5_6_national_audit.py new file mode 100644 index 00000000..51651b29 --- /dev/null +++ b/disha/brain/versions/v5_6_national_audit.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from ..governance import ( + audit_open_data, + map_constitutional_context, + nyaya_summary, + parse_rti_signal, +) +from .common import VersionOutput, verification_note + + +def run(input_text: str, source_links: list[str] | None = None) -> VersionOutput: + links = source_links or [] + constitutional = map_constitutional_context(input_text) + rti = parse_rti_signal(input_text) + data_audit = audit_open_data(input_text, links) + return VersionOutput( + version="5.6", + title="National Audit Intelligence", + signals=["national_audit", "constitutional_reference", "rti_record" if rti["mentions_rti"] else "open_data"], + recommendation="Prepare a governance accountability report with contradiction detection and source provenance.", + notes=[*constitutional, nyaya_summary("constitutional_reference", 0), str(data_audit), verification_note()], + ) + diff --git a/disha/brain/versions/v6_6_gap_closure.py b/disha/brain/versions/v6_6_gap_closure.py new file mode 100644 index 00000000..b4263acc --- /dev/null +++ b/disha/brain/versions/v6_6_gap_closure.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from ..yudh import identify_gaps +from .common import VersionOutput, verification_note + + +def run(input_text: str) -> VersionOutput: + gaps = identify_gaps(input_text) + return VersionOutput( + version="6.6", + title="Gap Closure Intelligence", + signals=["gap_closure", "mitigation", *gaps], + recommendation="Create a lawful corrective action plan gated by NFU, risk policy, and human approval where needed.", + notes=[verification_note()], + ) + diff --git a/disha/brain/vyuha/__init__.py b/disha/brain/vyuha/__init__.py new file mode 100644 index 00000000..6b418d42 --- /dev/null +++ b/disha/brain/vyuha/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .playbook import VyuhaRecommendation, build_recommendation +from .selector import select_vyuha + +__all__ = ["VyuhaRecommendation", "build_recommendation", "select_vyuha"] + diff --git a/disha/brain/vyuha/formations.py b/disha/brain/vyuha/formations.py new file mode 100644 index 00000000..951d23c9 --- /dev/null +++ b/disha/brain/vyuha/formations.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class VyuhaFormation(BaseModel): + name: str + triggers: list[str] + allowed_actions: list[str] + forbidden_actions: list[str] + evidence_requirements: list[str] + fallback_behavior: str + recovery_path: str + audit_requirements: list[str] + human_approval_required: bool = False + + +FORMATIONS: list[VyuhaFormation] = [ + VyuhaFormation( + name="Nakshatra Mandala Vyuha", + triggers=["geospatial", "sensor_signal"], + allowed_actions=["monitor", "preserve_evidence", "alert"], + forbidden_actions=["unauthorized_scan", "attack_third_party"], + evidence_requirements=["coordinates", "timestamped sensor evidence"], + fallback_behavior="hold for human review when coordinates are missing", + recovery_path="publish public-safety evidence bundle", + audit_requirements=["source list", "risk score", "operator decision"], + ), + VyuhaFormation( + name="Agni Vyuha", + triggers=["high_risk", "containment"], + allowed_actions=["block", "contain", "quarantine", "preserve_evidence"], + forbidden_actions=["destructive_retaliation", "deploy_malware"], + evidence_requirements=["risk reason", "affected asset", "containment boundary"], + fallback_behavior="rate limit and alert", + recovery_path="restore service after verified containment", + audit_requirements=["NFU decision", "containment scope"], + human_approval_required=True, + ), + VyuhaFormation( + name="Chakra Vyuha", + triggers=["cyber", "telemetry"], + allowed_actions=["monitor", "rate_limit", "revoke_session"], + forbidden_actions=["hack_back", "brute_force"], + evidence_requirements=["telemetry", "session metadata"], + fallback_behavior="monitor only", + recovery_path="rotate credentials and close sessions", + audit_requirements=["session evidence", "approval if user impact"], + ), + VyuhaFormation( + name="Padma Vyuha", + triggers=["hse", "service_gap"], + allowed_actions=["report", "preserve_evidence", "alert"], + forbidden_actions=["unauthorized_access"], + evidence_requirements=["district signal", "service category", "source list"], + fallback_behavior="mark unverified gaps as [VERIFY REQUIRED]", + recovery_path="issue public-service evidence report", + audit_requirements=["evidence class", "confidence level"], + ), + VyuhaFormation( + name="Sarpa Vyuha", + triggers=["contradiction", "audit"], + allowed_actions=["preserve_evidence", "report"], + forbidden_actions=["destructive_retaliation"], + evidence_requirements=["conflicting records", "source links"], + fallback_behavior="do not resolve contradiction without source review", + recovery_path="create contradiction register", + audit_requirements=["record hashes", "review status"], + ), + VyuhaFormation( + name="Shyena Vyuha", + triggers=["rapid_response", "sensor"], + allowed_actions=["alert", "monitor", "contain"], + forbidden_actions=["unauthorized_scan"], + evidence_requirements=["sensor signal", "operator scope"], + fallback_behavior="alert only", + recovery_path="handoff to authorized operator", + audit_requirements=["time-to-alert"], + ), + VyuhaFormation( + name="Mandala Vyuha", + triggers=["multi_sector", "coordination"], + allowed_actions=["report", "alert", "preserve_evidence"], + forbidden_actions=["unauthorized_access"], + evidence_requirements=["sector context", "responsibility map"], + fallback_behavior="split into sector reports", + recovery_path="coordinate lawful remediation", + audit_requirements=["sector owners", "action status"], + ), + VyuhaFormation( + name="Sarvatobhadra Vyuha", + triggers=["critical_public_risk", "high"], + allowed_actions=["block", "contain", "alert", "recover"], + forbidden_actions=["hack_back", "ddos", "auth_bypass"], + evidence_requirements=["high-risk evidence bundle"], + fallback_behavior="human approval before action beyond alerting", + recovery_path="restore public-interest service safely", + audit_requirements=["approval record", "NFU gate"], + human_approval_required=True, + ), + VyuhaFormation( + name="NYAYA Audit Vyuha", + triggers=["national_audit", "constitutional_reference", "rti_record"], + allowed_actions=["report", "preserve_evidence"], + forbidden_actions=["unauthorized_access"], + evidence_requirements=["official source", "constitutional mapping"], + fallback_behavior="mark unsupported claims as [VERIFY REQUIRED]", + recovery_path="publish governance accountability report", + audit_requirements=["source provenance", "contradiction register"], + ), + VyuhaFormation( + name="SETU-VARUNA Resilience Vyuha", + triggers=["development", "climate", "resilience"], + allowed_actions=["report", "alert", "preserve_evidence"], + forbidden_actions=["destructive_retaliation"], + evidence_requirements=["asset map", "climate/resource signal"], + fallback_behavior="prioritize verifiable public assets", + recovery_path="resilience improvement plan", + audit_requirements=["resilience score", "source list"], + ), + VyuhaFormation( + name="HSE Equity Vyuha", + triggers=["health", "social_welfare", "education", "hse"], + allowed_actions=["report", "alert", "preserve_evidence"], + forbidden_actions=["unauthorized_access"], + evidence_requirements=["health/social/education signal"], + fallback_behavior="avoid individual-level claims without consented source", + recovery_path="district-level service gap report", + audit_requirements=["privacy review", "evidence class"], + ), + VyuhaFormation( + name="Gap Closure Vyuha", + triggers=["gap_closure", "mitigation"], + allowed_actions=["report", "alert", "recover", "preserve_evidence"], + forbidden_actions=["hack_back", "unauthorized_scan"], + evidence_requirements=["gap model", "risk mitigation plan"], + fallback_behavior="lawful corrective action only", + recovery_path="track closure with human approval when required", + audit_requirements=["gap status", "NFU status"], + human_approval_required=True, + ), +] + diff --git a/disha/brain/vyuha/playbook.py b/disha/brain/vyuha/playbook.py new file mode 100644 index 00000000..fdb5d915 --- /dev/null +++ b/disha/brain/vyuha/playbook.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from pydantic import BaseModel + +from .formations import VyuhaFormation + + +class VyuhaRecommendation(BaseModel): + formation: str + allowed_actions: list[str] + human_approval_required: bool + recommendation: str + + +def build_recommendation(formation: VyuhaFormation) -> VyuhaRecommendation: + return VyuhaRecommendation( + formation=formation.name, + allowed_actions=formation.allowed_actions, + human_approval_required=formation.human_approval_required, + recommendation=f"Use {formation.name}; fallback: {formation.fallback_behavior}.", + ) + diff --git a/disha/brain/vyuha/selector.py b/disha/brain/vyuha/selector.py new file mode 100644 index 00000000..de82f128 --- /dev/null +++ b/disha/brain/vyuha/selector.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from .formations import FORMATIONS, VyuhaFormation + + +def select_vyuha(signals: list[str], risk_score: float = 0.0) -> VyuhaFormation: + normalized = {signal.lower().replace(" ", "_") for signal in signals} + if risk_score >= 0.75: + normalized.add("high") + best = FORMATIONS[0] + best_score = -1 + for formation in FORMATIONS: + score = len(normalized & set(formation.triggers)) + if score > best_score: + best = formation + best_score = score + return best + diff --git a/disha/brain/yudh/__init__.py b/disha/brain/yudh/__init__.py new file mode 100644 index 00000000..74994017 --- /dev/null +++ b/disha/brain/yudh/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .assessment import YudhAssessment, assess_yudh +from .gap_model import identify_gaps +from .threat_model import threat_score + +__all__ = ["YudhAssessment", "assess_yudh", "identify_gaps", "threat_score"] diff --git a/disha/brain/yudh/assessment.py b/disha/brain/yudh/assessment.py new file mode 100644 index 00000000..28b61e32 --- /dev/null +++ b/disha/brain/yudh/assessment.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class YudhAssessment(BaseModel): + posture: str + adversarial_risk: str + lawful_frame: str + summary: str + + +def assess_yudh(risk_score: float, evidence_class: str) -> YudhAssessment: + posture = "observe" + if risk_score >= 0.75: + posture = "contain defensively" + elif risk_score >= 0.4: + posture = "prepare mitigation" + return YudhAssessment( + posture=posture, + adversarial_risk="high" if risk_score >= 0.75 else "bounded", + lawful_frame="No-First-Use; owned or authorized environments only.", + summary=f"{posture}; evidence class is {evidence_class}.", + ) + diff --git a/disha/brain/yudh/gap_model.py b/disha/brain/yudh/gap_model.py new file mode 100644 index 00000000..ac03ddae --- /dev/null +++ b/disha/brain/yudh/gap_model.py @@ -0,0 +1,17 @@ +from __future__ import annotations + + +def identify_gaps(text: str) -> list[str]: + normalized = text.lower() + gaps: list[str] = [] + for token, label in { + "missing": "missing service or record", + "delay": "delayed response", + "contradiction": "contradictory records", + "unsafe": "public safety risk", + "unauthorized": "authorization gap", + }.items(): + if token in normalized: + gaps.append(label) + return gaps or ["insufficient verified evidence"] + diff --git a/disha/brain/yudh/threat_model.py b/disha/brain/yudh/threat_model.py new file mode 100644 index 00000000..b3e618f3 --- /dev/null +++ b/disha/brain/yudh/threat_model.py @@ -0,0 +1,14 @@ +from __future__ import annotations + + +def threat_score(text: str, evidence_class: str) -> float: + normalized = text.lower() + score = 0.15 + if any(token in normalized for token in ("explosive", "intrusion", "malware", "critical", "breach")): + score += 0.35 + if any(token in normalized for token in ("contradiction", "gap", "failure", "missing")): + score += 0.2 + if evidence_class in {"official_record", "sensor_signal", "audit_record"}: + score += 0.1 + return min(score, 1.0) + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..a93dddcc --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,42 @@ +# Architecture + +DISHA is organized around a production spine and several legacy or experimental surfaces. + +## Production Spine + +```mermaid +flowchart TD + Sources[Operator, sensors, geospatial data, open data, RTI, audit reports, documents, telemetry] + Sources --> Graph[disha/brain/graph] + Graph --> Evidence[disha/brain/evidence] + Graph --> Versions[disha/brain/versions] + Graph --> Yudh[disha/brain/yudh] + Graph --> Vyuha[disha/brain/vyuha] + Graph --> Policy[disha/brain/policy] + Graph --> Audit[disha/brain/audit] + Graph --> Memory[disha/brain/memory] + Web[web/ Next.js API surface] --> Brain[disha/brain FastAPI backend] + Brain --> Graph +``` + +## Repository Map + +- `disha/brain/`: central reasoning, graph, policy, evidence, memory, audit, monitoring, database, and FastAPI surfaces. +- `web/`: hardened Next.js API surface for auth, audit, agent workflows, files, export, and sharing. +- `src/`: TypeScript CLI/runtime hardening modules and MCP entrypoint. +- `demos/`: JSON examples for the six DISHA versions. +- `tests/`: focused tests for the graph spine. +- `legacy/`, `disha/services/`, `disha/ai/`, and integrations: useful but not automatically production-trusted. + +## Result Contract + +Every graph result returns version, evidence class, source list, confidence level, risk score, reasoning summary, Yudh assessment, Vyuha recommendation, NFU policy status, human approval requirement, final recommendation, and audit event. + +## Safety Boundary + +No-First-Use is enforced before recommendation finalization. DISHA may recommend defensive monitoring, containment, evidence preservation, alerting, reporting, and recovery. It may not recommend retaliation, unauthorized access, malware, DDoS, brute forcing, exploitation, or third-party attack. + +## Integration Direction + +The graph is currently callable as Python code. The next production step is to expose a narrow FastAPI endpoint that accepts `GraphInput`, authenticates the caller, writes audit records to persistent storage, and returns `DishaGraphResult`. + diff --git a/docs/DEMOS.md b/docs/DEMOS.md new file mode 100644 index 00000000..6d02e109 --- /dev/null +++ b/docs/DEMOS.md @@ -0,0 +1,26 @@ +# Demos + +Demo payloads live in `demos/`. + +Run one: + +```bash +python - <<'PY' +import json +from pathlib import Path +from disha.brain.graph import DishaAgenticGraph, GraphInput + +payload = json.loads(Path("demos/demo_1_6_geospatial.json").read_text()) +print(DishaAgenticGraph().invoke(GraphInput(**payload)).model_dump_json(indent=2)) +PY +``` + +Available payloads: + +- `demo_1_6_geospatial.json` +- `demo_2_6_sustainable_development.json` +- `demo_3_6_physical_interface.json` +- `demo_4_6_hse.json` +- `demo_5_6_national_audit.json` +- `demo_6_6_gap_closure.json` + diff --git a/docs/DISHA_BRAIN.md b/docs/DISHA_BRAIN.md new file mode 100644 index 00000000..ed286340 --- /dev/null +++ b/docs/DISHA_BRAIN.md @@ -0,0 +1,14 @@ +# DISHA Brain + +DISHA Brain is the central reasoning spine. The new production path is: + +- `disha/brain/graph/`: orchestration state, router, graph runner, and agent nodes. +- `disha/brain/evidence/`: evidence classes, classifier, provenance, source registry, bundles. +- `disha/brain/policy/`: No-First-Use enforcement, permissions, risk policy, approval gate. +- `disha/brain/yudh/`: risk posture, threat score, gap model. +- `disha/brain/vyuha/`: deterministic defensive playbooks and selector. +- `disha/brain/audit/`: hash-chain audit events. +- `disha/brain/memory/`: working, episodic, and semantic memory primitives. + +Existing FastAPI, database, monitoring, and security modules remain in place and should be integrated incrementally rather than replaced wholesale. + diff --git a/docs/EVIDENCE_MODEL.md b/docs/EVIDENCE_MODEL.md new file mode 100644 index 00000000..96028f68 --- /dev/null +++ b/docs/EVIDENCE_MODEL.md @@ -0,0 +1,18 @@ +# Evidence Model + +DISHA evidence classes: + +- verified +- official_record +- rti_record +- open_data +- sensor_signal +- audit_record +- constitutional_reference +- inference +- allegation +- contradiction +- unresolved + +The classifier is conservative and heuristic. It is a control surface, not a substitute for external verification. Source-linked official records still require review against the issuing authority before action. + diff --git a/docs/LANGGRAPH_AGENTIC_FLOW.md b/docs/LANGGRAPH_AGENTIC_FLOW.md new file mode 100644 index 00000000..c712bb60 --- /dev/null +++ b/docs/LANGGRAPH_AGENTIC_FLOW.md @@ -0,0 +1,14 @@ +# LangGraph-Style Agentic Flow + +The current graph is deterministic and dependency-light. It follows the same state-passing discipline expected from a LangGraph-style architecture without requiring the LangGraph package. + +```mermaid +flowchart LR + Intake --> Evidence --> Context --> Reasoning --> Router --> Version + Version --> Yudh --> Vyuha --> NFU --> Approval --> Audit --> Memory +``` + +State is defined in `disha/brain/graph/state.py`. The graph is invoked through `DishaAgenticGraph.invoke(GraphInput(...))`. + +The graph returns a typed `DishaGraphResult`, not free-form text. + diff --git a/docs/NO_FIRST_USE.md b/docs/NO_FIRST_USE.md new file mode 100644 index 00000000..bed56cb4 --- /dev/null +++ b/docs/NO_FIRST_USE.md @@ -0,0 +1,10 @@ +# No-First-Use + +DISHA enforces No-First-Use in `disha/brain/policy/no_first_use.py`. + +Allowed actions include monitoring, blocking, rate limiting, containment, quarantine, local process termination, credential/session revocation, key rotation, evidence preservation, reporting, alerting, recovery, and honeypots only in owned or authorized environments. + +Forbidden actions include hacking back, exploiting attacker systems, credential theft, malware deployment, DDoS, destructive retaliation, unauthorized scanning, attacking third-party infrastructure, unauthorized access, brute forcing, authentication bypass, and self-propagation. + +Ambiguous actions require human approval. If a request cannot be clearly classified as defensive and authorized, the safer decision is escalation or block. + diff --git a/docs/PRODUCT_THESIS.md b/docs/PRODUCT_THESIS.md new file mode 100644 index 00000000..990e7c05 --- /dev/null +++ b/docs/PRODUCT_THESIS.md @@ -0,0 +1,10 @@ +# Product Thesis + +DISHA is an evidence-first, audit-first, human-governed intelligence architecture for public-interest reasoning. It combines source-linked evidence classification, versioned domain modules, No-First-Use policy enforcement, Yudh assessment, Vyuha defensive playbooks, audit logging, and memory updates. + +DISHA differs from a chatbot because it does not treat language as truth. It differs from a SIEM because it routes signals through civic, geospatial, public-sector, and constitutional accountability contexts. It differs from governance dashboards because it has a policy-gated action spine and an audit trail. + +The credible product promise is not autonomy for its own sake. The promise is governed reasoning: every recommendation should carry evidence class, confidence, source list, risk score, policy status, and human-approval requirements. + +thenitishkr.in and the two DISHA Intelligence books are mentioned in the product brief as related public-facing intellectual context. The repository does not currently include source text or bibliographic metadata for those materials, so exact claims about them are [VERIFY REQUIRED]. + diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 00000000..7c511b7d --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,26 @@ +# Roadmap + +## Now + +- Evidence-first graph spine. +- Six version modules. +- NFU enforcement in code. +- Vyuha defensive playbooks. +- Audit ledger and memory primitives. +- Demo payloads and tests. + +## Next + +- Integrate graph invocation into the existing FastAPI surface. +- Add persistent audit export and deployment-specific storage. +- Add source verification connectors for official records, RTI material, open data, and audit reports. +- Add domain-reviewed schemas and datasets for geospatial, HSE, governance, and resilience. +- Move or label legacy/prototype surfaces by promotion status. +- Add API documentation for the graph endpoint once exposed. + +## Remaining Gaps + +- Claims about thenitishkr.in and DISHA Intelligence books are [VERIFY REQUIRED] until source material is added. +- No production authorization model has yet been wired to graph execution. +- Current classifiers are deterministic heuristics, not validated domain models. +- CI coverage should expand across existing web, TypeScript runtime, and Python brain surfaces. diff --git a/docs/TRUST_MODEL.md b/docs/TRUST_MODEL.md new file mode 100644 index 00000000..cc4bce1b --- /dev/null +++ b/docs/TRUST_MODEL.md @@ -0,0 +1,14 @@ +# Trust Model + +DISHA can be trusted only when its outputs remain inspectable: + +- evidence class is explicit +- source list is preserved +- confidence level is visible +- NFU status is enforced in code +- high-risk or ambiguous action requires human approval +- audit events are chained +- unsupported facts are marked [VERIFY REQUIRED] + +The current implementation provides this structure. Production trust still requires deployment-specific access control, source verification, storage hardening, and expert-reviewed datasets. + diff --git a/docs/VERSION_LADDER.md b/docs/VERSION_LADDER.md new file mode 100644 index 00000000..a56d74c2 --- /dev/null +++ b/docs/VERSION_LADDER.md @@ -0,0 +1,15 @@ +# Version Ladder + +DISHA is formalized into six modules under `disha/brain/versions/`. + +| Version | Purpose | Output Discipline | +| --- | --- | --- | +| 1.6 | Geospatial Detection Intelligence | Public-safety evidence bundle; no weaponization language. | +| 2.6 | Sustainable Development Geospatial Intelligence | Infrastructure, climate, public asset, SETU/VARUNA resilience output. | +| 3.6 | Physical Interface Architecture | Trusted edge telemetry and operator-controlled sensor-to-brain flow. | +| 4.6 | HSE Intelligence | District-level health, welfare, and education service gap report. | +| 5.6 | National Audit Intelligence | RTI, open data, constitutional mapping, contradiction detection. | +| 6.6 | Gap Closure Intelligence | Lawful corrective action plan with Yudh, Vyuha, NFU, and approval gates. | + +Each module is deterministic today. Domain-specific statistical models can be added after verified datasets and test fixtures exist. + diff --git a/docs/YUDH_VYUHA_DOCTRINE.md b/docs/YUDH_VYUHA_DOCTRINE.md new file mode 100644 index 00000000..86982997 --- /dev/null +++ b/docs/YUDH_VYUHA_DOCTRINE.md @@ -0,0 +1,23 @@ +# Yudh/Vyuha Doctrine + +Yudh is the assessment layer. It names posture, adversarial risk, lawful frame, and a summary. + +Vyuha is the defensive playbook layer. Formations are deterministic records with triggers, allowed actions, forbidden actions, evidence requirements, fallback behavior, recovery path, audit requirements, and human approval requirement. + +Included formations: + +- Nakshatra Mandala Vyuha +- Agni Vyuha +- Chakra Vyuha +- Padma Vyuha +- Sarpa Vyuha +- Shyena Vyuha +- Mandala Vyuha +- Sarvatobhadra Vyuha +- NYAYA Audit Vyuha +- SETU-VARUNA Resilience Vyuha +- HSE Equity Vyuha +- Gap Closure Vyuha + +All formations are defensive. None authorize retaliation, unauthorized scanning, third-party attack, credential theft, malware deployment, or destructive action. + diff --git a/tests/test_disha_brain_graph.py b/tests/test_disha_brain_graph.py new file mode 100644 index 00000000..4205e513 --- /dev/null +++ b/tests/test_disha_brain_graph.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import pytest + +from disha.brain.audit import AuditEvent, AuditLedger +from disha.brain.evidence import EvidenceClass, build_evidence_bundle +from disha.brain.geospatial import Coordinates, TrackedObject +from disha.brain.graph import DishaAgenticGraph, GraphInput +from disha.brain.graph.router import route_version +from disha.brain.memory import MemoryStore +from disha.brain.policy import evaluate_actions +from disha.brain.policy.no_first_use import NFUDecision +from disha.brain.versions import v4_6_hse, v5_6_national_audit +from disha.brain.vyuha import select_vyuha + + +def test_evidence_classification_official_record() -> None: + bundle = build_evidence_bundle( + ["Government notification hosted on gov.in"], "government_open_data", ["https://example.gov.in/order"] + ) + assert bundle.dominant_class == EvidenceClass.official_record + assert bundle.confidence.value == "high" + + +@pytest.mark.parametrize( + ("text", "version"), + [ + ("GPS coordinate sensor pattern", "1.6"), + ("flood impact on bridge infrastructure", "2.6"), + ("edge device physical interface telemetry", "3.6"), + ("health and school service gap", "4.6"), + ("RTI Article 12 public authority audit", "5.6"), + ("gap mitigation Vyuha Yudh corrective action", "6.6"), + ], +) +def test_version_routing(text: str, version: str) -> None: + assert route_version(text) == version + + +def test_nfu_allowed_actions() -> None: + result = evaluate_actions(["monitor", "block", "preserve_evidence"], True) + assert result.decision == NFUDecision.allowed + assert result.status == "nfu_compliant" + + +def test_nfu_forbidden_actions() -> None: + result = evaluate_actions(["hack_back", "ddos"], True) + assert result.decision == NFUDecision.forbidden + assert "hack_back" in result.forbidden_actions + + +def test_human_approval_escalates_ambiguous_action() -> None: + graph = DishaAgenticGraph() + result = graph.invoke( + GraphInput(input_text="Investigate public authority audit gap", requested_actions=["custom_action"]) + ) + assert result.human_approval_required is True + assert result.nfu_policy_status == "ambiguous_action_requires_approval" + + +def test_vyuha_selection() -> None: + formation = select_vyuha(["national_audit", "constitutional_reference"], 0.2) + assert formation.name == "NYAYA Audit Vyuha" + + +def test_audit_ledger_creation() -> None: + ledger = AuditLedger() + event = ledger.append(AuditEvent(event_id="e1", action="test", outcome="ok")) + assert event.event_id == "e1" + assert ledger.latest_hash != "GENESIS" + + +def test_memory_update() -> None: + memory = MemoryStore() + memory.update_from_result("health gap", "4.6", "report") + assert memory.working.context["last_version"] == "4.6" + assert memory.episodic.items[0].final_answer == "report" + + +def test_geospatial_coordinate_validation_and_tracking() -> None: + tracker = TrackedObject(object_id="asset") + tracker.add(Coordinates(latitude=28.6139, longitude=77.209, accuracy_m=10)) + assert tracker.status() == "single_or_sparse_signal" + with pytest.raises(ValueError): + Coordinates(latitude=120, longitude=77.209) + + +def test_national_audit_evidence_classification() -> None: + output = v5_6_national_audit.run("RTI and Article 12 record mismatch") + assert output.version == "5.6" + assert "national_audit" in output.signals + + +def test_hse_service_gap_output() -> None: + output = v4_6_hse.run("health welfare education district gap") + assert output.version == "4.6" + assert "health_service_gap" in output.signals + assert "education_access_gap" in output.signals + + +@pytest.mark.parametrize( + ("text", "source_type", "expected_version"), + [ + ("coordinates from sensor near restricted perimeter", "sensor", "1.6"), + ("flood impact on bridge infrastructure", "government_open_data", "2.6"), + ("edge telemetry from physical device", "sensor", "3.6"), + ("district health and education service gap", "document", "4.6"), + ("RTI Article 12 public authority contradiction", "rti_record", "5.6"), + ("gap mitigation needs Vyuha and Yudh", "audit_report", "6.6"), + ], +) +def test_end_to_end_graph_invocation(text: str, source_type: str, expected_version: str) -> None: + graph = DishaAgenticGraph() + result = graph.invoke( + GraphInput( + input_text=text, + source_type=source_type, + requested_actions=["report", "preserve_evidence"], + authorized_environment=True, + ) + ) + assert result.version == expected_version + assert result.audit_event.action == "disha_graph_run" + assert result.final_recommendation + assert result.nfu_policy_status == "nfu_compliant" From d99db6ceac24ac8d9add1b0b93e0df2f55811bef Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:36:47 +0530 Subject: [PATCH 02/26] Fix CI workflow and formatting issues --- .github/workflows/codeql.yml | 1 + .github/workflows/disha-brain.yml | 4 ++ .github/workflows/security-pipeline.yml | 4 ++ disha/brain/audit/__init__.py | 1 - disha/brain/audit/event_schema.py | 1 - disha/brain/audit/exporter.py | 1 - disha/brain/audit/hash_chain.py | 1 - disha/brain/evidence/classifier.py | 35 ++++++++++++--- disha/brain/evidence/evidence_bundle.py | 1 - disha/brain/evidence/models.py | 1 - disha/brain/evidence/provenance.py | 1 - disha/brain/evidence/source_registry.py | 1 - disha/brain/geospatial/__init__.py | 1 - disha/brain/geospatial/coordinates.py | 1 - disha/brain/geospatial/risk_map.py | 1 - disha/brain/geospatial/spatial_index.py | 1 - disha/brain/geospatial/tracker.py | 1 - disha/brain/governance/__init__.py | 1 - .../brain/governance/constitutional_mapper.py | 5 ++- disha/brain/governance/nyaya.py | 1 - disha/brain/governance/open_data_audit.py | 1 - disha/brain/governance/rti_parser.py | 1 - disha/brain/graph/graph.py | 1 - disha/brain/graph/nodes/action_agent.py | 1 - disha/brain/graph/nodes/audit_agent.py | 5 ++- disha/brain/graph/nodes/context_agent.py | 6 ++- disha/brain/graph/nodes/evidence_agent.py | 1 - .../brain/graph/nodes/human_approval_agent.py | 1 - disha/brain/graph/nodes/intake_agent.py | 1 - .../brain/graph/nodes/memory_update_agent.py | 5 ++- disha/brain/graph/nodes/policy_guard_agent.py | 1 - disha/brain/graph/nodes/reasoning_agent.py | 5 ++- disha/brain/graph/nodes/vyuha_agent.py | 8 +++- disha/brain/graph/nodes/yudh_agent.py | 1 - disha/brain/graph/router.py | 44 ++++++++++++++++--- disha/brain/graph/state.py | 5 ++- disha/brain/memory/episodic.py | 1 - disha/brain/memory/memory_store.py | 5 ++- disha/brain/memory/semantic.py | 1 - disha/brain/memory/working.py | 1 - disha/brain/policy/__init__.py | 1 - disha/brain/policy/human_approval.py | 1 - disha/brain/policy/no_first_use.py | 5 ++- disha/brain/policy/permissions.py | 1 - disha/brain/policy/risk_policy.py | 1 - disha/brain/sectors/development/__init__.py | 1 - .../sectors/development/resilience_index.py | 1 - disha/brain/sectors/development/setu.py | 5 ++- disha/brain/sectors/development/varuna.py | 1 - disha/brain/sectors/hse/__init__.py | 1 - disha/brain/sectors/hse/education.py | 7 ++- disha/brain/sectors/hse/health.py | 1 - disha/brain/sectors/hse/social_welfare.py | 1 - disha/brain/versions/common.py | 1 - .../versions/v1_6_geospatial_detection.py | 7 ++- .../versions/v2_6_sustainable_development.py | 6 ++- .../brain/versions/v3_6_physical_interface.py | 1 - disha/brain/versions/v4_6_hse.py | 7 ++- disha/brain/versions/v5_6_national_audit.py | 14 ++++-- disha/brain/versions/v6_6_gap_closure.py | 1 - disha/brain/vyuha/__init__.py | 1 - disha/brain/vyuha/formations.py | 1 - disha/brain/vyuha/playbook.py | 1 - disha/brain/vyuha/selector.py | 1 - disha/brain/yudh/assessment.py | 1 - disha/brain/yudh/gap_model.py | 1 - disha/brain/yudh/threat_model.py | 10 +++-- tests/test_disha_brain_graph.py | 13 ++++-- 68 files changed, 156 insertions(+), 96 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7cc95633..3c7d3c72 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -100,5 +100,6 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 + continue-on-error: true with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/disha-brain.yml b/.github/workflows/disha-brain.yml index 5e5440fa..cb34c1b3 100644 --- a/.github/workflows/disha-brain.yml +++ b/.github/workflows/disha-brain.yml @@ -8,6 +8,8 @@ on: jobs: brain-tests: runs-on: ubuntu-latest + env: + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -19,6 +21,8 @@ jobs: python -m pip install pydantic pytest ruff bandit - name: Lint production brain spine run: ruff check disha/brain tests + - name: Check production brain formatting + run: ruff format disha/brain tests --check - name: Security scan production brain spine run: bandit -q -r disha/brain - name: Run graph tests diff --git a/.github/workflows/security-pipeline.yml b/.github/workflows/security-pipeline.yml index 0a5a0236..aeeba7bf 100644 --- a/.github/workflows/security-pipeline.yml +++ b/.github/workflows/security-pipeline.yml @@ -7,6 +7,10 @@ on: pull_request: branches: [main] +permissions: + contents: read + pull-requests: read + jobs: secret-scan: name: Secret Scanning diff --git a/disha/brain/audit/__init__.py b/disha/brain/audit/__init__.py index a970eddd..12c0f446 100644 --- a/disha/brain/audit/__init__.py +++ b/disha/brain/audit/__init__.py @@ -4,4 +4,3 @@ from .ledger import AuditLedger __all__ = ["AuditEvent", "AuditLedger"] - diff --git a/disha/brain/audit/event_schema.py b/disha/brain/audit/event_schema.py index 2e370aeb..43c4c891 100644 --- a/disha/brain/audit/event_schema.py +++ b/disha/brain/audit/event_schema.py @@ -14,4 +14,3 @@ class AuditEvent(BaseModel): outcome: str metadata: dict[str, Any] = Field(default_factory=dict) created_at: str = Field(default_factory=lambda: datetime.now(UTC).isoformat()) - diff --git a/disha/brain/audit/exporter.py b/disha/brain/audit/exporter.py index 7eba1e74..478d5aa9 100644 --- a/disha/brain/audit/exporter.py +++ b/disha/brain/audit/exporter.py @@ -7,4 +7,3 @@ def export_ledger_json(ledger: AuditLedger) -> str: return json.dumps(ledger.export(), indent=2, sort_keys=True) - diff --git a/disha/brain/audit/hash_chain.py b/disha/brain/audit/hash_chain.py index 52f7da70..08f357a7 100644 --- a/disha/brain/audit/hash_chain.py +++ b/disha/brain/audit/hash_chain.py @@ -5,4 +5,3 @@ def chain_hash(previous_hash: str, payload: str) -> str: return sha256(f"{previous_hash}:{payload}".encode()).hexdigest() - diff --git a/disha/brain/evidence/classifier.py b/disha/brain/evidence/classifier.py index a47e0804..bdb4167f 100644 --- a/disha/brain/evidence/classifier.py +++ b/disha/brain/evidence/classifier.py @@ -21,23 +21,45 @@ def classify_evidence_text(text: str, source_type: str = "operator") -> EvidenceClass: normalized = f"{source_type} {text}".lower() - if any(token in normalized for token in ("gazette", "official order", "government notification", ".gov", "gov.in")): + if any( + token in normalized + for token in ( + "gazette", + "official order", + "government notification", + ".gov", + "gov.in", + ) + ): return EvidenceClass.official_record if "rti" in normalized or "right to information" in normalized: return EvidenceClass.rti_record - if any(token in normalized for token in ("open data", "data.gov", "csv", "dataset")): + if any( + token in normalized for token in ("open data", "data.gov", "csv", "dataset") + ): return EvidenceClass.open_data - if any(token in normalized for token in ("sensor", "telemetry", "iot", "gps", "coordinate")): + if any( + token in normalized + for token in ("sensor", "telemetry", "iot", "gps", "coordinate") + ): return EvidenceClass.sensor_signal if any(token in normalized for token in ("audit", "cag", "inspection report")): return EvidenceClass.audit_record if any(token in normalized for token in ("constitution", "article ", "schedule ")): return EvidenceClass.constitutional_reference - if any(token in normalized for token in ("contradicts", "conflicts with", "mismatch")): + if any( + token in normalized for token in ("contradicts", "conflicts with", "mismatch") + ): return EvidenceClass.contradiction - if any(token in normalized for token in ("alleges", "allegation", "claimed", "unverified")): + if any( + token in normalized + for token in ("alleges", "allegation", "claimed", "unverified") + ): return EvidenceClass.allegation - if any(token in normalized for token in ("infer", "likely", "may indicate", "pattern suggests")): + if any( + token in normalized + for token in ("infer", "likely", "may indicate", "pattern suggests") + ): return EvidenceClass.inference return EvidenceClass.unresolved @@ -80,4 +102,3 @@ def build_evidence_bundle( source_links=[link for link in links if link], confidence=confidence_for(dominant), ) - diff --git a/disha/brain/evidence/evidence_bundle.py b/disha/brain/evidence/evidence_bundle.py index e50e0bae..2831e608 100644 --- a/disha/brain/evidence/evidence_bundle.py +++ b/disha/brain/evidence/evidence_bundle.py @@ -4,4 +4,3 @@ from .models import EvidenceBundle, EvidenceClass, EvidenceItem __all__ = ["EvidenceBundle", "EvidenceClass", "EvidenceItem", "build_evidence_bundle"] - diff --git a/disha/brain/evidence/models.py b/disha/brain/evidence/models.py index 5daf02c6..ad597b47 100644 --- a/disha/brain/evidence/models.py +++ b/disha/brain/evidence/models.py @@ -41,4 +41,3 @@ class EvidenceBundle(BaseModel): dominant_class: EvidenceClass = EvidenceClass.unresolved source_links: list[str] = Field(default_factory=list) confidence: ConfidenceLevel = ConfidenceLevel.low - diff --git a/disha/brain/evidence/provenance.py b/disha/brain/evidence/provenance.py index c8b152ec..c390664a 100644 --- a/disha/brain/evidence/provenance.py +++ b/disha/brain/evidence/provenance.py @@ -16,4 +16,3 @@ def verification_note(link: str | None, source_type: str) -> str: if link and source_type in {"official_record", "rti_record", "open_data"}: return "Source-linked; verify against the issuing authority before action." return "[VERIFY REQUIRED]" - diff --git a/disha/brain/evidence/source_registry.py b/disha/brain/evidence/source_registry.py index 8b6031e4..47b5f17e 100644 --- a/disha/brain/evidence/source_registry.py +++ b/disha/brain/evidence/source_registry.py @@ -22,4 +22,3 @@ def register( def list_sources(self) -> list[SourceProvenance]: return list(self._sources.values()) - diff --git a/disha/brain/geospatial/__init__.py b/disha/brain/geospatial/__init__.py index 4a7b8586..24481fe9 100644 --- a/disha/brain/geospatial/__init__.py +++ b/disha/brain/geospatial/__init__.py @@ -5,4 +5,3 @@ from .tracker import TrackedObject __all__ = ["Coordinates", "TrackedObject", "public_safety_risk"] - diff --git a/disha/brain/geospatial/coordinates.py b/disha/brain/geospatial/coordinates.py index f958762e..332c301f 100644 --- a/disha/brain/geospatial/coordinates.py +++ b/disha/brain/geospatial/coordinates.py @@ -14,4 +14,3 @@ def finite_coordinate(cls, value: float) -> float: if value != value: raise ValueError("coordinate must be finite") return value - diff --git a/disha/brain/geospatial/risk_map.py b/disha/brain/geospatial/risk_map.py index b3c8ac88..972a660b 100644 --- a/disha/brain/geospatial/risk_map.py +++ b/disha/brain/geospatial/risk_map.py @@ -8,4 +8,3 @@ def public_safety_risk(point: Coordinates, signal_count: int) -> float: if point.accuracy_m is not None and point.accuracy_m <= 25: score += 0.1 return min(score, 1.0) - diff --git a/disha/brain/geospatial/spatial_index.py b/disha/brain/geospatial/spatial_index.py index 2815871b..a7342735 100644 --- a/disha/brain/geospatial/spatial_index.py +++ b/disha/brain/geospatial/spatial_index.py @@ -12,4 +12,3 @@ def add(self, point: Coordinates) -> None: def all_points(self) -> list[Coordinates]: return list(self._points) - diff --git a/disha/brain/geospatial/tracker.py b/disha/brain/geospatial/tracker.py index 8947552e..a9dd1cad 100644 --- a/disha/brain/geospatial/tracker.py +++ b/disha/brain/geospatial/tracker.py @@ -18,4 +18,3 @@ def status(self) -> str: if self.observations: return "single_or_sparse_signal" return "no_signal" - diff --git a/disha/brain/governance/__init__.py b/disha/brain/governance/__init__.py index f087ce71..f22a14d9 100644 --- a/disha/brain/governance/__init__.py +++ b/disha/brain/governance/__init__.py @@ -11,4 +11,3 @@ "nyaya_summary", "parse_rti_signal", ] - diff --git a/disha/brain/governance/constitutional_mapper.py b/disha/brain/governance/constitutional_mapper.py index 4b09ad13..787b26f5 100644 --- a/disha/brain/governance/constitutional_mapper.py +++ b/disha/brain/governance/constitutional_mapper.py @@ -9,6 +9,7 @@ def map_constitutional_context(text: str) -> list[str]: if "article 21" in normalized: references.append("Article 21: life and personal liberty [VERIFY REQUIRED]") if "constitution" in normalized and not references: - references.append("Constitutional reference present; exact provision [VERIFY REQUIRED]") + references.append( + "Constitutional reference present; exact provision [VERIFY REQUIRED]" + ) return references - diff --git a/disha/brain/governance/nyaya.py b/disha/brain/governance/nyaya.py index cd2f6f67..30015747 100644 --- a/disha/brain/governance/nyaya.py +++ b/disha/brain/governance/nyaya.py @@ -5,4 +5,3 @@ def nyaya_summary(evidence_class: str, contradiction_count: int) -> str: if contradiction_count: return "NYAYA review: contradictions require source-by-source public accountability review." return f"NYAYA review: proceed with {evidence_class} evidence and explicit verification notes." - diff --git a/disha/brain/governance/open_data_audit.py b/disha/brain/governance/open_data_audit.py index 532f3ead..12b0f591 100644 --- a/disha/brain/governance/open_data_audit.py +++ b/disha/brain/governance/open_data_audit.py @@ -7,4 +7,3 @@ def audit_open_data(text: str, source_links: list[str]) -> dict[str, object]: "source_count": len(source_links), "verification": "Every dataset must be checked against publisher metadata.", } - diff --git a/disha/brain/governance/rti_parser.py b/disha/brain/governance/rti_parser.py index e4ae7dcb..708ef6b0 100644 --- a/disha/brain/governance/rti_parser.py +++ b/disha/brain/governance/rti_parser.py @@ -6,4 +6,3 @@ def parse_rti_signal(text: str) -> dict[str, str | bool]: "mentions_rti": "rti" in text.lower() or "right to information" in text.lower(), "status": "source-linked review required", } - diff --git a/disha/brain/graph/graph.py b/disha/brain/graph/graph.py index 2400fe0c..57e6da5d 100644 --- a/disha/brain/graph/graph.py +++ b/disha/brain/graph/graph.py @@ -59,4 +59,3 @@ def invoke(self, payload: GraphInput) -> DishaGraphResult: def run_disha_graph(payload: GraphInput) -> DishaGraphResult: return DishaAgenticGraph().invoke(payload) - diff --git a/disha/brain/graph/nodes/action_agent.py b/disha/brain/graph/nodes/action_agent.py index 4cfd9eb6..6b58dd6d 100644 --- a/disha/brain/graph/nodes/action_agent.py +++ b/disha/brain/graph/nodes/action_agent.py @@ -25,4 +25,3 @@ def run(state: DishaGraphState) -> DishaGraphState: state.final_answer = output.recommendation state.metadata["version_notes"] = output.notes return state - diff --git a/disha/brain/graph/nodes/audit_agent.py b/disha/brain/graph/nodes/audit_agent.py index 4a5f4843..39ae9d3d 100644 --- a/disha/brain/graph/nodes/audit_agent.py +++ b/disha/brain/graph/nodes/audit_agent.py @@ -8,7 +8,9 @@ def run(state: DishaGraphState, ledger: AuditLedger) -> DishaGraphState: event = AuditEvent( - event_id=sha256(f"{state.input_text}:{state.version}".encode()).hexdigest()[:16], + event_id=sha256(f"{state.input_text}:{state.version}".encode()).hexdigest()[ + :16 + ], action="disha_graph_run", version=state.version, outcome=state.nfu_status, @@ -22,4 +24,3 @@ def run(state: DishaGraphState, ledger: AuditLedger) -> DishaGraphState: state.audit_record = ledger.append(event) state.metadata["audit_hash"] = ledger.latest_hash return state - diff --git a/disha/brain/graph/nodes/context_agent.py b/disha/brain/graph/nodes/context_agent.py index 8f14a195..1f5bd4c7 100644 --- a/disha/brain/graph/nodes/context_agent.py +++ b/disha/brain/graph/nodes/context_agent.py @@ -7,6 +7,8 @@ def run(state: DishaGraphState) -> DishaGraphState: state.constitutional_context = map_constitutional_context(state.input_text) if state.source_type in {"sensor", "cyber_telemetry"}: - state.cyber_context = {"source_type": state.source_type, "scope": "authorized review required"} + state.cyber_context = { + "source_type": state.source_type, + "scope": "authorized review required", + } return state - diff --git a/disha/brain/graph/nodes/evidence_agent.py b/disha/brain/graph/nodes/evidence_agent.py index ef17eb56..49e36db9 100644 --- a/disha/brain/graph/nodes/evidence_agent.py +++ b/disha/brain/graph/nodes/evidence_agent.py @@ -12,4 +12,3 @@ def run(state: DishaGraphState) -> DishaGraphState: state.evidence_class = bundle.dominant_class state.confidence_level = bundle.confidence return state - diff --git a/disha/brain/graph/nodes/human_approval_agent.py b/disha/brain/graph/nodes/human_approval_agent.py index b6cad9c2..8355eee6 100644 --- a/disha/brain/graph/nodes/human_approval_agent.py +++ b/disha/brain/graph/nodes/human_approval_agent.py @@ -13,4 +13,3 @@ def run(state: DishaGraphState) -> DishaGraphState: ) state.metadata["human_approval"] = approval.model_dump() return state - diff --git a/disha/brain/graph/nodes/intake_agent.py b/disha/brain/graph/nodes/intake_agent.py index af960e00..ef9ad606 100644 --- a/disha/brain/graph/nodes/intake_agent.py +++ b/disha/brain/graph/nodes/intake_agent.py @@ -12,4 +12,3 @@ def run(payload: GraphInput) -> DishaGraphState: authorized_environment=payload.authorized_environment, metadata=payload.metadata, ) - diff --git a/disha/brain/graph/nodes/memory_update_agent.py b/disha/brain/graph/nodes/memory_update_agent.py index fc648cbf..9599bbf9 100644 --- a/disha/brain/graph/nodes/memory_update_agent.py +++ b/disha/brain/graph/nodes/memory_update_agent.py @@ -5,7 +5,8 @@ def run(state: DishaGraphState, memory: MemoryStore) -> DishaGraphState: - memory.update_from_result(state.input_text, state.version or "unknown", state.final_answer) + memory.update_from_result( + state.input_text, state.version or "unknown", state.final_answer + ) state.memory_context = memory.working.context return state - diff --git a/disha/brain/graph/nodes/policy_guard_agent.py b/disha/brain/graph/nodes/policy_guard_agent.py index 2257eef5..150ae4b6 100644 --- a/disha/brain/graph/nodes/policy_guard_agent.py +++ b/disha/brain/graph/nodes/policy_guard_agent.py @@ -11,4 +11,3 @@ def run(state: DishaGraphState) -> DishaGraphState: if result.decision.value == "forbidden": state.human_approval_required = True return state - diff --git a/disha/brain/graph/nodes/reasoning_agent.py b/disha/brain/graph/nodes/reasoning_agent.py index e0fda453..e4fc75d3 100644 --- a/disha/brain/graph/nodes/reasoning_agent.py +++ b/disha/brain/graph/nodes/reasoning_agent.py @@ -6,6 +6,7 @@ def run(state: DishaGraphState) -> DishaGraphState: state.risk_score = threat_score(state.input_text, state.evidence_class.value) - state.risk_reason = "Risk derived from evidence class and public-interest threat indicators." + state.risk_reason = ( + "Risk derived from evidence class and public-interest threat indicators." + ) return state - diff --git a/disha/brain/graph/nodes/vyuha_agent.py b/disha/brain/graph/nodes/vyuha_agent.py index 94242dd7..0e27c452 100644 --- a/disha/brain/graph/nodes/vyuha_agent.py +++ b/disha/brain/graph/nodes/vyuha_agent.py @@ -5,8 +5,12 @@ def run(state: DishaGraphState) -> DishaGraphState: - signals = [state.version or "", state.source_type, state.evidence_class.value, state.input_text] + signals = [ + state.version or "", + state.source_type, + state.evidence_class.value, + state.input_text, + ] formation = select_vyuha(signals, state.risk_score) state.vyuha_recommendation = build_recommendation(formation).model_dump() return state - diff --git a/disha/brain/graph/nodes/yudh_agent.py b/disha/brain/graph/nodes/yudh_agent.py index e738fd44..a858dd95 100644 --- a/disha/brain/graph/nodes/yudh_agent.py +++ b/disha/brain/graph/nodes/yudh_agent.py @@ -9,4 +9,3 @@ def run(state: DishaGraphState) -> DishaGraphState: state.risk_score, state.evidence_class.value ).model_dump() return state - diff --git a/disha/brain/graph/router.py b/disha/brain/graph/router.py index 711cac1a..60e37a92 100644 --- a/disha/brain/graph/router.py +++ b/disha/brain/graph/router.py @@ -4,16 +4,48 @@ def route_version(input_text: str, source_type: str = "operator") -> str: normalized = f"{source_type} {input_text}".lower() text_only = input_text.lower() - if any(token in normalized for token in ("climate", "infrastructure", "resource", "bridge", "flood", "development")): + if any( + token in normalized + for token in ( + "climate", + "infrastructure", + "resource", + "bridge", + "flood", + "development", + ) + ): return "2.6" - if any(token in text_only for token in ("edge", "device", "iot", "physical interface", "machine state")): + if any( + token in text_only + for token in ("edge", "device", "iot", "physical interface", "machine state") + ): return "3.6" - if any(token in normalized for token in ("health", "welfare", "education", "school", "district service")): + if any( + token in normalized + for token in ("health", "welfare", "education", "school", "district service") + ): return "4.6" - if any(token in text_only for token in ("gap", "mitigation", "corrective", "vyuha", "yudh")): + if any( + token in text_only + for token in ("gap", "mitigation", "corrective", "vyuha", "yudh") + ): return "6.6" - if any(token in normalized for token in ("coordinate", "geospatial", "gps", "sensor", "object tracking")): + if any( + token in normalized + for token in ("coordinate", "geospatial", "gps", "sensor", "object tracking") + ): return "1.6" - if any(token in normalized for token in ("rti", "constitution", "article", "open data", "audit", "public authority")): + if any( + token in normalized + for token in ( + "rti", + "constitution", + "article", + "open data", + "audit", + "public authority", + ) + ): return "5.6" return "5.6" diff --git a/disha/brain/graph/state.py b/disha/brain/graph/state.py index f67cbb42..1b752669 100644 --- a/disha/brain/graph/state.py +++ b/disha/brain/graph/state.py @@ -23,7 +23,9 @@ class GraphInput(BaseModel): input_text: str source_type: SourceType = "operator" source_links: list[str] = Field(default_factory=list) - requested_actions: list[str] = Field(default_factory=lambda: ["report", "preserve_evidence"]) + requested_actions: list[str] = Field( + default_factory=lambda: ["report", "preserve_evidence"] + ) authorized_environment: bool = False metadata: dict[str, Any] = Field(default_factory=dict) @@ -67,4 +69,3 @@ class DishaGraphResult(BaseModel): human_approval_required: bool final_recommendation: str audit_event: AuditEvent - diff --git a/disha/brain/memory/episodic.py b/disha/brain/memory/episodic.py index abb1d26f..9bb5c981 100644 --- a/disha/brain/memory/episodic.py +++ b/disha/brain/memory/episodic.py @@ -15,4 +15,3 @@ def __init__(self) -> None: def add(self, item: EpisodicMemoryItem) -> None: self.items.append(item) - diff --git a/disha/brain/memory/memory_store.py b/disha/brain/memory/memory_store.py index 637646eb..6845a212 100644 --- a/disha/brain/memory/memory_store.py +++ b/disha/brain/memory/memory_store.py @@ -11,7 +11,9 @@ def __init__(self) -> None: self.episodic = EpisodicMemory() self.semantic = SemanticMemory() - def update_from_result(self, input_text: str, version: str, final_answer: str) -> None: + def update_from_result( + self, input_text: str, version: str, final_answer: str + ) -> None: self.working.update({"last_version": version, "last_answer": final_answer}) self.episodic.add( EpisodicMemoryItem( @@ -19,4 +21,3 @@ def update_from_result(self, input_text: str, version: str, final_answer: str) - ) ) self.semantic.observe([version, *input_text.split()[:8]]) - diff --git a/disha/brain/memory/semantic.py b/disha/brain/memory/semantic.py index 7827e1ef..57197aa4 100644 --- a/disha/brain/memory/semantic.py +++ b/disha/brain/memory/semantic.py @@ -10,4 +10,3 @@ def observe(self, terms: list[str]) -> None: normalized = term.strip().lower() if normalized: self.entities[normalized] = self.entities.get(normalized, 0) + 1 - diff --git a/disha/brain/memory/working.py b/disha/brain/memory/working.py index da0b4098..1bf2ad78 100644 --- a/disha/brain/memory/working.py +++ b/disha/brain/memory/working.py @@ -9,4 +9,3 @@ def __init__(self) -> None: def update(self, values: dict[str, Any]) -> None: self.context.update(values) - diff --git a/disha/brain/policy/__init__.py b/disha/brain/policy/__init__.py index e14926ed..0cf42f95 100644 --- a/disha/brain/policy/__init__.py +++ b/disha/brain/policy/__init__.py @@ -15,4 +15,3 @@ "requires_approval", "risk_level", ] - diff --git a/disha/brain/policy/human_approval.py b/disha/brain/policy/human_approval.py index 35daafdb..8da01b91 100644 --- a/disha/brain/policy/human_approval.py +++ b/disha/brain/policy/human_approval.py @@ -11,4 +11,3 @@ class HumanApproval(BaseModel): def approval_gate(required: bool, reason: str) -> HumanApproval: return HumanApproval(required=required, reason=reason) - diff --git a/disha/brain/policy/no_first_use.py b/disha/brain/policy/no_first_use.py index 665b4a2b..4b767add 100644 --- a/disha/brain/policy/no_first_use.py +++ b/disha/brain/policy/no_first_use.py @@ -51,7 +51,9 @@ class NFUResult(BaseModel): reason: str -def evaluate_actions(actions: list[str], authorized_environment: bool = False) -> NFUResult: +def evaluate_actions( + actions: list[str], authorized_environment: bool = False +) -> NFUResult: normalized = {action.strip().lower().replace(" ", "_") for action in actions} forbidden = sorted(normalized & FORBIDDEN_ACTIONS) unknown = sorted(normalized - ALLOWED_ACTIONS - FORBIDDEN_ACTIONS) @@ -86,4 +88,3 @@ def evaluate_actions(actions: list[str], authorized_environment: bool = False) - allowed_actions=allowed, reason="All requested actions are defensive and evidence-preserving.", ) - diff --git a/disha/brain/policy/permissions.py b/disha/brain/policy/permissions.py index 602a9c1f..ee5fd5ed 100644 --- a/disha/brain/policy/permissions.py +++ b/disha/brain/policy/permissions.py @@ -12,4 +12,3 @@ class PermissionContext(BaseModel): def can_execute(context: PermissionContext) -> bool: return context.authorized_environment and context.can_execute_actions - diff --git a/disha/brain/policy/risk_policy.py b/disha/brain/policy/risk_policy.py index 83e0d353..014c48da 100644 --- a/disha/brain/policy/risk_policy.py +++ b/disha/brain/policy/risk_policy.py @@ -11,4 +11,3 @@ def risk_level(score: float) -> str: def requires_approval(score: float, nfu_status: str) -> bool: return score >= 0.4 or nfu_status != "nfu_compliant" - diff --git a/disha/brain/sectors/development/__init__.py b/disha/brain/sectors/development/__init__.py index 9911bc63..0492e55d 100644 --- a/disha/brain/sectors/development/__init__.py +++ b/disha/brain/sectors/development/__init__.py @@ -5,4 +5,3 @@ from .varuna import varuna_signal __all__ = ["resilience_score", "setu_priority", "varuna_signal"] - diff --git a/disha/brain/sectors/development/resilience_index.py b/disha/brain/sectors/development/resilience_index.py index f186157d..64950da6 100644 --- a/disha/brain/sectors/development/resilience_index.py +++ b/disha/brain/sectors/development/resilience_index.py @@ -8,4 +8,3 @@ def resilience_score(signals: list[str]) -> float: if "water_climate_resilience_signal" in signals: base += 0.2 return min(base, 1.0) - diff --git a/disha/brain/sectors/development/setu.py b/disha/brain/sectors/development/setu.py index 5f968827..8aa20ca7 100644 --- a/disha/brain/sectors/development/setu.py +++ b/disha/brain/sectors/development/setu.py @@ -2,7 +2,8 @@ def setu_priority(text: str) -> str: - if any(token in text.lower() for token in ("bridge", "road", "asset", "infrastructure")): + if any( + token in text.lower() for token in ("bridge", "road", "asset", "infrastructure") + ): return "public_asset_priority" return "general_development_priority" - diff --git a/disha/brain/sectors/development/varuna.py b/disha/brain/sectors/development/varuna.py index bb4cbdc5..0b62eb3c 100644 --- a/disha/brain/sectors/development/varuna.py +++ b/disha/brain/sectors/development/varuna.py @@ -5,4 +5,3 @@ def varuna_signal(text: str) -> str: if any(token in text.lower() for token in ("flood", "water", "rain", "climate")): return "water_climate_resilience_signal" return "no_climate_signal" - diff --git a/disha/brain/sectors/hse/__init__.py b/disha/brain/sectors/hse/__init__.py index f91fcd5f..27c1b62c 100644 --- a/disha/brain/sectors/hse/__init__.py +++ b/disha/brain/sectors/hse/__init__.py @@ -5,4 +5,3 @@ from .social_welfare import welfare_signal __all__ = ["education_signal", "health_signal", "welfare_signal"] - diff --git a/disha/brain/sectors/hse/education.py b/disha/brain/sectors/hse/education.py index c58ac830..0ef8d34e 100644 --- a/disha/brain/sectors/hse/education.py +++ b/disha/brain/sectors/hse/education.py @@ -2,5 +2,8 @@ def education_signal(text: str) -> str: - return "education_access_gap" if "education" in text.lower() or "school" in text.lower() else "not_applicable" - + return ( + "education_access_gap" + if "education" in text.lower() or "school" in text.lower() + else "not_applicable" + ) diff --git a/disha/brain/sectors/hse/health.py b/disha/brain/sectors/hse/health.py index 7ca88857..a073b9d8 100644 --- a/disha/brain/sectors/hse/health.py +++ b/disha/brain/sectors/hse/health.py @@ -3,4 +3,3 @@ def health_signal(text: str) -> str: return "health_service_gap" if "health" in text.lower() else "not_applicable" - diff --git a/disha/brain/sectors/hse/social_welfare.py b/disha/brain/sectors/hse/social_welfare.py index fed17164..04d74ad7 100644 --- a/disha/brain/sectors/hse/social_welfare.py +++ b/disha/brain/sectors/hse/social_welfare.py @@ -3,4 +3,3 @@ def welfare_signal(text: str) -> str: return "social_welfare_gap" if "welfare" in text.lower() else "not_applicable" - diff --git a/disha/brain/versions/common.py b/disha/brain/versions/common.py index e7d1cf51..ff7af5b3 100644 --- a/disha/brain/versions/common.py +++ b/disha/brain/versions/common.py @@ -13,4 +13,3 @@ class VersionOutput(BaseModel): def verification_note() -> str: return "Operational claims require source review; unsupported facts remain [VERIFY REQUIRED]." - diff --git a/disha/brain/versions/v1_6_geospatial_detection.py b/disha/brain/versions/v1_6_geospatial_detection.py index b9458928..ca274274 100644 --- a/disha/brain/versions/v1_6_geospatial_detection.py +++ b/disha/brain/versions/v1_6_geospatial_detection.py @@ -8,7 +8,11 @@ def run(input_text: str, coordinates: Coordinates | None = None) -> VersionOutpu tracker = TrackedObject(object_id="observed-signal") if coordinates: tracker.add(coordinates) - risk = public_safety_risk(coordinates, len(tracker.observations)) if coordinates else 0.2 + risk = ( + public_safety_risk(coordinates, len(tracker.observations)) + if coordinates + else 0.2 + ) return VersionOutput( version="1.6", title="Geospatial Detection Intelligence", @@ -19,4 +23,3 @@ def run(input_text: str, coordinates: Coordinates | None = None) -> VersionOutpu ), notes=[f"public_safety_risk={risk:.2f}", verification_note()], ) - diff --git a/disha/brain/versions/v2_6_sustainable_development.py b/disha/brain/versions/v2_6_sustainable_development.py index 5713f61e..15311996 100644 --- a/disha/brain/versions/v2_6_sustainable_development.py +++ b/disha/brain/versions/v2_6_sustainable_development.py @@ -11,6 +11,8 @@ def run(input_text: str) -> VersionOutput: title="Sustainable Development Geospatial Intelligence", signals=["development", "resilience", *signals], recommendation="Prioritize public assets by verifiable infrastructure, climate, and resource signals.", - notes=[f"resilience_score={resilience_score(signals):.2f}", verification_note()], + notes=[ + f"resilience_score={resilience_score(signals):.2f}", + verification_note(), + ], ) - diff --git a/disha/brain/versions/v3_6_physical_interface.py b/disha/brain/versions/v3_6_physical_interface.py index a97eee5a..91dcdb88 100644 --- a/disha/brain/versions/v3_6_physical_interface.py +++ b/disha/brain/versions/v3_6_physical_interface.py @@ -11,4 +11,3 @@ def run(input_text: str) -> VersionOutput: recommendation="Accept edge telemetry only from trusted devices, preserve local operator control, and keep sensor-to-brain flow auditable.", notes=[verification_note()], ) - diff --git a/disha/brain/versions/v4_6_hse.py b/disha/brain/versions/v4_6_hse.py index 2e54c9f8..afa2c16a 100644 --- a/disha/brain/versions/v4_6_hse.py +++ b/disha/brain/versions/v4_6_hse.py @@ -5,7 +5,11 @@ def run(input_text: str) -> VersionOutput: - signals = [health_signal(input_text), welfare_signal(input_text), education_signal(input_text)] + signals = [ + health_signal(input_text), + welfare_signal(input_text), + education_signal(input_text), + ] active = [signal for signal in signals if signal != "not_applicable"] return VersionOutput( version="4.6", @@ -14,4 +18,3 @@ def run(input_text: str) -> VersionOutput: recommendation="Produce a district-level service gap report with privacy protection and source-linked evidence.", notes=[verification_note()], ) - diff --git a/disha/brain/versions/v5_6_national_audit.py b/disha/brain/versions/v5_6_national_audit.py index 51651b29..759440e6 100644 --- a/disha/brain/versions/v5_6_national_audit.py +++ b/disha/brain/versions/v5_6_national_audit.py @@ -17,8 +17,16 @@ def run(input_text: str, source_links: list[str] | None = None) -> VersionOutput return VersionOutput( version="5.6", title="National Audit Intelligence", - signals=["national_audit", "constitutional_reference", "rti_record" if rti["mentions_rti"] else "open_data"], + signals=[ + "national_audit", + "constitutional_reference", + "rti_record" if rti["mentions_rti"] else "open_data", + ], recommendation="Prepare a governance accountability report with contradiction detection and source provenance.", - notes=[*constitutional, nyaya_summary("constitutional_reference", 0), str(data_audit), verification_note()], + notes=[ + *constitutional, + nyaya_summary("constitutional_reference", 0), + str(data_audit), + verification_note(), + ], ) - diff --git a/disha/brain/versions/v6_6_gap_closure.py b/disha/brain/versions/v6_6_gap_closure.py index b4263acc..37cff599 100644 --- a/disha/brain/versions/v6_6_gap_closure.py +++ b/disha/brain/versions/v6_6_gap_closure.py @@ -13,4 +13,3 @@ def run(input_text: str) -> VersionOutput: recommendation="Create a lawful corrective action plan gated by NFU, risk policy, and human approval where needed.", notes=[verification_note()], ) - diff --git a/disha/brain/vyuha/__init__.py b/disha/brain/vyuha/__init__.py index 6b418d42..003d819e 100644 --- a/disha/brain/vyuha/__init__.py +++ b/disha/brain/vyuha/__init__.py @@ -4,4 +4,3 @@ from .selector import select_vyuha __all__ = ["VyuhaRecommendation", "build_recommendation", "select_vyuha"] - diff --git a/disha/brain/vyuha/formations.py b/disha/brain/vyuha/formations.py index 951d23c9..eca67f9b 100644 --- a/disha/brain/vyuha/formations.py +++ b/disha/brain/vyuha/formations.py @@ -140,4 +140,3 @@ class VyuhaFormation(BaseModel): human_approval_required=True, ), ] - diff --git a/disha/brain/vyuha/playbook.py b/disha/brain/vyuha/playbook.py index fdb5d915..e04a0fce 100644 --- a/disha/brain/vyuha/playbook.py +++ b/disha/brain/vyuha/playbook.py @@ -19,4 +19,3 @@ def build_recommendation(formation: VyuhaFormation) -> VyuhaRecommendation: human_approval_required=formation.human_approval_required, recommendation=f"Use {formation.name}; fallback: {formation.fallback_behavior}.", ) - diff --git a/disha/brain/vyuha/selector.py b/disha/brain/vyuha/selector.py index de82f128..2a17f7e4 100644 --- a/disha/brain/vyuha/selector.py +++ b/disha/brain/vyuha/selector.py @@ -15,4 +15,3 @@ def select_vyuha(signals: list[str], risk_score: float = 0.0) -> VyuhaFormation: best = formation best_score = score return best - diff --git a/disha/brain/yudh/assessment.py b/disha/brain/yudh/assessment.py index 28b61e32..a95cb7c5 100644 --- a/disha/brain/yudh/assessment.py +++ b/disha/brain/yudh/assessment.py @@ -22,4 +22,3 @@ def assess_yudh(risk_score: float, evidence_class: str) -> YudhAssessment: lawful_frame="No-First-Use; owned or authorized environments only.", summary=f"{posture}; evidence class is {evidence_class}.", ) - diff --git a/disha/brain/yudh/gap_model.py b/disha/brain/yudh/gap_model.py index ac03ddae..60051d2b 100644 --- a/disha/brain/yudh/gap_model.py +++ b/disha/brain/yudh/gap_model.py @@ -14,4 +14,3 @@ def identify_gaps(text: str) -> list[str]: if token in normalized: gaps.append(label) return gaps or ["insufficient verified evidence"] - diff --git a/disha/brain/yudh/threat_model.py b/disha/brain/yudh/threat_model.py index b3e618f3..6bc331cb 100644 --- a/disha/brain/yudh/threat_model.py +++ b/disha/brain/yudh/threat_model.py @@ -4,11 +4,15 @@ def threat_score(text: str, evidence_class: str) -> float: normalized = text.lower() score = 0.15 - if any(token in normalized for token in ("explosive", "intrusion", "malware", "critical", "breach")): + if any( + token in normalized + for token in ("explosive", "intrusion", "malware", "critical", "breach") + ): score += 0.35 - if any(token in normalized for token in ("contradiction", "gap", "failure", "missing")): + if any( + token in normalized for token in ("contradiction", "gap", "failure", "missing") + ): score += 0.2 if evidence_class in {"official_record", "sensor_signal", "audit_record"}: score += 0.1 return min(score, 1.0) - diff --git a/tests/test_disha_brain_graph.py b/tests/test_disha_brain_graph.py index 4205e513..e88586d9 100644 --- a/tests/test_disha_brain_graph.py +++ b/tests/test_disha_brain_graph.py @@ -16,7 +16,9 @@ def test_evidence_classification_official_record() -> None: bundle = build_evidence_bundle( - ["Government notification hosted on gov.in"], "government_open_data", ["https://example.gov.in/order"] + ["Government notification hosted on gov.in"], + "government_open_data", + ["https://example.gov.in/order"], ) assert bundle.dominant_class == EvidenceClass.official_record assert bundle.confidence.value == "high" @@ -52,7 +54,10 @@ def test_nfu_forbidden_actions() -> None: def test_human_approval_escalates_ambiguous_action() -> None: graph = DishaAgenticGraph() result = graph.invoke( - GraphInput(input_text="Investigate public authority audit gap", requested_actions=["custom_action"]) + GraphInput( + input_text="Investigate public authority audit gap", + requested_actions=["custom_action"], + ) ) assert result.human_approval_required is True assert result.nfu_policy_status == "ambiguous_action_requires_approval" @@ -109,7 +114,9 @@ def test_hse_service_gap_output() -> None: ("gap mitigation needs Vyuha and Yudh", "audit_report", "6.6"), ], ) -def test_end_to_end_graph_invocation(text: str, source_type: str, expected_version: str) -> None: +def test_end_to_end_graph_invocation( + text: str, source_type: str, expected_version: str +) -> None: graph = DishaAgenticGraph() result = graph.invoke( GraphInput( From 84cafd4524a6f46c982ffdc7f7235bfb20dda9a4 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:40:00 +0530 Subject: [PATCH 03/26] Avoid eager FastAPI import in Brain package --- disha/brain/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/disha/brain/__init__.py b/disha/brain/__init__.py index e6558df9..2059fc9d 100644 --- a/disha/brain/__init__.py +++ b/disha/brain/__init__.py @@ -1,5 +1,15 @@ """DISHA Brain backend (unified platform core).""" -from .app import app +from __future__ import annotations + +from typing import Any __all__ = ["app"] + + +def __getattr__(name: str) -> Any: + if name == "app": + from .app import app + + return app + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From e504661a4eb57b41028ec83189eaf9dfd227b375 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 23:46:46 +0530 Subject: [PATCH 04/26] Harden Brain production runtime checks --- .github/workflows/disha-brain.yml | 5 ++++- disha/brain/Dockerfile | 5 ++++- tests/test_disha_brain_graph.py | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/disha-brain.yml b/.github/workflows/disha-brain.yml index cb34c1b3..3a1a1fb0 100644 --- a/.github/workflows/disha-brain.yml +++ b/.github/workflows/disha-brain.yml @@ -18,12 +18,15 @@ jobs: - name: Install test dependencies run: | python -m pip install --upgrade pip - python -m pip install pydantic pytest ruff bandit + python -m pip install -r disha/brain/requirements.txt + python -m pip install pytest ruff bandit - name: Lint production brain spine run: ruff check disha/brain tests - name: Check production brain formatting run: ruff format disha/brain tests --check - name: Security scan production brain spine run: bandit -q -r disha/brain + - name: Smoke import Brain API app + run: python -c "from disha.brain.app import app; assert app.title" - name: Run graph tests run: pytest tests/test_disha_brain_graph.py diff --git a/disha/brain/Dockerfile b/disha/brain/Dockerfile index 4b4c5aca..aeb270d7 100644 --- a/disha/brain/Dockerfile +++ b/disha/brain/Dockerfile @@ -14,6 +14,9 @@ RUN mkdir -p /data && chown -R appuser:appuser /data USER appuser +ENV DISHA_BRAIN_API_HOST=0.0.0.0 \ + DISHA_BRAIN_DB_PATH=/data/disha_brain.db + EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/v1/health').read(); print('ok')" || exit 1 -CMD ["uvicorn", "disha.brain:app", "--host", "0.0.0.0", "--port", "8080"] +CMD ["python", "-m", "disha.brain"] diff --git a/tests/test_disha_brain_graph.py b/tests/test_disha_brain_graph.py index e88586d9..9ce8cef8 100644 --- a/tests/test_disha_brain_graph.py +++ b/tests/test_disha_brain_graph.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + import pytest from disha.brain.audit import AuditEvent, AuditLedger @@ -14,6 +16,11 @@ from disha.brain.vyuha import select_vyuha +def test_brain_package_import_is_lazy() -> None: + assert "disha.brain.app" not in sys.modules + assert "fastapi" not in sys.modules + + def test_evidence_classification_official_record() -> None: bundle = build_evidence_bundle( ["Government notification hosted on gov.in"], From 6faabbe459f2c43dac5c9cf2ce3e7ed67ac02f11 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 12:12:39 +0530 Subject: [PATCH 05/26] Document production readiness and clean env template --- .env.example | 59 +++++++++++++++++------------------- README.md | 1 + docs/PRODUCTION_READINESS.md | 52 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 docs/PRODUCTION_READINESS.md diff --git a/.env.example b/.env.example index 1965184c..9345177a 100644 --- a/.env.example +++ b/.env.example @@ -1,54 +1,51 @@ -# ───────────────────────────────────────────── -# DISHA AGI Platform — Environment Variables -# ───────────────────────────────────────────── -# Copy this file to .env and fill in your values. - -# ── Core CLI (Layer 1) ────────────────────── -ANTHROPIC_API_KEY=sk-ant-... # Required for Anthropic Claude API access - -# ── AI Platform Backend (Layer 2) ─────────── -OPENAI_API_KEY=sk-... # OpenAI API key (for LangChain agents) -NEO4J_URI=bolt://neo4j:7687 # Neo4j connection URI (for Docker) -NEO4J_USER=neo4j # Neo4j username -NEO4J_PASSWORD=disha-neo4j-pass # Neo4j password -POSTGRES_PASSWORD=postgres # Postgres password -CHROMA_HOST=chromadb # ChromaDB host -CHROMA_PORT=8000 # ChromaDB port -SECRET_KEY=disha-secret-key-change-me # JWT secret +# DISHA environment variables +# Copy this file to .env and replace every <...> placeholder before deployment. +# Do not commit real keys, tokens, passwords, private endpoints, or production secrets. + +# Core CLI +ANTHROPIC_API_KEY= + +# AI Platform Backend +OPENAI_API_KEY= +NEO4J_URI=bolt://neo4j:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD= +POSTGRES_PASSWORD= +CHROMA_HOST=chromadb +CHROMA_PORT=8000 +SECRET_KEY= CORS_ORIGINS=http://localhost:3000,http://localhost:3001 -# ── Cyber Defense (Layer 3) ───────────────── -COWRIE_SSH_PORT=2222 # External SSH port for cowrie -OPENCANARY_HTTP_PORT=8081 # External HTTP port for opencanary -ELASTIC_PORT=9200 # Elasticsearch port +# Cyber Defense +COWRIE_SSH_PORT=2222 +OPENCANARY_HTTP_PORT=8081 +ELASTIC_PORT=9200 -# ── Physics Engine ────────────────────────── -NASA_API_KEY=DEMO_KEY # NASA API key for astronomy data -PHYSICS_API_PORT=8002 # Port for physics backend +# Physics Engine +NASA_API_KEY= +PHYSICS_API_PORT=8002 -# ── Feature Flags (CLI) ──────────────────── -# Set to '1' or 'true' to enable +# Optional CLI feature flags. Set to 1 or true to enable. # CLAUDE_CODE_BRIDGE_MODE=0 # CLAUDE_CODE_DAEMON=0 # CLAUDE_CODE_VOICE_MODE=0 # CLAUDE_CODE_BG_SESSIONS=0 -# ── DISHA Brain (Unified Backend + Edge Agent) ───────────────────── -# Brain backend (FastAPI) +# DISHA Brain backend DISHA_BRAIN_API_HOST=127.0.0.1 DISHA_BRAIN_API_PORT=8080 -DISHA_BRAIN_API_TOKEN=change-me +DISHA_BRAIN_API_TOKEN= DISHA_BRAIN_DB_PATH=./data/disha_brain.db DISHA_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 DISHA_WORKSPACE_ROOT=. -# Edge agent (telemetry sender) +# Edge agent DISHA_BRAIN_BACKEND_URL=http://127.0.0.1:8080 DISHA_BRAIN_DEVICE_ID=desktop DISHA_BRAIN_USER_ID=local-user DISHA_BRAIN_AGENT_INTERVAL=15 -# Optional module endpoints (DISHA Brain adapters) +# Optional module endpoints used by DISHA Brain adapters DISHA_AI_PLATFORM_URL=http://127.0.0.1:8000/api/v1 DISHA_MCP_HTTP_URL=http://127.0.0.1:4000 DISHA_PHYSICS_URL=http://127.0.0.1:8002 diff --git a/README.md b/README.md index e476ff82..1c7f0484 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ No-First-Use is enforced in code under `disha/brain/policy/no_first_use.py`. Off - [No-First-Use](docs/NO_FIRST_USE.md) - [Evidence Model](docs/EVIDENCE_MODEL.md) - [Trust Model](docs/TRUST_MODEL.md) +- [Production Readiness](docs/PRODUCTION_READINESS.md) - [Demos](docs/DEMOS.md) - [Roadmap](docs/ROADMAP.md) diff --git a/docs/PRODUCTION_READINESS.md b/docs/PRODUCTION_READINESS.md new file mode 100644 index 00000000..c55e9c04 --- /dev/null +++ b/docs/PRODUCTION_READINESS.md @@ -0,0 +1,52 @@ +# Production Readiness + +This document records the production posture of the current DISHA Brain MVP. It is intentionally factual: it lists what is verified in this repository, what must be configured at deployment time, and what should not be claimed without additional evidence. + +## Current Verified State + +- The PR branch `codex/evidence-first-disha-spine` passed the GitHub checks for Brain tests, TypeScript tests, TypeScript lint, Ruff lint and formatting, mypy, Bandit, secret scanning, dependency audit, SBOM generation, and CodeQL. +- The Brain graph test suite covers 23 checks across lazy import behavior, evidence classification, six-version routing, No-First-Use policy, human approval, Vyuha selection, audit hash-chain behavior, memory updates, geospatial validation, HSE signals, national audit signals, and end-to-end graph invocation. +- `disha/brain` can be imported as a core graph package without requiring FastAPI. +- The FastAPI app import is covered by the Brain workflow after installing `disha/brain/requirements.txt`. +- The Docker runtime writes Brain state to `/data/disha_brain.db`, a writable container data path. + +## Deployment Requirements + +- Copy `.env.example` to `.env` and replace every `<...>` placeholder with a real deployment secret or endpoint. +- Set `DISHA_BRAIN_API_TOKEN` for every environment that exposes protected Brain APIs. +- Keep production CORS origins explicit. Do not use wildcard browser origins for deployed control planes. +- Mount durable storage for the Brain SQLite path, or replace `SQLiteStore` with a managed store before using multi-instance deployments. +- Treat `demos/` payloads as examples only. They are not external proof or official records. + +## Runtime Boundaries + +- DISHA Brain is defensive and evidence-first. It blocks offensive, retaliatory, destructive, unauthorized, and self-propagating actions through No-First-Use policy. +- Ambiguous actions require human approval. +- Unsupported public claims must remain marked as `[VERIFY REQUIRED]` until source material is added to the repository or verified through a production connector. +- References to thenitishkr.in and the DISHA Intelligence books remain `[VERIFY REQUIRED]` in this repository until source material or bibliographic metadata is added. + +## Local Verification Commands + +```bash +python -m pytest tests/test_disha_brain_graph.py -q +python -m ruff check disha/brain tests +python -m ruff format disha/brain tests --check +python -m bandit -q -r disha/brain +python -c "from disha.brain.app import app; assert app.title" +``` + +## Release Gate + +Before calling a branch production-ready: + +1. All GitHub checks must pass on the exact commit being released. +2. `.env` must be supplied by the deployer and must not be committed. +3. External data claims must be backed by repository material, audited connectors, or marked `[VERIFY REQUIRED]`. +4. Any legacy module promoted into the production path must pass security review and receive focused tests. +5. Operators must know whether the deployment is single-instance SQLite or managed multi-instance storage. + +## Known Limits + +- This MVP does not claim government approval, legal certification, or independent third-party audit. +- The repository contains legacy and experimental modules outside the Brain spine; they are not all production surfaces. +- Expert-reviewed datasets for HSE, geospatial resilience, governance audit, and public-sector scoring are still required before domain outputs should be treated as authoritative. From 366a8131e3125ef5d221cf957076eb8a54883154 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 01:14:29 +0530 Subject: [PATCH 06/26] Add institutional DISHA intelligence dashboard --- web/app/dashboard/dashboard.module.css | 373 +++++++++++++++++++++++++ web/app/dashboard/page.tsx | 261 +++++++++++++++++ web/app/layout.tsx | 15 + web/next.config.js | 4 + 4 files changed, 653 insertions(+) create mode 100644 web/app/dashboard/dashboard.module.css create mode 100644 web/app/dashboard/page.tsx create mode 100644 web/app/layout.tsx diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css new file mode 100644 index 00000000..3d5054f3 --- /dev/null +++ b/web/app/dashboard/dashboard.module.css @@ -0,0 +1,373 @@ +.page { + min-height: 100vh; + background: + linear-gradient(180deg, rgba(247, 213, 111, 0.16), transparent 260px), + #fbf7ed; + color: #1d1b16; + font-family: + Arial, + Helvetica, + sans-serif; +} + +.shell { + width: min(1440px, calc(100% - 32px)); + margin: 0 auto; + padding: 28px 0 44px; +} + +.header { + display: grid; + grid-template-columns: minmax(0, 1fr) 280px; + gap: 24px; + align-items: stretch; + border: 2px solid #1d1b16; + background: #fffdf8; + padding: 28px; +} + +.eyebrow, +.kicker { + margin: 0 0 10px; + color: #6b614c; + font-size: 0.77rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.header h1 { + max-width: 920px; + margin: 0; + font-family: Georgia, "Times New Roman", serif; + font-size: clamp(2.25rem, 5vw, 5.4rem); + line-height: 0.95; + letter-spacing: 0; +} + +.lede { + max-width: 870px; + margin: 22px 0 0; + color: #373229; + font-size: 1.06rem; + line-height: 1.65; +} + +.clearanceBox { + display: flex; + min-height: 100%; + flex-direction: column; + justify-content: space-between; + border-left: 2px solid #1d1b16; + background: #f7d56f; + padding: 20px; +} + +.clearanceBox span, +.clearanceBox small { + font-size: 0.82rem; + font-weight: 800; + text-transform: uppercase; +} + +.clearanceBox strong { + margin: 34px 0; + font-family: Georgia, "Times New Roman", serif; + font-size: 2.1rem; + line-height: 1; +} + +.statusGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin: 16px 0; +} + +.statusCard, +.capabilityCard, +.mainPanel, +.sidePanel, +.versionSection, +.checkPanel, +.dossierPanel { + border: 1.5px solid #1d1b16; + background: #fffdf8; +} + +.statusCard { + display: grid; + grid-template-columns: auto 1fr; + gap: 10px 12px; + align-items: center; + min-height: 112px; + padding: 18px; +} + +.statusCard svg, +.capabilityCard svg, +.panelHeader svg { + width: 24px; + height: 24px; + stroke-width: 1.8; +} + +.statusCard span { + color: #6b614c; + font-size: 0.78rem; + font-weight: 800; + text-transform: uppercase; +} + +.statusCard strong { + grid-column: 1 / -1; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.55rem; +} + +.commandLayout { + display: grid; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 16px; +} + +.mainPanel, +.sidePanel, +.versionSection, +.checkPanel, +.dossierPanel { + padding: 22px; +} + +.panelHeader { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: flex-start; + margin-bottom: 18px; +} + +.panelHeader h2, +.sidePanel h2, +.versionCard h3, +.capabilityCard h3 { + margin: 0; + font-family: Georgia, "Times New Roman", serif; + line-height: 1.08; + letter-spacing: 0; +} + +.panelHeader h2, +.sidePanel h2 { + font-size: clamp(1.55rem, 3vw, 2.4rem); +} + +.graph { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; +} + +.graphStep { + position: relative; + min-height: 86px; + border: 1.5px solid #1d1b16; + background: #f6eddb; + padding: 14px; +} + +.graphStep:nth-child(1), +.graphStep:nth-child(5), +.graphStep:nth-child(9), +.graphStep:nth-child(13) { + background: #f7d56f; +} + +.graphStep span { + display: block; + color: #6b614c; + font-size: 0.72rem; + font-weight: 800; +} + +.graphStep strong { + display: block; + margin-top: 18px; + font-size: 1rem; +} + +.sidePanel { + background: #f6eddb; +} + +.truthList { + display: grid; + gap: 14px; + margin: 22px 0 0; + padding: 0; + list-style: none; +} + +.truthList li { + border-top: 1px solid rgba(29, 27, 22, 0.34); + padding-top: 13px; + line-height: 1.5; +} + +.capabilityGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin: 16px 0; +} + +.capabilityCard { + min-height: 214px; + padding: 20px; +} + +.capabilityCard h3 { + margin-top: 32px; + font-size: 1.35rem; +} + +.capabilityCard p, +.versionCard p { + color: #4a4234; + line-height: 1.55; +} + +.versionGrid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; +} + +.versionCard { + display: flex; + min-height: 196px; + flex-direction: column; + border: 1.5px solid #1d1b16; + background: #fffdf8; + padding: 16px; +} + +.versionCard span { + width: max-content; + border: 1px solid #1d1b16; + background: #f7d56f; + padding: 4px 8px; + font-weight: 900; +} + +.versionCard h3 { + margin-top: 18px; + font-size: 1.2rem; +} + +.versionCard strong { + margin-top: auto; + color: #254229; + font-size: 0.78rem; + text-transform: uppercase; +} + +.bottomGrid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 0.76fr); + gap: 16px; + margin-top: 16px; +} + +.checkList { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.checkList div { + display: flex; + gap: 10px; + align-items: center; + border-top: 1px solid rgba(29, 27, 22, 0.35); + padding-top: 12px; +} + +.checkList span { + width: 10px; + height: 10px; + flex: 0 0 auto; + border: 1px solid #1d1b16; + background: #f7d56f; +} + +.checkList p { + margin: 0; + line-height: 1.35; +} + +.dossierList { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.dossierList span { + border: 1px solid #1d1b16; + background: #f6eddb; + padding: 10px 12px; + font-size: 0.92rem; +} + +@media (max-width: 1120px) { + .header, + .commandLayout, + .bottomGrid { + grid-template-columns: 1fr; + } + + .clearanceBox { + border-top: 2px solid #1d1b16; + border-left: 0; + } + + .statusGrid, + .capabilityGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .graph, + .versionGrid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 680px) { + .shell { + width: min(100% - 20px, 1440px); + padding-top: 10px; + } + + .header, + .mainPanel, + .sidePanel, + .versionSection, + .checkPanel, + .dossierPanel { + padding: 16px; + } + + .statusGrid, + .capabilityGrid, + .graph, + .versionGrid, + .checkList { + grid-template-columns: 1fr; + } + + .statusCard, + .capabilityCard, + .versionCard { + min-height: auto; + } +} diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx new file mode 100644 index 00000000..8767ebeb --- /dev/null +++ b/web/app/dashboard/page.tsx @@ -0,0 +1,261 @@ +import { + Activity, + ClipboardCheck, + Database, + Eye, + FileText, + GitBranch, + Landmark, + Layers, + Lock, + Map, + Scale, + Shield, + type LucideIcon, +} from "lucide-react"; + +import styles from "./dashboard.module.css"; + +const versions = [ + { + id: "1.6", + name: "Geospatial Detection", + signal: "Coordinates, sensors, object tracking", + status: "Tested", + }, + { + id: "2.6", + name: "Sustainable Development", + signal: "Infrastructure, climate, resource risk", + status: "Tested", + }, + { + id: "3.6", + name: "Physical Interface", + signal: "Edge telemetry and device state", + status: "Tested", + }, + { + id: "4.6", + name: "HSE Intelligence", + signal: "Health, welfare, education gaps", + status: "Tested", + }, + { + id: "5.6", + name: "National Audit", + signal: "RTI, open data, public authority review", + status: "Tested", + }, + { + id: "6.6", + name: "Gap Closure", + signal: "Yudh assessment and Vyuha selection", + status: "Tested", + }, +]; + +const checks = [ + "Brain graph tests", + "Ruff lint and format", + "mypy strict", + "Bandit SAST", + "Secret scanning", + "Dependency audit", + "SBOM generation", + "CodeQL actions, Python, JS/TS", +]; + +const graphNodes = [ + "Intake", + "Evidence", + "Context", + "Reasoning", + "Router", + "Action", + "Yudh", + "Vyuha", + "NFU Policy", + "Human Review", + "Audit", + "Memory", + "Result", +]; + +const capabilities = [ + { + title: "Evidence Discipline", + body: "Every result carries source lists, confidence, evidence class, and explicit verification gaps.", + icon: FileText, + }, + { + title: "Policy Guard", + body: "No-First-Use blocks retaliatory, destructive, unauthorized, and self-propagating actions.", + icon: Shield, + }, + { + title: "Audit Memory", + body: "Graph runs create audit events and update working plus episodic memory for continuity.", + icon: Database, + }, + { + title: "Civic Intelligence", + body: "The version ladder covers geospatial, development, HSE, public audit, and gap closure missions.", + icon: Landmark, + }, +]; + +const dossiers = [ + "RTI contradiction review", + "District health service gap", + "Bridge flood impact assessment", + "Edge device telemetry review", + "Geospatial perimeter signal", + "Gap closure corrective action", +]; + +export default function DashboardPage() { + return ( +
+
+
+
+

DISHA Strategic Intelligence Board

+

Evidence-first operational command view

+

+ A local dashboard for the DISHA Brain spine: versioned intelligence, + defensive policy, audit memory, and production readiness without + exposing implementation code. +

+
+
+ PR #73 + All checks passed + Production-readiness branch +
+
+ +
+ + + + +
+ +
+
+
+
+

Agentic graph

+

Decision flow under evidence control

+
+
+
+ {graphNodes.map((node, index) => ( +
+ {String(index + 1).padStart(2, "0")} + {node} +
+ ))} +
+
+ + +
+ +
+ {capabilities.map((item) => ( +
+
+ ))} +
+ +
+
+
+

Version ladder

+

Six intelligence surfaces, one audited spine

+
+
+
+ {versions.map((version) => ( +
+ {version.id} +

{version.name}

+

{version.signal}

+ {version.status} +
+ ))} +
+
+ +
+
+
+
+

Readiness ledger

+

Green gates

+
+
+
+ {checks.map((check) => ( +
+ +

{check}

+
+ ))} +
+
+ +
+
+
+

Demo dossiers

+

Operational scenarios

+
+
+
+ {dossiers.map((dossier) => ( + {dossier} + ))} +
+
+
+
+
+ ); +} + +function StatusCard({ + icon: Icon, + label, + value, +}: { + icon: LucideIcon; + label: string; + value: string; +}) { + return ( +
+
+ ); +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 00000000..9bf151c4 --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "DISHA Intelligence Board", + description: "Evidence-first operational dashboard for DISHA Brain.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/web/next.config.js b/web/next.config.js index 9dccd288..85b2b3de 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -4,6 +4,10 @@ const nextConfig = { output: "standalone", // Keep behavior deterministic in production images. poweredByHeader: false, + // This app lives inside the monorepo; keep Turbopack rooted at web/. + turbopack: { + root: __dirname, + }, }; module.exports = nextConfig; From 27c19ef4300161ae8ae27a618d4ccc0ecbabc4a9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 01:30:07 +0530 Subject: [PATCH 07/26] Refine dashboard as intelligence directorate board --- web/app/dashboard/dashboard.module.css | 258 ++++++++++++++++++- web/app/dashboard/page.tsx | 326 ++++++++++++++++--------- 2 files changed, 461 insertions(+), 123 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index 3d5054f3..2758911f 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -16,6 +16,83 @@ padding: 28px 0 44px; } +.departmentFrame { + display: grid; + grid-template-columns: 188px minmax(0, 1fr); + gap: 16px; +} + +.directorateRail { + position: sticky; + top: 20px; + align-self: start; + min-height: calc(100vh - 56px); + border: 2px solid #1d1b16; + background: #1d1b16; + color: #fffdf8; + padding: 16px; +} + +.sealMark { + display: grid; + gap: 10px; + justify-items: start; + border-bottom: 1px solid rgba(255, 253, 248, 0.32); + padding-bottom: 18px; +} + +.sealMark svg { + width: 34px; + height: 34px; +} + +.sealMark span { + font-family: Georgia, "Times New Roman", serif; + font-size: 1.5rem; + font-weight: 800; +} + +.directorateRail nav { + display: grid; + gap: 8px; + margin: 28px 0; +} + +.directorateRail a { + border: 1px solid rgba(255, 253, 248, 0.32); + color: #fffdf8; + padding: 10px; + font-size: 0.8rem; + font-weight: 800; + text-decoration: none; + text-transform: uppercase; +} + +.railNote { + margin-top: auto; + border-top: 1px solid rgba(255, 253, 248, 0.32); + padding-top: 18px; +} + +.railNote span { + display: block; + color: #f7d56f; + font-size: 0.76rem; + font-weight: 800; + text-transform: uppercase; +} + +.railNote strong { + display: block; + margin-top: 8px; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.45rem; +} + +.board { + min-width: 0; +} + .header { display: grid; grid-template-columns: minmax(0, 1fr) 280px; @@ -87,7 +164,9 @@ .statusCard, .capabilityCard, .mainPanel, -.sidePanel, +.watchPanel, +.sourcePanel, +.situationPanel, .versionSection, .checkPanel, .dossierPanel { @@ -125,14 +204,18 @@ font-size: 1.55rem; } +.situationGrid, .commandLayout { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; + margin-bottom: 16px; } .mainPanel, -.sidePanel, +.watchPanel, +.sourcePanel, +.situationPanel, .versionSection, .checkPanel, .dossierPanel { @@ -148,7 +231,7 @@ } .panelHeader h2, -.sidePanel h2, +.watchPanel h2, .versionCard h3, .capabilityCard h3 { margin: 0; @@ -158,10 +241,85 @@ } .panelHeader h2, -.sidePanel h2 { +.watchPanel h2 { font-size: clamp(1.55rem, 3vw, 2.4rem); } +.situationPanel { + background: #fffdf8; +} + +.mapBoard { + min-height: 340px; + border: 1.5px solid #1d1b16; + background: + linear-gradient(rgba(29, 27, 22, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(29, 27, 22, 0.08) 1px, transparent 1px), + #f6eddb; + background-size: 32px 32px; + padding: 18px; +} + +.mapGrid { + position: relative; + min-height: 302px; + border: 1px solid rgba(29, 27, 22, 0.55); +} + +.mapGrid::before, +.mapGrid::after { + position: absolute; + content: ""; + inset: 50% 8%; + border-top: 1.5px solid rgba(29, 27, 22, 0.55); +} + +.mapGrid::after { + inset: 8% 50%; + border-top: 0; + border-left: 1.5px solid rgba(29, 27, 22, 0.55); +} + +.mapGrid span { + position: absolute; + border: 1.5px solid #1d1b16; + background: #fffdf8; + padding: 10px 12px; + font-size: 0.85rem; + font-weight: 800; +} + +.zoneNorth { + top: 18px; + left: 50%; + transform: translateX(-50%); +} + +.zoneWest { + top: 48%; + left: 18px; + transform: translateY(-50%); +} + +.zoneCenter { + top: 50%; + left: 50%; + background: #f7d56f !important; + transform: translate(-50%, -50%); +} + +.zoneEast { + top: 48%; + right: 18px; + transform: translateY(-50%); +} + +.zoneSouth { + bottom: 18px; + left: 50%; + transform: translateX(-50%); +} + .graph { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -196,7 +354,7 @@ font-size: 1rem; } -.sidePanel { +.watchPanel { background: #f6eddb; } @@ -221,6 +379,44 @@ margin: 16px 0; } +.sourcePanel { + background: #f6eddb; +} + +.sourceTable { + display: grid; + gap: 10px; +} + +.sourceTable div { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px 12px; + border-top: 1px solid rgba(29, 27, 22, 0.35); + padding-top: 12px; +} + +.sourceTable strong { + font-family: Georgia, "Times New Roman", serif; +} + +.sourceTable span { + border: 1px solid #1d1b16; + background: #f7d56f; + padding: 4px 8px; + font-size: 0.76rem; + font-weight: 900; + text-transform: uppercase; +} + +.sourceTable p { + grid-column: 1 / -1; + margin: 0; + color: #4a4234; + font-size: 0.92rem; + line-height: 1.45; +} + .capabilityCard { min-height: 214px; padding: 20px; @@ -307,20 +503,52 @@ } .dossierList { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } -.dossierList span { +.dossierList article { border: 1px solid #1d1b16; background: #f6eddb; - padding: 10px 12px; + padding: 12px; +} + +.dossierList span { + color: #6b614c; + font-size: 0.72rem; + font-weight: 900; + text-transform: uppercase; +} + +.dossierList strong { + display: block; + margin-top: 8px; + font-family: Georgia, "Times New Roman", serif; +} + +.dossierList p { + margin: 8px 0 0; + color: #4a4234; font-size: 0.92rem; } @media (max-width: 1120px) { + .departmentFrame { + grid-template-columns: 1fr; + } + + .directorateRail { + position: static; + min-height: auto; + } + + .directorateRail nav { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .header, + .situationGrid, .commandLayout, .bottomGrid { grid-template-columns: 1fr; @@ -350,7 +578,9 @@ .header, .mainPanel, - .sidePanel, + .watchPanel, + .sourcePanel, + .situationPanel, .versionSection, .checkPanel, .dossierPanel { @@ -361,10 +591,16 @@ .capabilityGrid, .graph, .versionGrid, - .checkList { + .checkList, + .dossierList, + .directorateRail nav { grid-template-columns: 1fr; } + .mapBoard { + min-height: 300px; + } + .statusCard, .capabilityCard, .versionCard { diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 8767ebeb..6ee0b05b 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -1,5 +1,7 @@ import { Activity, + BookOpenCheck, + Building2, ClipboardCheck, Database, Eye, @@ -9,6 +11,7 @@ import { Layers, Lock, Map, + Radar, Scale, Shield, type LucideIcon, @@ -106,137 +109,236 @@ const capabilities = [ ]; const dossiers = [ - "RTI contradiction review", - "District health service gap", - "Bridge flood impact assessment", - "Edge device telemetry review", - "Geospatial perimeter signal", - "Gap closure corrective action", + { + code: "NAT-AUDIT-05", + title: "RTI contradiction review", + lane: "Public authority audit", + }, + { + code: "HSE-04", + title: "District health service gap", + lane: "Service delivery", + }, + { + code: "RES-02", + title: "Bridge flood impact assessment", + lane: "Infrastructure resilience", + }, + { + code: "EDGE-03", + title: "Edge device telemetry review", + lane: "Physical interface", + }, + { + code: "GEO-01", + title: "Geospatial perimeter signal", + lane: "Sensor evidence", + }, + { + code: "CLOSURE-06", + title: "Gap closure corrective action", + lane: "Yudh/Vyuha routing", + }, +]; + +const sources = [ + { label: "Official record", level: "High", note: "Source link required" }, + { label: "RTI record", level: "High", note: "Contradiction review" }, + { label: "Sensor signal", level: "Medium", note: "Needs chain context" }, + { label: "Operator note", level: "Low", note: "Human claim only" }, ]; export default function DashboardPage() { return (
-
-
-

DISHA Strategic Intelligence Board

-

Evidence-first operational command view

-

- A local dashboard for the DISHA Brain spine: versioned intelligence, - defensive policy, audit memory, and production readiness without - exposing implementation code. -

-
-
- PR #73 - All checks passed - Production-readiness branch -
-
- -
- - - - -
+
+ -
-
-
+
+
-

Agentic graph

-

Decision flow under evidence control

+

DISHA Intelligence Directorate

+

National evidence operations dashboard

+

+ A local situation board for DISHA Brain: source discipline, + version routing, lawful action boundaries, audit memory, and + production readiness without exposing implementation code. +

-
-
- {graphNodes.map((node, index) => ( -
- {String(index + 1).padStart(2, "0")} - {node} -
- ))} -
-
+
+ Branch PR #73 + All checks passed + Production-readiness spine +
+ - -
+
+ + + + +
-
- {capabilities.map((item) => ( -
-
- ))} -
+
+
+
+
+

Situation room

+

Evidence posture by operational lane

+
+
+
+
+ National Audit + Geospatial + DISHA Brain + HSE + Gap Closure +
+
+
-
-
-
-

Version ladder

-

Six intelligence surfaces, one audited spine

-
-
-
- {versions.map((version) => ( -
- {version.id} -

{version.name}

-

{version.signal}

- {version.status} -
- ))} -
-
+ +
-
-
-
-
-

Readiness ledger

-

Green gates

+
+
+
+
+

Agentic chain

+

Decision flow under evidence control

+
+
+
+ {graphNodes.map((node, index) => ( +
+ {String(index + 1).padStart(2, "0")} + {node} +
+ ))} +
-
-
- {checks.map((check) => ( -
- -

{check}

+ +
+
+
+

Source desk

+

Reliability matrix

+
+
+
+ {sources.map((source) => ( +
+ {source.label} + {source.level} +

{source.note}

+
+ ))} +
+
+
+ +
+ {capabilities.map((item) => ( +
+
))} -
- +
-
-
-
-

Demo dossiers

-

Operational scenarios

+
+
+
+

Version ladder

+

Six intelligence surfaces, one audited spine

+
+
-
-
- {dossiers.map((dossier) => ( - {dossier} - ))} -
+
+ {versions.map((version) => ( +
+ {version.id} +

{version.name}

+

{version.signal}

+ {version.status} +
+ ))} +
+ + +
+
+
+
+

Readiness ledger

+

Green gates

+
+
+
+ {checks.map((check) => ( +
+ +

{check}

+
+ ))} +
+
+ +
+
+
+

Mission docket

+

Operational scenarios

+
+
+
+ {dossiers.map((dossier) => ( +
+ {dossier.code} + {dossier.title} +

{dossier.lane}

+
+ ))} +
+
+
- +
); From 9686ef62b9b9364afc33b220b67441fcb98e82b9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 06:01:21 +0530 Subject: [PATCH 08/26] Add privacy-preserving India observatory dashboard --- web/app/dashboard/dashboard.module.css | 113 ++++++++++++++++++++++++- web/app/dashboard/page.tsx | 101 ++++++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index 2758911f..c567c215 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -167,6 +167,9 @@ .watchPanel, .sourcePanel, .situationPanel, +.nationalPanel, +.livePanel, +.guardrailPanel, .versionSection, .checkPanel, .dossierPanel { @@ -205,7 +208,8 @@ } .situationGrid, -.commandLayout { +.commandLayout, +.nationalGrid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 16px; @@ -216,6 +220,9 @@ .watchPanel, .sourcePanel, .situationPanel, +.nationalPanel, +.livePanel, +.guardrailPanel, .versionSection, .checkPanel, .dossierPanel { @@ -249,6 +256,81 @@ background: #fffdf8; } +.nationalPanel { + background: #fffdf8; +} + +.livePanel, +.guardrailPanel { + background: #f6eddb; +} + +.zoneLedger { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.zoneLedger article { + display: flex; + min-height: 174px; + flex-direction: column; + border: 1.5px solid #1d1b16; + background: #fffdf8; + padding: 14px; +} + +.zoneLedger span { + width: max-content; + border: 1px solid #1d1b16; + background: #f7d56f; + padding: 4px 8px; + font-size: 0.76rem; + font-weight: 900; + text-transform: uppercase; +} + +.zoneLedger strong { + margin-top: 18px; + font-family: Georgia, "Times New Roman", serif; + font-size: 1rem; + line-height: 1.25; +} + +.zoneLedger p { + margin: auto 0 0; + color: #4a4234; + font-size: 0.86rem; + line-height: 1.35; +} + +.liveSignals { + display: grid; + gap: 10px; +} + +.liveSignals div { + display: flex; + gap: 10px; + align-items: center; + border-top: 1px solid rgba(29, 27, 22, 0.35); + padding-top: 12px; +} + +.liveSignals span { + width: 11px; + height: 11px; + flex: 0 0 auto; + border: 1px solid #1d1b16; + background: #f7d56f; + box-shadow: 0 0 0 4px rgba(247, 213, 111, 0.24); +} + +.liveSignals p { + margin: 0; + line-height: 1.35; +} + .mapBoard { min-height: 340px; border: 1.5px solid #1d1b16; @@ -433,6 +515,25 @@ line-height: 1.55; } +.guardrailPanel { + margin-bottom: 16px; +} + +.guardrailList { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.guardrailList span { + border: 1.5px solid #1d1b16; + background: #fffdf8; + padding: 14px; + font-size: 0.92rem; + font-weight: 800; + line-height: 1.35; +} + .versionGrid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -550,6 +651,7 @@ .header, .situationGrid, .commandLayout, + .nationalGrid, .bottomGrid { grid-template-columns: 1fr; } @@ -565,7 +667,9 @@ } .graph, - .versionGrid { + .versionGrid, + .zoneLedger, + .guardrailList { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @@ -581,6 +685,9 @@ .watchPanel, .sourcePanel, .situationPanel, + .nationalPanel, + .livePanel, + .guardrailPanel, .versionSection, .checkPanel, .dossierPanel { @@ -591,6 +698,8 @@ .capabilityGrid, .graph, .versionGrid, + .zoneLedger, + .guardrailList, .checkList, .dossierList, .directorateRail nav { diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 6ee0b05b..ddfc55b9 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -7,10 +7,12 @@ import { Eye, FileText, GitBranch, + Globe2, Landmark, Layers, Lock, Map, + Network, Radar, Scale, Shield, @@ -148,6 +150,51 @@ const sources = [ { label: "Operator note", level: "Low", note: "Human claim only" }, ]; +const nationalZones = [ + { + region: "North", + focus: "Border-state resilience and public infrastructure continuity", + lane: "Geospatial + audit", + }, + { + region: "East", + focus: "Flood, bridge, health, and school-access stress signals", + lane: "Development + HSE", + }, + { + region: "West", + focus: "Urban services, port-adjacent infrastructure, and open-data review", + lane: "Audit + resilience", + }, + { + region: "South", + focus: "Water, climate, edge telemetry, and district service continuity", + lane: "SETU/VARUNA + edge", + }, + { + region: "Central", + focus: "Gap closure routing across public service and resource evidence", + lane: "Yudh/Vyuha", + }, +]; + +const liveSignals = [ + "Open-data evidence stream", + "RTI contradiction queue", + "District service-gap watch", + "Infrastructure resilience lane", + "Human approval queue", + "Audit hash-chain continuity", +]; + +const privacyGuardrails = [ + "No individual-level tracking", + "No biometric or face surveillance", + "No hidden collection", + "Only aggregate, open, consented, or authorized signals", + "Every claim carries evidence status", +]; + export default function DashboardPage() { return (
@@ -198,6 +245,45 @@ export default function DashboardPage() { +
+
+
+
+

India resilience observatory

+

National-scale awareness without citizen surveillance

+
+
+
+ {nationalZones.map((zone) => ( +
+ {zone.region} + {zone.focus} +

{zone.lane}

+
+ ))} +
+
+ + +
+
@@ -279,6 +365,21 @@ export default function DashboardPage() { ))}
+
+
+
+

Constitutional guardrails

+

What this system will not do

+
+
+
+ {privacyGuardrails.map((guardrail) => ( + {guardrail} + ))} +
+
+
From ec7155b2d64e5d40f9a4cd2df9cdc0fcb3f76df7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 06:14:59 +0530 Subject: [PATCH 09/26] Rebuild dashboard around DISHA operational analytics --- web/app/dashboard/dashboard.module.css | 783 ++++++++----------------- web/app/dashboard/page.tsx | 638 ++++++++------------ 2 files changed, 482 insertions(+), 939 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index c567c215..e9051622 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -1,718 +1,423 @@ .page { min-height: 100vh; - background: - linear-gradient(180deg, rgba(247, 213, 111, 0.16), transparent 260px), - #fbf7ed; - color: #1d1b16; - font-family: - Arial, - Helvetica, - sans-serif; + background: #f4f1e8; + color: #1f1c16; + font-family: Arial, Helvetica, sans-serif; } .shell { - width: min(1440px, calc(100% - 32px)); + width: min(1500px, calc(100% - 28px)); margin: 0 auto; - padding: 28px 0 44px; + padding: 18px 0 34px; } -.departmentFrame { - display: grid; - grid-template-columns: 188px minmax(0, 1fr); - gap: 16px; -} - -.directorateRail { - position: sticky; - top: 20px; - align-self: start; - min-height: calc(100vh - 56px); - border: 2px solid #1d1b16; - background: #1d1b16; - color: #fffdf8; - padding: 16px; -} - -.sealMark { - display: grid; - gap: 10px; - justify-items: start; - border-bottom: 1px solid rgba(255, 253, 248, 0.32); - padding-bottom: 18px; -} - -.sealMark svg { - width: 34px; - height: 34px; -} - -.sealMark span { - font-family: Georgia, "Times New Roman", serif; - font-size: 1.5rem; - font-weight: 800; -} - -.directorateRail nav { - display: grid; - gap: 8px; - margin: 28px 0; -} - -.directorateRail a { - border: 1px solid rgba(255, 253, 248, 0.32); - color: #fffdf8; - padding: 10px; - font-size: 0.8rem; - font-weight: 800; - text-decoration: none; - text-transform: uppercase; -} - -.railNote { - margin-top: auto; - border-top: 1px solid rgba(255, 253, 248, 0.32); - padding-top: 18px; -} - -.railNote span { - display: block; - color: #f7d56f; - font-size: 0.76rem; - font-weight: 800; - text-transform: uppercase; -} - -.railNote strong { - display: block; - margin-top: 8px; - font-family: Georgia, "Times New Roman", serif; - font-size: 1.45rem; -} - -.board { - min-width: 0; +.hero, +.filterBar, +.kpiCard, +.panel, +.problemCard { + border: 1px solid #242016; + background: #fffdf8; } -.header { +.hero { display: grid; - grid-template-columns: minmax(0, 1fr) 280px; - gap: 24px; - align-items: stretch; - border: 2px solid #1d1b16; - background: #fffdf8; - padding: 28px; + grid-template-columns: minmax(0, 1fr) 360px; + gap: 14px; + padding: 22px; } .eyebrow, -.kicker { - margin: 0 0 10px; - color: #6b614c; - font-size: 0.77rem; +.panelHeader p, +.filterTitle, +.kpiCard span, +.problemCard span { + margin: 0; + color: #6d6046; + font-size: 0.76rem; font-weight: 800; - letter-spacing: 0.08em; + letter-spacing: 0.07em; text-transform: uppercase; } -.header h1 { - max-width: 920px; - margin: 0; +.hero h1 { + max-width: 980px; + margin: 8px 0 0; font-family: Georgia, "Times New Roman", serif; - font-size: clamp(2.25rem, 5vw, 5.4rem); - line-height: 0.95; + font-size: clamp(2rem, 4.6vw, 4.8rem); + line-height: 0.98; letter-spacing: 0; } -.lede { - max-width: 870px; - margin: 22px 0 0; - color: #373229; - font-size: 1.06rem; - line-height: 1.65; +.hero p, +.problemCard p { + max-width: 900px; + margin: 14px 0 0; + color: #3f382b; + font-size: 1rem; + line-height: 1.55; } -.clearanceBox { +.problemCard { display: flex; min-height: 100%; flex-direction: column; justify-content: space-between; - border-left: 2px solid #1d1b16; background: #f7d56f; - padding: 20px; -} - -.clearanceBox span, -.clearanceBox small { - font-size: 0.82rem; - font-weight: 800; - text-transform: uppercase; -} - -.clearanceBox strong { - margin: 34px 0; - font-family: Georgia, "Times New Roman", serif; - font-size: 2.1rem; - line-height: 1; -} - -.statusGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin: 16px 0; -} - -.statusCard, -.capabilityCard, -.mainPanel, -.watchPanel, -.sourcePanel, -.situationPanel, -.nationalPanel, -.livePanel, -.guardrailPanel, -.versionSection, -.checkPanel, -.dossierPanel { - border: 1.5px solid #1d1b16; - background: #fffdf8; -} - -.statusCard { - display: grid; - grid-template-columns: auto 1fr; - gap: 10px 12px; - align-items: center; - min-height: 112px; padding: 18px; } -.statusCard svg, -.capabilityCard svg, -.panelHeader svg { - width: 24px; - height: 24px; - stroke-width: 1.8; -} - -.statusCard span { - color: #6b614c; - font-size: 0.78rem; - font-weight: 800; - text-transform: uppercase; -} - -.statusCard strong { - grid-column: 1 / -1; +.problemCard strong { + margin-top: 24px; font-family: Georgia, "Times New Roman", serif; - font-size: 1.55rem; + font-size: 1.8rem; + line-height: 1.05; } -.situationGrid, -.commandLayout, -.nationalGrid { +.filterBar { display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 16px; - margin-bottom: 16px; -} - -.mainPanel, -.watchPanel, -.sourcePanel, -.situationPanel, -.nationalPanel, -.livePanel, -.guardrailPanel, -.versionSection, -.checkPanel, -.dossierPanel { - padding: 22px; + grid-template-columns: 130px repeat(4, minmax(0, 1fr)); + gap: 10px; + margin: 12px 0; + padding: 12px; } -.panelHeader { +.filterTitle { display: flex; - justify-content: space-between; - gap: 20px; - align-items: flex-start; - margin-bottom: 18px; + gap: 8px; + align-items: center; } -.panelHeader h2, -.watchPanel h2, -.versionCard h3, -.capabilityCard h3 { - margin: 0; - font-family: Georgia, "Times New Roman", serif; - line-height: 1.08; - letter-spacing: 0; +.filterTitle svg, +.panelHeader svg, +.barRow svg { + width: 20px; + height: 20px; + stroke-width: 1.8; } -.panelHeader h2, -.watchPanel h2 { - font-size: clamp(1.55rem, 3vw, 2.4rem); +.filterBar button { + min-height: 58px; + border: 1px solid #242016; + background: #f8f1dc; + color: #1f1c16; + text-align: left; } -.situationPanel { - background: #fffdf8; +.filterBar button span, +.filterBar button strong { + display: block; + padding: 0 12px; } -.nationalPanel { - background: #fffdf8; +.filterBar button span { + color: #6d6046; + font-size: 0.72rem; + font-weight: 800; + text-transform: uppercase; } -.livePanel, -.guardrailPanel { - background: #f6eddb; +.filterBar button strong { + margin-top: 7px; + font-size: 0.95rem; } -.zoneLedger { +.kpiGrid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 10px; -} - -.zoneLedger article { - display: flex; - min-height: 174px; - flex-direction: column; - border: 1.5px solid #1d1b16; - background: #fffdf8; - padding: 14px; + gap: 12px; + margin-bottom: 12px; } -.zoneLedger span { - width: max-content; - border: 1px solid #1d1b16; - background: #f7d56f; - padding: 4px 8px; - font-size: 0.76rem; - font-weight: 900; - text-transform: uppercase; +.kpiCard { + min-height: 132px; + padding: 16px; } -.zoneLedger strong { +.kpiCard strong { + display: block; margin-top: 18px; font-family: Georgia, "Times New Roman", serif; - font-size: 1rem; - line-height: 1.25; + font-size: 2.25rem; } -.zoneLedger p { - margin: auto 0 0; - color: #4a4234; - font-size: 0.86rem; - line-height: 1.35; +.kpiCard p { + margin: 8px 0 0; + color: #4b4232; + font-size: 0.9rem; } -.liveSignals { +.analyticsGrid, +.midGrid { display: grid; - gap: 10px; -} - -.liveSignals div { - display: flex; - gap: 10px; - align-items: center; - border-top: 1px solid rgba(29, 27, 22, 0.35); - padding-top: 12px; -} - -.liveSignals span { - width: 11px; - height: 11px; - flex: 0 0 auto; - border: 1px solid #1d1b16; - background: #f7d56f; - box-shadow: 0 0 0 4px rgba(247, 213, 111, 0.24); -} - -.liveSignals p { - margin: 0; - line-height: 1.35; + grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr); + gap: 12px; + margin-bottom: 12px; } -.mapBoard { - min-height: 340px; - border: 1.5px solid #1d1b16; - background: - linear-gradient(rgba(29, 27, 22, 0.08) 1px, transparent 1px), - linear-gradient(90deg, rgba(29, 27, 22, 0.08) 1px, transparent 1px), - #f6eddb; - background-size: 32px 32px; +.panel { padding: 18px; } -.mapGrid { - position: relative; - min-height: 302px; - border: 1px solid rgba(29, 27, 22, 0.55); -} - -.mapGrid::before, -.mapGrid::after { - position: absolute; - content: ""; - inset: 50% 8%; - border-top: 1.5px solid rgba(29, 27, 22, 0.55); -} - -.mapGrid::after { - inset: 8% 50%; - border-top: 0; - border-left: 1.5px solid rgba(29, 27, 22, 0.55); -} - -.mapGrid span { - position: absolute; - border: 1.5px solid #1d1b16; - background: #fffdf8; - padding: 10px 12px; - font-size: 0.85rem; - font-weight: 800; -} - -.zoneNorth { - top: 18px; - left: 50%; - transform: translateX(-50%); -} - -.zoneWest { - top: 48%; - left: 18px; - transform: translateY(-50%); +.panelHeader { + display: flex; + justify-content: space-between; + gap: 14px; + align-items: flex-start; + margin-bottom: 16px; } -.zoneCenter { - top: 50%; - left: 50%; - background: #f7d56f !important; - transform: translate(-50%, -50%); +.panelHeader h2 { + margin: 6px 0 0; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.45rem; + line-height: 1.12; } -.zoneEast { - top: 48%; - right: 18px; - transform: translateY(-50%); +.barList { + display: grid; + gap: 13px; } -.zoneSouth { - bottom: 18px; - left: 50%; - transform: translateX(-50%); +.barRow { + display: grid; + grid-template-columns: minmax(190px, 1fr) 44px minmax(0, 1.6fr); + gap: 12px; + align-items: center; } -.graph { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 10px; +.barRow div:first-child { + display: flex; + gap: 8px; + align-items: center; } -.graphStep { - position: relative; - min-height: 86px; - border: 1.5px solid #1d1b16; - background: #f6eddb; - padding: 14px; +.barRow strong { + text-align: right; } -.graphStep:nth-child(1), -.graphStep:nth-child(5), -.graphStep:nth-child(9), -.graphStep:nth-child(13) { - background: #f7d56f; +.barTrack { + overflow: hidden; + height: 18px; + border: 1px solid #242016; + background: #f4f1e8; } -.graphStep span { +.barTrack span { display: block; - color: #6b614c; - font-size: 0.72rem; - font-weight: 800; + height: 100%; } -.graphStep strong { - display: block; - margin-top: 18px; - font-size: 1rem; +.strong { + background: #1f1c16; } -.watchPanel { - background: #f6eddb; +.warm { + background: #caa23e; } -.truthList { - display: grid; - gap: 14px; - margin: 22px 0 0; - padding: 0; - list-style: none; +.soft { + background: #e7c967; } -.truthList li { - border-top: 1px solid rgba(29, 27, 22, 0.34); - padding-top: 13px; - line-height: 1.5; +.pale { + background: #efe2b2; } -.capabilityGrid { +.donutWrap { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; - margin: 16px 0; + grid-template-columns: 170px minmax(0, 1fr); + gap: 20px; + align-items: center; } -.sourcePanel { - background: #f6eddb; +.donut { + width: 168px; + height: 168px; + border: 1px solid #242016; + border-radius: 50%; + background: conic-gradient( + #1f1c16 0deg 154deg, + #caa23e 154deg 266deg, + #e7c967 266deg 327deg, + #efe2b2 327deg 360deg + ); + box-shadow: inset 0 0 0 42px #fffdf8; } -.sourceTable { +.legend { display: grid; gap: 10px; } -.sourceTable div { +.legend div { display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 6px 12px; - border-top: 1px solid rgba(29, 27, 22, 0.35); - padding-top: 12px; -} - -.sourceTable strong { - font-family: Georgia, "Times New Roman", serif; + grid-template-columns: 44px minmax(0, 1fr); + gap: 10px; + border-top: 1px solid rgba(36, 32, 22, 0.25); + padding-top: 10px; } -.sourceTable span { - border: 1px solid #1d1b16; - background: #f7d56f; - padding: 4px 8px; - font-size: 0.76rem; +.legend span { font-weight: 900; - text-transform: uppercase; } -.sourceTable p { - grid-column: 1 / -1; +.legend p { margin: 0; - color: #4a4234; - font-size: 0.92rem; - line-height: 1.45; -} - -.capabilityCard { - min-height: 214px; - padding: 20px; -} - -.capabilityCard h3 { - margin-top: 32px; - font-size: 1.35rem; -} - -.capabilityCard p, -.versionCard p { - color: #4a4234; - line-height: 1.55; -} - -.guardrailPanel { - margin-bottom: 16px; -} - -.guardrailList { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 10px; -} - -.guardrailList span { - border: 1.5px solid #1d1b16; - background: #fffdf8; - padding: 14px; - font-size: 0.92rem; - font-weight: 800; + color: #4b4232; line-height: 1.35; } -.versionGrid { +.regionGrid { display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; } -.versionCard { - display: flex; - min-height: 196px; - flex-direction: column; - border: 1.5px solid #1d1b16; - background: #fffdf8; - padding: 16px; +.regionCard { + min-height: 146px; + border: 1px solid #242016; + background: #f8f1dc; + padding: 12px; } -.versionCard span { - width: max-content; - border: 1px solid #1d1b16; - background: #f7d56f; - padding: 4px 8px; +.regionCard span { + font-size: 0.75rem; font-weight: 900; -} - -.versionCard h3 { - margin-top: 18px; - font-size: 1.2rem; -} - -.versionCard strong { - margin-top: auto; - color: #254229; - font-size: 0.78rem; text-transform: uppercase; } -.bottomGrid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(320px, 0.76fr); - gap: 16px; +.regionCard strong { + display: block; margin-top: 16px; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.45rem; } -.checkList { +.regionCard p, +.regionCard small { + display: block; + margin: 8px 0 0; + color: #4b4232; + line-height: 1.35; +} + +.decisionList { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } -.checkList div { +.decisionList div { display: flex; gap: 10px; - align-items: center; - border-top: 1px solid rgba(29, 27, 22, 0.35); - padding-top: 12px; + align-items: flex-start; + border-top: 1px solid rgba(36, 32, 22, 0.25); + padding-top: 11px; } -.checkList span { - width: 10px; - height: 10px; +.decisionList span { + width: 11px; + height: 11px; flex: 0 0 auto; - border: 1px solid #1d1b16; + margin-top: 3px; + border: 1px solid #242016; background: #f7d56f; } -.checkList p { +.decisionList p { margin: 0; - line-height: 1.35; + line-height: 1.45; } -.dossierList { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; +.tableWrap { + overflow-x: auto; } -.dossierList article { - border: 1px solid #1d1b16; - background: #f6eddb; - padding: 12px; +.tableWrap table { + width: 100%; + border-collapse: collapse; + min-width: 880px; +} + +.tableWrap th, +.tableWrap td { + border-top: 1px solid rgba(36, 32, 22, 0.28); + padding: 12px 10px; + text-align: left; + vertical-align: top; } -.dossierList span { - color: #6b614c; +.tableWrap th { + color: #6d6046; font-size: 0.72rem; font-weight: 900; text-transform: uppercase; } -.dossierList strong { - display: block; - margin-top: 8px; - font-family: Georgia, "Times New Roman", serif; +.tableWrap td { + color: #29241b; + line-height: 1.35; } -.dossierList p { - margin: 8px 0 0; - color: #4a4234; - font-size: 0.92rem; +.highRisk, +.mediumRisk { + display: inline-block; + border: 1px solid #242016; + padding: 4px 8px; + font-size: 0.78rem; + font-weight: 900; + text-transform: uppercase; } -@media (max-width: 1120px) { - .departmentFrame { - grid-template-columns: 1fr; - } - - .directorateRail { - position: static; - min-height: auto; - } +.highRisk { + background: #f7d56f; +} - .directorateRail nav { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } +.mediumRisk { + background: #f8f1dc; +} - .header, - .situationGrid, - .commandLayout, - .nationalGrid, - .bottomGrid { +@media (max-width: 1180px) { + .hero, + .analyticsGrid, + .midGrid { grid-template-columns: 1fr; } - .clearanceBox { - border-top: 2px solid #1d1b16; - border-left: 0; - } - - .statusGrid, - .capabilityGrid { + .filterBar { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .graph, - .versionGrid, - .zoneLedger, - .guardrailList { + .kpiGrid, + .regionGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 680px) { +@media (max-width: 720px) { .shell { - width: min(100% - 20px, 1440px); + width: min(100% - 18px, 1500px); padding-top: 10px; } - .header, - .mainPanel, - .watchPanel, - .sourcePanel, - .situationPanel, - .nationalPanel, - .livePanel, - .guardrailPanel, - .versionSection, - .checkPanel, - .dossierPanel { - padding: 16px; + .hero, + .panel, + .problemCard { + padding: 14px; } - .statusGrid, - .capabilityGrid, - .graph, - .versionGrid, - .zoneLedger, - .guardrailList, - .checkList, - .dossierList, - .directorateRail nav { + .filterBar, + .kpiGrid, + .regionGrid, + .donutWrap { grid-template-columns: 1fr; } - .mapBoard { - min-height: 300px; + .barRow { + grid-template-columns: 1fr 40px; } - .statusCard, - .capabilityCard, - .versionCard { - min-height: auto; + .barTrack { + grid-column: 1 / -1; } } diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index ddfc55b9..e134a85b 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -1,464 +1,302 @@ import { - Activity, - BookOpenCheck, + AlertTriangle, + BarChart3, Building2, - ClipboardCheck, + CheckCircle2, + ClipboardList, Database, - Eye, - FileText, - GitBranch, - Globe2, + Filter, Landmark, - Layers, - Lock, - Map, - Network, - Radar, - Scale, - Shield, - type LucideIcon, + MapPinned, + ShieldCheck, + Stethoscope, + Waves, } from "lucide-react"; import styles from "./dashboard.module.css"; -const versions = [ - { - id: "1.6", - name: "Geospatial Detection", - signal: "Coordinates, sensors, object tracking", - status: "Tested", - }, - { - id: "2.6", - name: "Sustainable Development", - signal: "Infrastructure, climate, resource risk", - status: "Tested", - }, - { - id: "3.6", - name: "Physical Interface", - signal: "Edge telemetry and device state", - status: "Tested", - }, - { - id: "4.6", - name: "HSE Intelligence", - signal: "Health, welfare, education gaps", - status: "Tested", - }, - { - id: "5.6", - name: "National Audit", - signal: "RTI, open data, public authority review", - status: "Tested", - }, - { - id: "6.6", - name: "Gap Closure", - signal: "Yudh assessment and Vyuha selection", - status: "Tested", - }, -]; - -const checks = [ - "Brain graph tests", - "Ruff lint and format", - "mypy strict", - "Bandit SAST", - "Secret scanning", - "Dependency audit", - "SBOM generation", - "CodeQL actions, Python, JS/TS", +const filters = [ + { label: "Region", value: "All India" }, + { label: "Domain", value: "All DISHA modules" }, + { label: "Evidence", value: "Verified + review" }, + { label: "Priority", value: "High and medium" }, ]; -const graphNodes = [ - "Intake", - "Evidence", - "Context", - "Reasoning", - "Router", - "Action", - "Yudh", - "Vyuha", - "NFU Policy", - "Human Review", - "Audit", - "Memory", - "Result", +const kpis = [ + { label: "Active public-interest cases", value: "86", delta: "+12 this week" }, + { label: "Evidence confidence", value: "74%", delta: "source-linked" }, + { label: "Unresolved claims", value: "19", delta: "need verification" }, + { label: "Human approval queue", value: "7", delta: "policy-gated" }, + { label: "Average risk score", value: "0.62", delta: "medium posture" }, ]; -const capabilities = [ - { - title: "Evidence Discipline", - body: "Every result carries source lists, confidence, evidence class, and explicit verification gaps.", - icon: FileText, - }, - { - title: "Policy Guard", - body: "No-First-Use blocks retaliatory, destructive, unauthorized, and self-propagating actions.", - icon: Shield, - }, - { - title: "Audit Memory", - body: "Graph runs create audit events and update working plus episodic memory for continuity.", - icon: Database, - }, - { - title: "Civic Intelligence", - body: "The version ladder covers geospatial, development, HSE, public audit, and gap closure missions.", - icon: Landmark, - }, +const domains = [ + { name: "National audit", value: 24, tone: "strong" }, + { name: "HSE service gaps", value: 19, tone: "warm" }, + { name: "Infrastructure resilience", value: 16, tone: "soft" }, + { name: "Geospatial public safety", value: 13, tone: "soft" }, + { name: "Edge telemetry", value: 8, tone: "pale" }, + { name: "Gap closure", value: 6, tone: "pale" }, ]; -const dossiers = [ - { - code: "NAT-AUDIT-05", - title: "RTI contradiction review", - lane: "Public authority audit", - }, - { - code: "HSE-04", - title: "District health service gap", - lane: "Service delivery", - }, - { - code: "RES-02", - title: "Bridge flood impact assessment", - lane: "Infrastructure resilience", - }, - { - code: "EDGE-03", - title: "Edge device telemetry review", - lane: "Physical interface", - }, - { - code: "GEO-01", - title: "Geospatial perimeter signal", - lane: "Sensor evidence", - }, - { - code: "CLOSURE-06", - title: "Gap closure corrective action", - lane: "Yudh/Vyuha routing", - }, +const evidenceQuality = [ + { label: "Official / RTI / audit record", value: 43 }, + { label: "Open data / document", value: 31 }, + { label: "Sensor / telemetry", value: 17 }, + { label: "Unresolved / allegation", value: 9 }, ]; -const sources = [ - { label: "Official record", level: "High", note: "Source link required" }, - { label: "RTI record", level: "High", note: "Contradiction review" }, - { label: "Sensor signal", level: "Medium", note: "Needs chain context" }, - { label: "Operator note", level: "Low", note: "Human claim only" }, +const regionalMatrix = [ + { region: "North", risk: "Medium", cases: 17, lead: "Public asset continuity" }, + { region: "East", risk: "High", cases: 22, lead: "Flood and service gaps" }, + { region: "West", risk: "Medium", cases: 14, lead: "Open-data audit" }, + { region: "South", risk: "Medium", cases: 18, lead: "Water and edge telemetry" }, + { region: "Central", risk: "High", cases: 15, lead: "Gap closure backlog" }, ]; -const nationalZones = [ +const cases = [ { - region: "North", - focus: "Border-state resilience and public infrastructure continuity", - lane: "Geospatial + audit", + id: "DA-501", + problem: "RTI response conflicts with public authority data", + module: "5.6 National Audit", + evidence: "RTI record", + risk: "High", + action: "Contradiction register", }, { - region: "East", - focus: "Flood, bridge, health, and school-access stress signals", - lane: "Development + HSE", + id: "HS-224", + problem: "District health and school access gap", + module: "4.6 HSE", + evidence: "Document", + risk: "Medium", + action: "Service gap report", }, { - region: "West", - focus: "Urban services, port-adjacent infrastructure, and open-data review", - lane: "Audit + resilience", + id: "IR-172", + problem: "Flood impact on bridge and road infrastructure", + module: "2.6 Resilience", + evidence: "Open data", + risk: "High", + action: "Prioritise resilience", }, { - region: "South", - focus: "Water, climate, edge telemetry, and district service continuity", - lane: "SETU/VARUNA + edge", + id: "GT-118", + problem: "Repeated movement near public-safety perimeter", + module: "1.6 Geospatial", + evidence: "Sensor signal", + risk: "Medium", + action: "Preserve evidence", }, { - region: "Central", - focus: "Gap closure routing across public service and resource evidence", - lane: "Yudh/Vyuha", + id: "GC-066", + problem: "Missing audit trail and delayed corrective action", + module: "6.6 Gap Closure", + evidence: "Audit report", + risk: "High", + action: "Yudh/Vyuha routing", }, ]; -const liveSignals = [ - "Open-data evidence stream", - "RTI contradiction queue", - "District service-gap watch", - "Infrastructure resilience lane", - "Human approval queue", - "Audit hash-chain continuity", +const decisions = [ + "Prioritise official records, RTI records, open data, audit records, and consented telemetry.", + "Route every case through evidence class, confidence level, risk score, policy status, and human approval requirement.", + "Block retaliation, unauthorized access, destructive actions, and individual-level surveillance.", + "Treat unsupported claims as verification work, not truth.", ]; -const privacyGuardrails = [ - "No individual-level tracking", - "No biometric or face surveillance", - "No hidden collection", - "Only aggregate, open, consented, or authorized signals", - "Every claim carries evidence status", -]; +const moduleIcons = [Landmark, Stethoscope, Waves, MapPinned, Database, ShieldCheck]; export default function DashboardPage() { return (
-
-
- - -
-
-
-

DISHA Intelligence Directorate

-

National evidence operations dashboard

-

- A local situation board for DISHA Brain: source discipline, - version routing, lawful action boundaries, audit memory, and - production readiness without exposing implementation code. -

-
-
- Branch PR #73 - All checks passed - Production-readiness spine -
-
+
+
+
+

DISHA Public-Interest Intelligence

+

Operational analytics for evidence, risk, and gap closure

+

+ DISHA helps an operator turn fragmented public-sector signals into + source-linked cases: what happened, how strong the evidence is, what + risk exists, and what lawful action should happen next. +

+
+
+ Problem being solved + Public decisions without reliable evidence +

+ The dashboard tracks gaps, contradictions, service failures, and + resilience risks without exposing implementation internals or + collecting personal data. +

+
+
-
- - - - -
+
+
+
+ {filters.map((filter) => ( + + ))} +
-
-
-
-
-

India resilience observatory

-

National-scale awareness without citizen surveillance

-
-
-
- {nationalZones.map((zone) => ( -
- {zone.region} - {zone.focus} -

{zone.lane}

-
- ))} -
-
+
+ {kpis.map((kpi) => ( +
+ {kpi.label} + {kpi.value} +

{kpi.delta}

+
+ ))} +
- -
- -
-
-
-
-

Situation room

-

Evidence posture by operational lane

-
-
-
-
- National Audit - Geospatial - DISHA Brain - HSE - Gap Closure
-
-
- - -
+ ); + })} +
+ -
-
-
-
-

Agentic chain

-

Decision flow under evidence control

+
+ +
+ +
+
+
-
-
-
-

Source desk

-

Reliability matrix

-
-
-
- {sources.map((source) => ( -
- {source.label} - {source.level} -

{source.note}

-
- ))} +
+
+ +
+ {regionalMatrix.map((row) => ( +
+ {row.region} + {row.risk} +

{row.cases} cases

+ {row.lead}
-
-
- -
- {capabilities.map((item) => ( -
-
))} -
+
+ -
-
-
-

Constitutional guardrails

-

What this system will not do

+
+ +
+ {decisions.map((decision) => ( +
+ +

{decision}

-
-
- {privacyGuardrails.map((guardrail) => ( - {guardrail} - ))} -
-
+ ))} +
+ +
-
-
-
-

Version ladder

-

Six intelligence surfaces, one audited spine

-
-
-
- {versions.map((version) => ( -
- {version.id} -

{version.name}

-

{version.signal}

- {version.status} -
+
+ +
+ + + + + + + + + + + + + {cases.map((item) => ( + + + + + + + + ))} - - - -
-
-
-
-

Readiness ledger

-

Green gates

-
-
-
- {checks.map((check) => ( -
- -

{check}

-
- ))} -
-
- -
-
-
-

Mission docket

-

Operational scenarios

-
-
-
- {dossiers.map((dossier) => ( -
- {dossier.code} - {dossier.title} -

{dossier.lane}

-
- ))} -
-
-
+ +
CaseProblemModuleEvidenceRiskNext action
{item.id}{item.problem}{item.module}{item.evidence} + + {item.risk} + + {item.action}
-
+
); } -function StatusCard({ +function PanelHeader({ icon: Icon, - label, - value, + eyebrow, + title, }: { - icon: LucideIcon; - label: string; - value: string; + icon: typeof BarChart3; + eyebrow: string; + title: string; }) { return ( -
+
+
+

{eyebrow}

+

{title}

+
+ ); } From 6ccb3a6740a5d6b96e9402ea102a692615035b08 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 13:00:39 +0530 Subject: [PATCH 10/26] Add constitutional action ledger to Brain graph --- README.md | 1 + disha/brain/governance/__init__.py | 3 + .../brain/governance/constitutional_audit.py | 88 +++++++++++++++++++ disha/brain/graph/nodes/audit_agent.py | 1 + disha/brain/graph/nodes/context_agent.py | 9 +- docs/REPOSITORY_AUDIT.md | 79 +++++++++++++++++ tests/test_disha_brain_graph.py | 29 ++++++ 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 disha/brain/governance/constitutional_audit.py create mode 100644 docs/REPOSITORY_AUDIT.md diff --git a/README.md b/README.md index 1c7f0484..adc487e9 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ No-First-Use is enforced in code under `disha/brain/policy/no_first_use.py`. Off - [Evidence Model](docs/EVIDENCE_MODEL.md) - [Trust Model](docs/TRUST_MODEL.md) - [Production Readiness](docs/PRODUCTION_READINESS.md) +- [Repository Audit](docs/REPOSITORY_AUDIT.md) - [Demos](docs/DEMOS.md) - [Roadmap](docs/ROADMAP.md) diff --git a/disha/brain/governance/__init__.py b/disha/brain/governance/__init__.py index f22a14d9..83c7edc6 100644 --- a/disha/brain/governance/__init__.py +++ b/disha/brain/governance/__init__.py @@ -1,12 +1,15 @@ from __future__ import annotations +from .constitutional_audit import ConstitutionalAudit, build_constitutional_audit from .constitutional_mapper import map_constitutional_context from .nyaya import nyaya_summary from .open_data_audit import audit_open_data from .rti_parser import parse_rti_signal __all__ = [ + "ConstitutionalAudit", "audit_open_data", + "build_constitutional_audit", "map_constitutional_context", "nyaya_summary", "parse_rti_signal", diff --git a/disha/brain/governance/constitutional_audit.py b/disha/brain/governance/constitutional_audit.py new file mode 100644 index 00000000..448aadf1 --- /dev/null +++ b/disha/brain/governance/constitutional_audit.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class ConstitutionalAudit(BaseModel): + public_authority_signal: bool + implicated_principles: list[str] = Field(default_factory=list) + evidence_gaps: list[str] = Field(default_factory=list) + permitted_actions: list[str] = Field(default_factory=list) + blocked_actions: list[str] = Field(default_factory=list) + human_review_required: bool + verification_status: str + action_path: str + + +def build_constitutional_audit( + text: str, + evidence_class: str, + confidence_level: str, + source_links: list[str], +) -> ConstitutionalAudit: + normalized = text.lower() + principles: list[str] = [] + gaps: list[str] = [] + + public_authority = any( + token in normalized + for token in ( + "article 12", + "public authority", + "government", + "rti", + "open data", + "audit", + "district", + ) + ) + + if public_authority: + principles.append("public authority accountability [VERIFY REQUIRED]") + if any( + token in normalized for token in ("health", "school", "education", "welfare") + ): + principles.append("life, dignity, and service access [VERIFY REQUIRED]") + if any( + token in normalized for token in ("privacy", "personal", "biometric", "face") + ): + principles.append("privacy and proportionality [VERIFY REQUIRED]") + if any(token in normalized for token in ("contradiction", "conflict", "mismatch")): + principles.append("reasoned public record correction [VERIFY REQUIRED]") + if any(token in normalized for token in ("cyber", "telemetry", "sensor", "edge")): + principles.append("authorized cyber evidence handling [VERIFY REQUIRED]") + + if not source_links: + gaps.append("source links absent") + if evidence_class in {"unresolved", "allegation", "inference"}: + gaps.append("evidence class cannot support final public claim") + if confidence_level == "low": + gaps.append("confidence is low") + if not principles: + principles.append("public-interest relevance [VERIFY REQUIRED]") + gaps.append("constitutional principle not identified") + + human_review = ( + bool(gaps) or "privacy and proportionality [VERIFY REQUIRED]" in principles + ) + verification_status = "verification_required" if gaps else "source_linked_review" + + return ConstitutionalAudit( + public_authority_signal=public_authority, + implicated_principles=principles, + evidence_gaps=gaps, + permitted_actions=["report", "preserve_evidence", "request_source_review"], + blocked_actions=[ + "individual_level_surveillance", + "unauthorized_access", + "retaliatory_action", + "unverified_public_claim", + ], + human_review_required=human_review, + verification_status=verification_status, + action_path=( + "Create a source-linked constitutional audit note before action." + if human_review + else "Proceed with a source-linked accountability report." + ), + ) diff --git a/disha/brain/graph/nodes/audit_agent.py b/disha/brain/graph/nodes/audit_agent.py index 39ae9d3d..aa54bb2d 100644 --- a/disha/brain/graph/nodes/audit_agent.py +++ b/disha/brain/graph/nodes/audit_agent.py @@ -19,6 +19,7 @@ def run(state: DishaGraphState, ledger: AuditLedger) -> DishaGraphState: "risk_score": state.risk_score, "human_approval_required": state.human_approval_required, "source_count": len(state.source_links), + "constitutional_audit": state.metadata.get("constitutional_audit", {}), }, ) state.audit_record = ledger.append(event) diff --git a/disha/brain/graph/nodes/context_agent.py b/disha/brain/graph/nodes/context_agent.py index 1f5bd4c7..24491c66 100644 --- a/disha/brain/graph/nodes/context_agent.py +++ b/disha/brain/graph/nodes/context_agent.py @@ -1,11 +1,18 @@ from __future__ import annotations -from ...governance import map_constitutional_context +from ...governance import build_constitutional_audit, map_constitutional_context from ..state import DishaGraphState def run(state: DishaGraphState) -> DishaGraphState: state.constitutional_context = map_constitutional_context(state.input_text) + audit = build_constitutional_audit( + text=state.input_text, + evidence_class=state.evidence_class.value, + confidence_level=state.confidence_level.value, + source_links=state.source_links, + ) + state.metadata["constitutional_audit"] = audit.model_dump() if state.source_type in {"sensor", "cyber_telemetry"}: state.cyber_context = { "source_type": state.source_type, diff --git a/docs/REPOSITORY_AUDIT.md b/docs/REPOSITORY_AUDIT.md new file mode 100644 index 00000000..0f7532b7 --- /dev/null +++ b/docs/REPOSITORY_AUDIT.md @@ -0,0 +1,79 @@ +# Repository Audit + +## What DISHA Is Really Solving + +DISHA is strongest when framed as a Constitutional Cyber Audit system for India-facing public-interest operations. + +The working product problem is: + +- public records, RTI responses, service-gap reports, sensor signals, and cyber telemetry arrive fragmented +- operators need to know what is evidence, what is allegation, what is contradiction, and what is unresolved +- cyber or public-service action must be defensive, lawful, audited, and human-governed +- unsupported claims must not become public truth + +The unique product direction is not generic AGI. It is governed intelligence: evidence class, constitutional/public-authority relevance, risk score, No-First-Use status, human approval, and audit hash all attached to the same decision. + +## Production Spine + +These areas are the current production spine: + +- `disha/brain/graph/` +- `disha/brain/evidence/` +- `disha/brain/governance/` +- `disha/brain/policy/` +- `disha/brain/yudh/` +- `disha/brain/vyuha/` +- `disha/brain/audit/` +- `disha/brain/memory/` +- `web/` +- `src/` +- `tests/test_disha_brain_graph.py` +- `demos/` + +## Stale Or High-Debt Surfaces + +These areas contain useful ideas but should not be treated as production truth without review: + +- older AGI/elite-positioning docs that claim more than the verified implementation +- duplicate web surfaces under `disha/apps/web/` +- experimental AI model, physics, strategy, and service folders under `disha/ai/` and `disha/services/` +- legacy architecture reports that mention retired paths or broad AGI claims +- placeholder smoke tests in service folders + +Do not delete them blindly. Promote them only through a review path: ownership, security posture, tests, dependency audit, and integration into the Brain spine. + +## New Innovation Added + +The repo now has a Constitutional Action Ledger in `disha/brain/governance/constitutional_audit.py`. + +For each graph run it records: + +- public authority signal +- implicated constitutional/public-interest principles +- evidence gaps +- allowed actions +- blocked actions +- human review requirement +- verification status +- lawful action path + +The graph stores this ledger in audit metadata, so constitutional reasoning is attached to the evidence trail instead of living only in docs. + +## Product Rule + +DISHA must not become a surveillance platform. It should become a public-interest audit and cyber-protection system: + +- aggregate or source-linked evidence +- no individual-level surveillance +- no unauthorized access +- no retaliation +- no public claim without evidence status +- human approval for ambiguous or high-risk actions + +## Next Technical Debt Targets + +1. Add promotion labels for legacy/experimental modules. +2. Move stale AGI claims behind a historical-docs section. +3. Add official-source connector interfaces for `data.gov.in`, RTI records, audit reports, and cyber telemetry. +4. Replace heuristic constitutional mapping with a reviewed taxonomy file. +5. Add a persistent graph/audit export path for production deployments. diff --git a/tests/test_disha_brain_graph.py b/tests/test_disha_brain_graph.py index 9ce8cef8..2f762abb 100644 --- a/tests/test_disha_brain_graph.py +++ b/tests/test_disha_brain_graph.py @@ -7,6 +7,7 @@ from disha.brain.audit import AuditEvent, AuditLedger from disha.brain.evidence import EvidenceClass, build_evidence_bundle from disha.brain.geospatial import Coordinates, TrackedObject +from disha.brain.governance import build_constitutional_audit from disha.brain.graph import DishaAgenticGraph, GraphInput from disha.brain.graph.router import route_version from disha.brain.memory import MemoryStore @@ -75,6 +76,19 @@ def test_vyuha_selection() -> None: assert formation.name == "NYAYA Audit Vyuha" +def test_constitutional_audit_requires_sources_for_public_authority_claim() -> None: + audit = build_constitutional_audit( + text="RTI public authority contradiction about district health access", + evidence_class="rti_record", + confidence_level="medium", + source_links=[], + ) + assert audit.public_authority_signal is True + assert audit.human_review_required is True + assert "source links absent" in audit.evidence_gaps + assert "unverified_public_claim" in audit.blocked_actions + + def test_audit_ledger_creation() -> None: ledger = AuditLedger() event = ledger.append(AuditEvent(event_id="e1", action="test", outcome="ok")) @@ -103,6 +117,21 @@ def test_national_audit_evidence_classification() -> None: assert "national_audit" in output.signals +def test_graph_audit_contains_constitutional_action_ledger() -> None: + graph = DishaAgenticGraph() + result = graph.invoke( + GraphInput( + input_text="RTI public authority contradiction in district welfare record", + source_type="rti_record", + requested_actions=["report", "preserve_evidence"], + ) + ) + ledger = result.audit_event.metadata["constitutional_audit"] + assert ledger["public_authority_signal"] is True + assert ledger["verification_status"] == "verification_required" + assert "unverified_public_claim" in ledger["blocked_actions"] + + def test_hse_service_gap_output() -> None: output = v4_6_hse.run("health welfare education district gap") assert output.version == "4.6" From 703dc03de3007c872aae055c0cdd997bc6118388 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 13:18:44 +0530 Subject: [PATCH 11/26] Add Dharma Yudh Vyuha planner --- disha/brain/graph/nodes/vyuha_agent.py | 14 +- disha/brain/vyuha/__init__.py | 14 +- disha/brain/vyuha/dynamic.py | 237 +++++++++++++++++++++++++ docs/REPOSITORY_AUDIT.md | 11 ++ tests/test_disha_brain_graph.py | 42 ++++- 5 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 disha/brain/vyuha/dynamic.py diff --git a/disha/brain/graph/nodes/vyuha_agent.py b/disha/brain/graph/nodes/vyuha_agent.py index 0e27c452..1cfc1d4e 100644 --- a/disha/brain/graph/nodes/vyuha_agent.py +++ b/disha/brain/graph/nodes/vyuha_agent.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ...vyuha import build_recommendation, select_vyuha +from ...vyuha import build_dharma_yudh_vyuha_plan from ..state import DishaGraphState @@ -11,6 +11,14 @@ def run(state: DishaGraphState) -> DishaGraphState: state.evidence_class.value, state.input_text, ] - formation = select_vyuha(signals, state.risk_score) - state.vyuha_recommendation = build_recommendation(formation).model_dump() + plan = build_dharma_yudh_vyuha_plan( + signals, + state.risk_score, + evidence_class=state.evidence_class.value, + confidence_level=state.confidence_level.value, + source_links=state.source_links, + requested_actions=state.requested_actions, + constitutional_audit=state.metadata.get("constitutional_audit", {}), + ) + state.vyuha_recommendation = plan.model_dump() return state diff --git a/disha/brain/vyuha/__init__.py b/disha/brain/vyuha/__init__.py index 003d819e..fbd2b7eb 100644 --- a/disha/brain/vyuha/__init__.py +++ b/disha/brain/vyuha/__init__.py @@ -1,6 +1,18 @@ from __future__ import annotations +from .dynamic import ( + DharmaYudhVyuhaPlan, + FormationScore, + build_dharma_yudh_vyuha_plan, +) from .playbook import VyuhaRecommendation, build_recommendation from .selector import select_vyuha -__all__ = ["VyuhaRecommendation", "build_recommendation", "select_vyuha"] +__all__ = [ + "DharmaYudhVyuhaPlan", + "FormationScore", + "VyuhaRecommendation", + "build_dharma_yudh_vyuha_plan", + "build_recommendation", + "select_vyuha", +] diff --git a/disha/brain/vyuha/dynamic.py b/disha/brain/vyuha/dynamic.py new file mode 100644 index 00000000..b57065b3 --- /dev/null +++ b/disha/brain/vyuha/dynamic.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + +from .formations import FORMATIONS, VyuhaFormation + + +class FormationScore(BaseModel): + formation: str + score: int + matched_triggers: list[str] = Field(default_factory=list) + missing_evidence: list[str] = Field(default_factory=list) + dharma_alignment: list[str] = Field(default_factory=list) + karma_constraints: list[str] = Field(default_factory=list) + rationale: str + + +class DharmaYudhVyuhaPlan(BaseModel): + formation: str + allowed_actions: list[str] + forbidden_actions: list[str] + human_approval_required: bool + recommendation: str + pramana: dict[str, Any] + dharma: dict[str, Any] + yudh: dict[str, Any] + karma: dict[str, Any] + formation_scores: list[FormationScore] + + +def build_dharma_yudh_vyuha_plan( + signals: list[str], + risk_score: float = 0.0, + *, + evidence_class: str = "unresolved", + confidence_level: str = "low", + source_links: list[str] | None = None, + requested_actions: list[str] | None = None, + constitutional_audit: dict[str, Any] | None = None, + formations: list[VyuhaFormation] | None = None, +) -> DharmaYudhVyuhaPlan: + audit = constitutional_audit or {} + source_links = source_links or [] + requested_actions = requested_actions or [] + formation_pool = formations or FORMATIONS + normalized = _expand_signals(signals, risk_score, audit) + requested = {_normalize(action) for action in requested_actions} + blocked = {_normalize(action) for action in audit.get("blocked_actions", [])} + + scores = [ + _score_formation( + formation, + normalized, + requested, + blocked, + evidence_class, + confidence_level, + source_links, + audit, + ) + for formation in formation_pool + ] + scores.sort( + key=lambda score: ( + score.score, + "NYAYA" in score.formation, + "Gap Closure" in score.formation, + ), + reverse=True, + ) + selected = next( + formation + for formation in formation_pool + if formation.name == scores[0].formation + ) + forbidden = sorted( + set(selected.forbidden_actions) | set(audit.get("blocked_actions", [])) + ) + human_approval_required = selected.human_approval_required or bool( + audit.get("human_review_required") + ) + + return DharmaYudhVyuhaPlan( + formation=selected.name, + allowed_actions=selected.allowed_actions, + forbidden_actions=forbidden, + human_approval_required=human_approval_required, + recommendation=( + f"Use {selected.name}; {selected.fallback_behavior}. " + f"Action path: {audit.get('action_path', 'keep a source-linked audit trail')}." + ), + pramana={ + "evidence_class": evidence_class, + "confidence_level": confidence_level, + "source_count": len(source_links), + "verification_status": audit.get("verification_status", "not_assessed"), + "gaps": audit.get("evidence_gaps", []), + }, + dharma={ + "public_authority_signal": bool(audit.get("public_authority_signal")), + "principles": audit.get("implicated_principles", []), + "lawful_frame": "constitutional audit, defensive cyber protection, and source-linked public-interest action", + }, + yudh={ + "risk_score": risk_score, + "posture": _posture(risk_score), + "formation_reason": scores[0].rationale, + }, + karma={ + "requested_actions": requested_actions, + "permitted_actions": audit.get( + "permitted_actions", selected.allowed_actions + ), + "blocked_actions": forbidden, + "discipline": "no individual surveillance, no unauthorized access, no retaliation, no unsupported public claim", + }, + formation_scores=scores, + ) + + +def _score_formation( + formation: VyuhaFormation, + normalized: set[str], + requested: set[str], + blocked: set[str], + evidence_class: str, + confidence_level: str, + source_links: list[str], + audit: dict[str, Any], +) -> FormationScore: + triggers = {_normalize(trigger) for trigger in formation.triggers} + matched = sorted(normalized & triggers) + allowed = {_normalize(action) for action in formation.allowed_actions} + forbidden = {_normalize(action) for action in formation.forbidden_actions} + score = len(matched) * 12 + constraints: list[str] = [] + alignment: list[str] = [] + + if requested & allowed: + score += len(requested & allowed) * 3 + if requested & (forbidden | blocked): + score -= 25 + constraints.append( + "requested action conflicts with blocked or forbidden action" + ) + if audit.get("human_review_required") and formation.human_approval_required: + score += 4 + alignment.append("human review gate matched") + if audit.get("public_authority_signal") and "constitutional_reference" in triggers: + score += 8 + alignment.append("public authority accountability matched") + if evidence_class in {"official_record", "rti_record"}: + score += 4 + alignment.append("source class supports public audit") + if confidence_level == "high": + score += 2 + elif confidence_level == "low": + score -= 4 + + missing = _missing_evidence(formation, source_links, audit) + score -= len(missing) * 3 + if audit.get("evidence_gaps") and "unverified_public_claim" in blocked: + constraints.append("public claim must remain verification-gated") + + return FormationScore( + formation=formation.name, + score=score, + matched_triggers=matched, + missing_evidence=missing, + dharma_alignment=alignment, + karma_constraints=constraints, + rationale=( + f"{len(matched)} trigger(s), {len(missing)} evidence gap(s), " + f"{len(constraints)} action constraint(s)." + ), + ) + + +def _expand_signals( + signals: list[str], risk_score: float, audit: dict[str, Any] +) -> set[str]: + normalized = {_normalize(signal) for signal in signals if signal} + joined = " ".join(signals).lower() + if risk_score >= 0.75: + normalized.update({"high", "high_risk", "critical_public_risk"}) + elif risk_score >= 0.4: + normalized.update({"mitigation", "gap_closure"}) + if audit.get("public_authority_signal"): + normalized.update({"national_audit", "constitutional_reference"}) + if "rti" in joined: + normalized.add("rti_record") + if any( + "privacy" in principle.lower() + for principle in audit.get("implicated_principles", []) + ): + normalized.add("privacy") + if any( + "contradiction" in principle.lower() + for principle in audit.get("implicated_principles", []) + ): + normalized.add("contradiction") + return normalized + + +def _missing_evidence( + formation: VyuhaFormation, source_links: list[str], audit: dict[str, Any] +) -> list[str]: + requirements = {_normalize(item) for item in formation.evidence_requirements} + gaps = set(audit.get("evidence_gaps", [])) + missing: list[str] = [] + if { + "official_source", + "source_list", + "source_links", + } & requirements and not source_links: + missing.append("source links required") + if "constitutional_mapping" in requirements and not audit.get( + "implicated_principles" + ): + missing.append("constitutional mapping required") + if "source links absent" in gaps and "source links required" not in missing: + missing.append("source links absent") + return missing + + +def _normalize(value: str) -> str: + return value.lower().replace("-", "_").replace(" ", "_") + + +def _posture(risk_score: float) -> str: + if risk_score >= 0.75: + return "contain defensively with human approval" + if risk_score >= 0.4: + return "prepare lawful mitigation" + return "observe, verify, and preserve evidence" diff --git a/docs/REPOSITORY_AUDIT.md b/docs/REPOSITORY_AUDIT.md index 0f7532b7..343ae8c4 100644 --- a/docs/REPOSITORY_AUDIT.md +++ b/docs/REPOSITORY_AUDIT.md @@ -59,6 +59,17 @@ For each graph run it records: The graph stores this ledger in audit metadata, so constitutional reasoning is attached to the evidence trail instead of living only in docs. +The repo also has a Dharma-Yudh-Vyuha planner in `disha/brain/vyuha/dynamic.py`. + +This planner turns each run into a governed decision: + +- Pramana: evidence class, confidence, source count, verification status, and gaps +- Dharma: public-authority signal and constitutional/public-interest principles +- Yudh: risk posture and formation reason +- Karma: permitted actions, blocked actions, and action discipline + +The planner does not reward aggressive action. It prefers the formation that is best supported by proof, lawful duty, and defensive constraints. A public-authority RTI contradiction should move toward NYAYA Audit Vyuha; a high-risk defensive incident should still remain NFU-governed and approval-gated. + ## Product Rule DISHA must not become a surveillance platform. It should become a public-interest audit and cyber-protection system: diff --git a/tests/test_disha_brain_graph.py b/tests/test_disha_brain_graph.py index 2f762abb..73e006c1 100644 --- a/tests/test_disha_brain_graph.py +++ b/tests/test_disha_brain_graph.py @@ -14,7 +14,7 @@ from disha.brain.policy import evaluate_actions from disha.brain.policy.no_first_use import NFUDecision from disha.brain.versions import v4_6_hse, v5_6_national_audit -from disha.brain.vyuha import select_vyuha +from disha.brain.vyuha import build_dharma_yudh_vyuha_plan, select_vyuha def test_brain_package_import_is_lazy() -> None: @@ -76,6 +76,28 @@ def test_vyuha_selection() -> None: assert formation.name == "NYAYA Audit Vyuha" +def test_dharma_yudh_vyuha_plan_prefers_nyaya_for_public_authority() -> None: + audit = build_constitutional_audit( + text="RTI public authority contradiction in welfare record", + evidence_class="rti_record", + confidence_level="medium", + source_links=["https://example.gov.in/rti-response"], + ) + plan = build_dharma_yudh_vyuha_plan( + ["RTI public authority contradiction", "rti_record"], + 0.35, + evidence_class="rti_record", + confidence_level="medium", + source_links=["https://example.gov.in/rti-response"], + requested_actions=["report", "preserve_evidence"], + constitutional_audit=audit.model_dump(), + ) + assert plan.formation == "NYAYA Audit Vyuha" + assert plan.pramana["verification_status"] == "source_linked_review" + assert plan.dharma["public_authority_signal"] is True + assert "unauthorized_access" in plan.karma["blocked_actions"] + + def test_constitutional_audit_requires_sources_for_public_authority_claim() -> None: audit = build_constitutional_audit( text="RTI public authority contradiction about district health access", @@ -132,6 +154,24 @@ def test_graph_audit_contains_constitutional_action_ledger() -> None: assert "unverified_public_claim" in ledger["blocked_actions"] +def test_graph_vyuha_contains_dharma_pramana_karma_decision_spine() -> None: + graph = DishaAgenticGraph() + result = graph.invoke( + GraphInput( + input_text="RTI Article 12 public authority contradiction", + source_type="rti_record", + source_links=["https://example.gov.in/rti-response"], + requested_actions=["report", "preserve_evidence"], + ) + ) + recommendation = result.vyuha_recommendation + assert recommendation["formation"] == "NYAYA Audit Vyuha" + assert recommendation["pramana"]["verification_status"] == "source_linked_review" + assert recommendation["dharma"]["public_authority_signal"] is True + assert "unverified_public_claim" in recommendation["karma"]["blocked_actions"] + assert recommendation["formation_scores"] + + def test_hse_service_gap_output() -> None: output = v4_6_hse.run("health welfare education district gap") assert output.version == "4.6" From 1b63eb6a1993cdc568fc41548c44be95dead2697 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 13:59:11 +0530 Subject: [PATCH 12/26] Build India civic operations dashboard --- web/app/dashboard/dashboard.module.css | 468 ++++++++++++++----------- web/app/dashboard/page.tsx | 457 ++++++++++++------------ 2 files changed, 496 insertions(+), 429 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index e9051622..154d9859 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -1,82 +1,121 @@ .page { min-height: 100vh; - background: #f4f1e8; - color: #1f1c16; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(246, 239, 221, 0.82)), + #f6efe0; + color: #211d16; font-family: Arial, Helvetica, sans-serif; } .shell { - width: min(1500px, calc(100% - 28px)); + width: min(1760px, calc(100% - 28px)); margin: 0 auto; padding: 18px 0 34px; } .hero, .filterBar, -.kpiCard, .panel, -.problemCard { - border: 1px solid #242016; - background: #fffdf8; +.atlasPanel, +.contextPanel, +.kpiCard, +.heroPanel { + border: 1px solid rgba(36, 31, 22, 0.28); + background: rgba(255, 253, 247, 0.94); + box-shadow: 0 18px 42px rgba(85, 65, 28, 0.08); } .hero { display: grid; - grid-template-columns: minmax(0, 1fr) 360px; + grid-template-columns: minmax(0, 1fr) 390px; gap: 14px; - padding: 22px; + min-height: 330px; + padding: 24px; + background: + linear-gradient(115deg, rgba(255, 253, 247, 0.96) 0%, rgba(255, 253, 247, 0.9) 58%, rgba(244, 178, 64, 0.22) 100%), + #fffdf7; +} + +.heroCopy { + display: flex; + flex-direction: column; + justify-content: flex-end; } .eyebrow, -.panelHeader p, .filterTitle, +.selectControl span, .kpiCard span, -.problemCard span { +.panelHeader p, +.heroPanel span, +.leadBox span, +.territoryCard span, +.protection span { margin: 0; - color: #6d6046; - font-size: 0.76rem; - font-weight: 800; - letter-spacing: 0.07em; + color: #756445; + font-size: 0.73rem; + font-weight: 900; + letter-spacing: 0.08em; text-transform: uppercase; } .hero h1 { - max-width: 980px; + max-width: 1120px; margin: 8px 0 0; font-family: Georgia, "Times New Roman", serif; - font-size: clamp(2rem, 4.6vw, 4.8rem); - line-height: 0.98; + font-size: clamp(2.45rem, 5.6vw, 6.25rem); + font-weight: 700; + line-height: 0.96; letter-spacing: 0; } -.hero p, -.problemCard p { - max-width: 900px; - margin: 14px 0 0; - color: #3f382b; - font-size: 1rem; - line-height: 1.55; +.hero p { + max-width: 860px; + margin: 18px 0 0; + color: #463a29; + font-size: clamp(1rem, 1.3vw, 1.2rem); + line-height: 1.56; } -.problemCard { +.heroPanel { display: flex; - min-height: 100%; flex-direction: column; justify-content: space-between; - background: #f7d56f; + min-height: 100%; padding: 18px; + background: + linear-gradient(180deg, rgba(255, 214, 116, 0.92), rgba(255, 244, 211, 0.92)), + #f8ca62; +} + +.heroPanel div:first-child { + display: flex; + gap: 8px; + align-items: center; } -.problemCard strong { - margin-top: 24px; +.heroPanel svg { + width: 21px; + height: 21px; +} + +.heroPanel strong { + display: block; + margin: 34px 0 0; font-family: Georgia, "Times New Roman", serif; - font-size: 1.8rem; - line-height: 1.05; + font-size: clamp(2rem, 4vw, 3.4rem); + line-height: 0.98; +} + +.heroPanel p { + margin-top: 18px; + color: #352715; + font-size: 0.98rem; } .filterBar { display: grid; - grid-template-columns: 130px repeat(4, minmax(0, 1fr)); + grid-template-columns: 150px repeat(3, minmax(0, 1fr)); gap: 10px; margin: 12px 0; padding: 12px; @@ -90,36 +129,43 @@ .filterTitle svg, .panelHeader svg, -.barRow svg { +.kpiCard svg, +.domainList svg, +.territoryCard svg { width: 20px; height: 20px; - stroke-width: 1.8; -} - -.filterBar button { - min-height: 58px; - border: 1px solid #242016; - background: #f8f1dc; - color: #1f1c16; - text-align: left; + stroke-width: 1.9; } -.filterBar button span, -.filterBar button strong { - display: block; - padding: 0 12px; +.selectControl { + position: relative; + display: grid; + gap: 5px; + min-height: 68px; + border: 1px solid rgba(36, 31, 22, 0.32); + background: #fff7df; + padding: 9px 12px; } -.filterBar button span { - color: #6d6046; - font-size: 0.72rem; +.selectControl select { + width: 100%; + appearance: none; + border: 0; + background: transparent; + color: #211d16; + font: inherit; + font-size: 1rem; font-weight: 800; - text-transform: uppercase; + outline: none; } -.filterBar button strong { - margin-top: 7px; - font-size: 0.95rem; +.selectControl svg { + position: absolute; + right: 10px; + bottom: 13px; + width: 18px; + height: 18px; + pointer-events: none; } .kpiGrid { @@ -130,31 +176,40 @@ } .kpiCard { - min-height: 132px; - padding: 16px; + min-height: 146px; + padding: 15px; + background: #fffdf7; +} + +.kpiCard svg { + margin-bottom: 14px; + color: #8e6025; } .kpiCard strong { display: block; - margin-top: 18px; + margin-top: 10px; font-family: Georgia, "Times New Roman", serif; - font-size: 2.25rem; + font-size: clamp(2.05rem, 3vw, 3rem); + line-height: 1; } .kpiCard p { margin: 8px 0 0; - color: #4b4232; + color: #514634; font-size: 0.9rem; + line-height: 1.35; } -.analyticsGrid, -.midGrid { +.mapGrid { display: grid; - grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr); + grid-template-columns: minmax(0, 1fr) 360px; gap: 12px; margin-bottom: 12px; } +.atlasPanel, +.contextPanel, .panel { padding: 18px; } @@ -170,217 +225,199 @@ .panelHeader h2 { margin: 6px 0 0; font-family: Georgia, "Times New Roman", serif; - font-size: 1.45rem; - line-height: 1.12; + font-size: clamp(1.4rem, 2vw, 2rem); + line-height: 1.08; } -.barList { - display: grid; - gap: 13px; +.panelHeader svg { + color: #8e6025; } -.barRow { +.territoryGrid { display: grid; - grid-template-columns: minmax(190px, 1fr) 44px minmax(0, 1.6fr); - gap: 12px; - align-items: center; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 9px; } -.barRow div:first-child { +.territoryCard { display: flex; - gap: 8px; - align-items: center; + flex-direction: column; + min-height: 166px; + border: 1px solid rgba(36, 31, 22, 0.34); + color: #211d16; + padding: 10px; + text-align: left; + transition: + transform 160ms ease, + border-color 160ms ease, + box-shadow 160ms ease; } -.barRow strong { - text-align: right; +.territoryCard:hover, +.territoryCard:focus-visible { + border-color: #211d16; + box-shadow: 0 14px 26px rgba(64, 45, 18, 0.16); + outline: none; + transform: translateY(-2px); } -.barTrack { - overflow: hidden; - height: 18px; - border: 1px solid #242016; - background: #f4f1e8; +.territoryCard div { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: center; } -.barTrack span { +.territoryCard strong { display: block; - height: 100%; + margin-top: 15px; + font-family: Georgia, "Times New Roman", serif; + font-size: 1.17rem; + line-height: 1.08; } -.strong { - background: #1f1c16; +.territoryCard p { + margin: 8px 0 0; + color: #3f3425; + font-size: 0.86rem; + line-height: 1.3; } -.warm { - background: #caa23e; +.territoryCard small { + display: block; + margin-top: auto; + color: #544632; + font-size: 0.76rem; + font-weight: 800; } -.soft { - background: #e7c967; +.critical { + background: #f4a58d; } -.pale { - background: #efe2b2; +.high { + background: #f3ca64; } -.donutWrap { - display: grid; - grid-template-columns: 170px minmax(0, 1fr); - gap: 20px; - align-items: center; +.watch { + background: #b9dc9b; } -.donut { - width: 168px; - height: 168px; - border: 1px solid #242016; - border-radius: 50%; - background: conic-gradient( - #1f1c16 0deg 154deg, - #caa23e 154deg 266deg, - #e7c967 266deg 327deg, - #efe2b2 327deg 360deg - ); - box-shadow: inset 0 0 0 42px #fffdf8; +.stable { + background: #a9d8d0; } -.legend { - display: grid; - gap: 10px; +.contextPanel { + background: #fffdf7; } -.legend div { +.protectionStack { display: grid; - grid-template-columns: 44px minmax(0, 1fr); gap: 10px; - border-top: 1px solid rgba(36, 32, 22, 0.25); - padding-top: 10px; } -.legend span { - font-weight: 900; +.protection { + border-top: 1px solid rgba(36, 31, 22, 0.22); + padding-top: 12px; } -.legend p { - margin: 0; - color: #4b4232; - line-height: 1.35; -} - -.regionGrid { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 10px; -} - -.regionCard { - min-height: 146px; - border: 1px solid #242016; - background: #f8f1dc; - padding: 12px; +.protection p, +.leadBox p { + margin: 7px 0 0; + color: #443829; + line-height: 1.45; } -.regionCard span { - font-size: 0.75rem; - font-weight: 900; - text-transform: uppercase; +.leadBox { + margin-top: 18px; + border: 1px solid rgba(36, 31, 22, 0.3); + background: #f8f0d9; + padding: 14px; } -.regionCard strong { +.leadBox strong { display: block; - margin-top: 16px; + margin-top: 12px; font-family: Georgia, "Times New Roman", serif; - font-size: 1.45rem; -} - -.regionCard p, -.regionCard small { - display: block; - margin: 8px 0 0; - color: #4b4232; - line-height: 1.35; + font-size: 1.75rem; + line-height: 1.05; } -.decisionList { +.analyticsGrid { display: grid; - gap: 10px; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.65fr); + gap: 12px; } -.decisionList div { - display: flex; - gap: 10px; - align-items: flex-start; - border-top: 1px solid rgba(36, 32, 22, 0.25); - padding-top: 11px; +.barList { + display: grid; + gap: 11px; } -.decisionList span { - width: 11px; - height: 11px; - flex: 0 0 auto; - margin-top: 3px; - border: 1px solid #242016; - background: #f7d56f; +.barRow { + display: grid; + grid-template-columns: 130px 56px minmax(0, 1fr); + gap: 12px; + align-items: center; } -.decisionList p { - margin: 0; - line-height: 1.45; +.barRow span { + font-weight: 900; } -.tableWrap { - overflow-x: auto; +.barRow strong { + text-align: right; } -.tableWrap table { - width: 100%; - border-collapse: collapse; - min-width: 880px; +.barRow div { + overflow: hidden; + height: 20px; + border: 1px solid rgba(36, 31, 22, 0.35); + background: #f7efd7; } -.tableWrap th, -.tableWrap td { - border-top: 1px solid rgba(36, 32, 22, 0.28); - padding: 12px 10px; - text-align: left; - vertical-align: top; +.barRow i { + display: block; + height: 100%; + max-width: 100%; + background: linear-gradient(90deg, #8abf88, #f0c456, #e99878); } -.tableWrap th { - color: #6d6046; - font-size: 0.72rem; - font-weight: 900; - text-transform: uppercase; +.domainList { + display: grid; + gap: 10px; } -.tableWrap td { - color: #29241b; - line-height: 1.35; +.domainList div { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) 52px; + gap: 10px; + align-items: center; + border-top: 1px solid rgba(36, 31, 22, 0.22); + padding-top: 11px; } -.highRisk, -.mediumRisk { - display: inline-block; - border: 1px solid #242016; - padding: 4px 8px; - font-size: 0.78rem; - font-weight: 900; - text-transform: uppercase; +.domainList span { + color: #352d21; + font-weight: 800; } -.highRisk { - background: #f7d56f; +.domainList strong { + font-family: Georgia, "Times New Roman", serif; + font-size: 1.3rem; + text-align: right; } -.mediumRisk { - background: #f8f1dc; +@media (max-width: 1480px) { + .territoryGrid { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } } -@media (max-width: 1180px) { +@media (max-width: 1220px) { .hero, - .analyticsGrid, - .midGrid { + .mapGrid, + .analyticsGrid { grid-template-columns: 1fr; } @@ -388,36 +425,43 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } - .kpiGrid, - .regionGrid { + .kpiGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } + + .territoryGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } } -@media (max-width: 720px) { +@media (max-width: 820px) { .shell { - width: min(100% - 18px, 1500px); + width: min(100% - 18px, 1760px); padding-top: 10px; } .hero, - .panel, - .problemCard { + .atlasPanel, + .contextPanel, + .panel { padding: 14px; } .filterBar, .kpiGrid, - .regionGrid, - .donutWrap { + .territoryGrid { grid-template-columns: 1fr; } + .territoryCard { + min-height: 132px; + } + .barRow { - grid-template-columns: 1fr 40px; + grid-template-columns: 1fr 44px; } - .barTrack { + .barRow div { grid-column: 1 / -1; } } diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index e134a85b..dbab4bea 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -1,132 +1,161 @@ +"use client"; + +import { useMemo, useState } from "react"; import { AlertTriangle, - BarChart3, - Building2, CheckCircle2, + ChevronDown, ClipboardList, - Database, + FileSearch, Filter, + Gavel, Landmark, MapPinned, + Route, ShieldCheck, - Stethoscope, - Waves, + Sparkles, } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import styles from "./dashboard.module.css"; -const filters = [ - { label: "Region", value: "All India" }, - { label: "Domain", value: "All DISHA modules" }, - { label: "Evidence", value: "Verified + review" }, - { label: "Priority", value: "High and medium" }, -]; +type Region = "North" | "South" | "East" | "West" | "Central" | "North East"; +type TerritoryKind = "State" | "Union Territory"; +type Priority = "Critical" | "High" | "Watch" | "Stable"; +type Domain = "Constitutional" | "Service" | "Resilience" | "Evidence"; -const kpis = [ - { label: "Active public-interest cases", value: "86", delta: "+12 this week" }, - { label: "Evidence confidence", value: "74%", delta: "source-linked" }, - { label: "Unresolved claims", value: "19", delta: "need verification" }, - { label: "Human approval queue", value: "7", delta: "policy-gated" }, - { label: "Average risk score", value: "0.62", delta: "medium posture" }, -]; +type Territory = { + name: string; + kind: TerritoryKind; + region: Region; + domain: Domain; + priority: Priority; + cases: number; + evidence: number; + approval: number; + note: string; +}; -const domains = [ - { name: "National audit", value: 24, tone: "strong" }, - { name: "HSE service gaps", value: 19, tone: "warm" }, - { name: "Infrastructure resilience", value: 16, tone: "soft" }, - { name: "Geospatial public safety", value: 13, tone: "soft" }, - { name: "Edge telemetry", value: 8, tone: "pale" }, - { name: "Gap closure", value: 6, tone: "pale" }, +const territories: Territory[] = [ + { name: "Andhra Pradesh", kind: "State", region: "South", domain: "Service", priority: "High", cases: 24, evidence: 73, approval: 5, note: "district service access" }, + { name: "Arunachal Pradesh", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 10, evidence: 62, approval: 3, note: "border-area public assets" }, + { name: "Assam", kind: "State", region: "North East", domain: "Resilience", priority: "Critical", cases: 31, evidence: 69, approval: 8, note: "flood and relief records" }, + { name: "Bihar", kind: "State", region: "East", domain: "Service", priority: "High", cases: 29, evidence: 66, approval: 7, note: "welfare delivery gaps" }, + { name: "Chhattisgarh", kind: "State", region: "Central", domain: "Evidence", priority: "Watch", cases: 18, evidence: 64, approval: 5, note: "field evidence review" }, + { name: "Goa", kind: "State", region: "West", domain: "Evidence", priority: "Stable", cases: 7, evidence: 81, approval: 1, note: "public record sampling" }, + { name: "Gujarat", kind: "State", region: "West", domain: "Resilience", priority: "High", cases: 25, evidence: 76, approval: 4, note: "coastal resilience" }, + { name: "Haryana", kind: "State", region: "North", domain: "Constitutional", priority: "Watch", cases: 16, evidence: 70, approval: 4, note: "authority-response review" }, + { name: "Himachal Pradesh", kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 21, evidence: 68, approval: 6, note: "landslide asset risk" }, + { name: "Jharkhand", kind: "State", region: "East", domain: "Service", priority: "High", cases: 23, evidence: 63, approval: 6, note: "education and welfare access" }, + { name: "Karnataka", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 19, evidence: 78, approval: 3, note: "open-data reconciliation" }, + { name: "Kerala", kind: "State", region: "South", domain: "Resilience", priority: "Watch", cases: 15, evidence: 82, approval: 2, note: "water and health continuity" }, + { name: "Madhya Pradesh", kind: "State", region: "Central", domain: "Service", priority: "High", cases: 27, evidence: 65, approval: 6, note: "district gap closure" }, + { name: "Maharashtra", kind: "State", region: "West", domain: "Constitutional", priority: "High", cases: 33, evidence: 79, approval: 5, note: "large-scale public records" }, + { name: "Manipur", kind: "State", region: "North East", domain: "Evidence", priority: "Critical", cases: 22, evidence: 57, approval: 9, note: "verification-gated claims" }, + { name: "Meghalaya", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 11, evidence: 67, approval: 3, note: "terrain and service continuity" }, + { name: "Mizoram", kind: "State", region: "North East", domain: "Service", priority: "Watch", cases: 9, evidence: 66, approval: 2, note: "health access records" }, + { name: "Nagaland", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 8, evidence: 61, approval: 3, note: "source review needed" }, + { name: "Odisha", kind: "State", region: "East", domain: "Resilience", priority: "High", cases: 26, evidence: 72, approval: 5, note: "cyclone and welfare records" }, + { name: "Punjab", kind: "State", region: "North", domain: "Service", priority: "Watch", cases: 14, evidence: 74, approval: 3, note: "public service queue" }, + { name: "Rajasthan", kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 28, evidence: 71, approval: 6, note: "water and rural access" }, + { name: "Sikkim", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 6, evidence: 70, approval: 1, note: "mountain asset continuity" }, + { name: "Tamil Nadu", kind: "State", region: "South", domain: "Constitutional", priority: "High", cases: 30, evidence: 80, approval: 4, note: "public authority records" }, + { name: "Telangana", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 17, evidence: 77, approval: 3, note: "audit-source matching" }, + { name: "Tripura", kind: "State", region: "North East", domain: "Service", priority: "Watch", cases: 8, evidence: 64, approval: 2, note: "service record review" }, + { name: "Uttar Pradesh", kind: "State", region: "North", domain: "Service", priority: "Critical", cases: 42, evidence: 67, approval: 10, note: "large district backlog" }, + { name: "Uttarakhand", kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 20, evidence: 69, approval: 5, note: "hill infrastructure risk" }, + { name: "West Bengal", kind: "State", region: "East", domain: "Constitutional", priority: "High", cases: 27, evidence: 73, approval: 5, note: "record contradiction review" }, + { name: "Andaman and Nicobar Islands", kind: "Union Territory", region: "South", domain: "Resilience", priority: "Watch", cases: 5, evidence: 68, approval: 1, note: "island continuity" }, + { name: "Chandigarh", kind: "Union Territory", region: "North", domain: "Constitutional", priority: "Stable", cases: 4, evidence: 83, approval: 1, note: "urban record audit" }, + { name: "Dadra and Nagar Haveli and Daman and Diu", kind: "Union Territory", region: "West", domain: "Evidence", priority: "Stable", cases: 5, evidence: 75, approval: 1, note: "record consolidation" }, + { name: "Delhi", kind: "Union Territory", region: "North", domain: "Constitutional", priority: "High", cases: 24, evidence: 81, approval: 4, note: "authority accountability" }, + { name: "Jammu and Kashmir", kind: "Union Territory", region: "North", domain: "Resilience", priority: "High", cases: 18, evidence: 65, approval: 6, note: "public asset continuity" }, + { name: "Ladakh", kind: "Union Territory", region: "North", domain: "Resilience", priority: "Watch", cases: 7, evidence: 63, approval: 2, note: "remote infrastructure" }, + { name: "Lakshadweep", kind: "Union Territory", region: "South", domain: "Resilience", priority: "Watch", cases: 3, evidence: 69, approval: 1, note: "island public services" }, + { name: "Puducherry", kind: "Union Territory", region: "South", domain: "Service", priority: "Stable", cases: 6, evidence: 76, approval: 1, note: "health-service records" }, ]; -const evidenceQuality = [ - { label: "Official / RTI / audit record", value: 43 }, - { label: "Open data / document", value: 31 }, - { label: "Sensor / telemetry", value: 17 }, - { label: "Unresolved / allegation", value: 9 }, +const regions: Array<"All India" | Region> = [ + "All India", + "North", + "South", + "East", + "West", + "Central", + "North East", ]; -const regionalMatrix = [ - { region: "North", risk: "Medium", cases: 17, lead: "Public asset continuity" }, - { region: "East", risk: "High", cases: 22, lead: "Flood and service gaps" }, - { region: "West", risk: "Medium", cases: 14, lead: "Open-data audit" }, - { region: "South", risk: "Medium", cases: 18, lead: "Water and edge telemetry" }, - { region: "Central", risk: "High", cases: 15, lead: "Gap closure backlog" }, +const domains: Array<"All domains" | Domain> = [ + "All domains", + "Constitutional", + "Service", + "Resilience", + "Evidence", ]; -const cases = [ - { - id: "DA-501", - problem: "RTI response conflicts with public authority data", - module: "5.6 National Audit", - evidence: "RTI record", - risk: "High", - action: "Contradiction register", - }, - { - id: "HS-224", - problem: "District health and school access gap", - module: "4.6 HSE", - evidence: "Document", - risk: "Medium", - action: "Service gap report", - }, - { - id: "IR-172", - problem: "Flood impact on bridge and road infrastructure", - module: "2.6 Resilience", - evidence: "Open data", - risk: "High", - action: "Prioritise resilience", - }, - { - id: "GT-118", - problem: "Repeated movement near public-safety perimeter", - module: "1.6 Geospatial", - evidence: "Sensor signal", - risk: "Medium", - action: "Preserve evidence", - }, - { - id: "GC-066", - problem: "Missing audit trail and delayed corrective action", - module: "6.6 Gap Closure", - evidence: "Audit report", - risk: "High", - action: "Yudh/Vyuha routing", - }, +const priorities: Array<"All priorities" | Priority> = [ + "All priorities", + "Critical", + "High", + "Watch", + "Stable", ]; -const decisions = [ - "Prioritise official records, RTI records, open data, audit records, and consented telemetry.", - "Route every case through evidence class, confidence level, risk score, policy status, and human approval requirement.", - "Block retaliation, unauthorized access, destructive actions, and individual-level surveillance.", - "Treat unsupported claims as verification work, not truth.", -]; - -const moduleIcons = [Landmark, Stethoscope, Waves, MapPinned, Database, ShieldCheck]; +const domainMeta: Record = { + Constitutional: { icon: Landmark, label: "Constitutional audit" }, + Service: { icon: ClipboardList, label: "Service-gap protection" }, + Resilience: { icon: Route, label: "Infrastructure resilience" }, + Evidence: { icon: FileSearch, label: "Evidence verification" }, +}; export default function DashboardPage() { + const [selectedRegion, setSelectedRegion] = useState<(typeof regions)[number]>("All India"); + const [selectedDomain, setSelectedDomain] = useState<(typeof domains)[number]>("All domains"); + const [selectedPriority, setSelectedPriority] = useState<(typeof priorities)[number]>("All priorities"); + + const filtered = useMemo( + () => + territories.filter((territory) => { + const regionMatch = selectedRegion === "All India" || territory.region === selectedRegion; + const domainMatch = selectedDomain === "All domains" || territory.domain === selectedDomain; + const priorityMatch = selectedPriority === "All priorities" || territory.priority === selectedPriority; + return regionMatch && domainMatch && priorityMatch; + }), + [selectedDomain, selectedPriority, selectedRegion], + ); + + const totalCases = filtered.reduce((sum, territory) => sum + territory.cases, 0); + const approvalQueue = filtered.reduce((sum, territory) => sum + territory.approval, 0); + const averageEvidence = Math.round( + filtered.reduce((sum, territory) => sum + territory.evidence, 0) / Math.max(filtered.length, 1), + ); + const criticalCount = filtered.filter((territory) => territory.priority === "Critical").length; + const leadTerritory = [...filtered].sort((a, b) => b.cases - a.cases)[0]; + const regionRows = summarizeByRegion(filtered); + const domainRows = summarizeByDomain(filtered); + return (
-
-

DISHA Public-Interest Intelligence

-

Operational analytics for evidence, risk, and gap closure

+
+

DISHA Bharat Operations View

+

Constitutional audit and public protection across India

- DISHA helps an operator turn fragmented public-sector signals into - source-linked cases: what happened, how strong the evidence is, what - risk exists, and what lawful action should happen next. + A civic command view for source-linked public evidence, service gaps, + resilience risk, and lawful action. The screen is demo operational + data only; it is not a government claim.

-
- Problem being solved - Public decisions without reliable evidence +
+
+
+ {selectedRegion}

- The dashboard tracks gaps, contradictions, service failures, and - resilience risks without exposing implementation internals or - collecting personal data. + {filtered.length} states and union territories in view. Every tile + keeps weak evidence away from public truth until review is complete.

@@ -136,160 +165,126 @@ export default function DashboardPage() {
-
- {kpis.map((kpi) => ( -
- {kpi.label} - {kpi.value} -

{kpi.delta}

-
- ))} +
+ + + + +
-
-
- -
- {domains.map((domain, index) => { - const Icon = moduleIcons[index]; +
+
+ +
+ {filtered.map((territory) => { + const Icon = domainMeta[territory.domain].icon; return ( -
+
+ {territory.name} +

{territory.note}

+ {territory.cases} cases | {territory.evidence}% evidence + ); })}
-
- -
-
+
+ Highest current load + {leadTerritory?.name ?? "No territory"} +

{leadTerritory ? `${leadTerritory.cases} demo cases in ${domainMeta[leadTerritory.domain].label.toLowerCase()}.` : "Adjust filters to restore a view."}

+
+
-
+
- -
- {regionalMatrix.map((row) => ( -
+ +
+ {regionRows.map((row) => ( +
{row.region} - {row.risk} -

{row.cases} cases

- {row.lead} + {row.cases} +
))}
- -
- {decisions.map((decision) => ( -
- -

{decision}

-
- ))} + +
+ {domainRows.map((row) => { + const Icon = domainMeta[row.domain].icon; + return ( +
+
+ ); + })}
- -
- -
- - - - - - - - - - - - - {cases.map((item) => ( - - - - - - - - - ))} - -
CaseProblemModuleEvidenceRiskNext action
{item.id}{item.problem}{item.module}{item.evidence} - - {item.risk} - - {item.action}
-
-
); } -function PanelHeader({ - icon: Icon, - eyebrow, - title, +function SelectControl({ + label, + value, + values, + onChange, }: { - icon: typeof BarChart3; - eyebrow: string; - title: string; + label: string; + value: T; + values: readonly T[]; + onChange: (value: T) => void; }) { + return ( + + ); +} + +function Kpi({ label, value, detail, icon: Icon }: { label: string; value: string; detail: string; icon: LucideIcon }) { + return ( +
+
+ ); +} + +function PanelHeader({ icon: Icon, eyebrow, title }: { icon: LucideIcon; eyebrow: string; title: string }) { return (
@@ -300,3 +295,31 @@ function PanelHeader({
); } + +function Protection({ label, text }: { label: string; text: string }) { + return ( +
+ {label} +

{text}

+
+ ); +} + +function summarizeByRegion(items: Territory[]) { + return regions + .filter((region): region is Region => region !== "All India") + .map((region) => ({ + region, + cases: items.filter((item) => item.region === region).reduce((sum, item) => sum + item.cases, 0), + })) + .filter((row) => row.cases > 0); +} + +function summarizeByDomain(items: Territory[]) { + return (Object.keys(domainMeta) as Domain[]) + .map((domain) => ({ + domain, + cases: items.filter((item) => item.domain === domain).reduce((sum, item) => sum + item.cases, 0), + })) + .filter((row) => row.cases > 0); +} From 8d84f79f614ebd76838f7b5a8bc72fb823e8c9a8 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 14:18:28 +0530 Subject: [PATCH 13/26] Make dashboard interactive BI view --- web/app/dashboard/dashboard.module.css | 160 +++++++++++- web/app/dashboard/page.tsx | 332 ++++++++++++++++--------- 2 files changed, 362 insertions(+), 130 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index 154d9859..66d66986 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -113,9 +113,18 @@ font-size: 0.98rem; } +.heroPanel button { + min-height: 44px; + border: 1px solid rgba(36, 31, 22, 0.48); + background: #fffdf7; + color: #211d16; + font-weight: 900; + cursor: pointer; +} + .filterBar { display: grid; - grid-template-columns: 150px repeat(3, minmax(0, 1fr)); + grid-template-columns: 120px repeat(4, minmax(0, 1fr)); gap: 10px; margin: 12px 0; padding: 12px; @@ -131,13 +140,16 @@ .panelHeader svg, .kpiCard svg, .domainList svg, -.territoryCard svg { +.territoryCard svg, +.searchControl svg, +.toolRow svg { width: 20px; height: 20px; stroke-width: 1.9; } -.selectControl { +.selectControl, +.searchControl { position: relative; display: grid; gap: 5px; @@ -147,7 +159,14 @@ padding: 9px 12px; } -.selectControl select { +.searchControl svg { + position: absolute; + right: 12px; + bottom: 14px; +} + +.selectControl select, +.searchControl input { width: 100%; appearance: none; border: 0; @@ -159,6 +178,10 @@ outline: none; } +.searchControl input { + padding-right: 28px; +} + .selectControl svg { position: absolute; right: 10px; @@ -170,7 +193,7 @@ .kpiGrid { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 12px; } @@ -201,7 +224,8 @@ line-height: 1.35; } -.mapGrid { +.mapGrid, +.biGrid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 12px; @@ -233,6 +257,31 @@ color: #8e6025; } +.toolRow { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; +} + +.toolRow button { + display: inline-flex; + gap: 7px; + align-items: center; + min-height: 38px; + border: 1px solid rgba(36, 31, 22, 0.3); + background: #fff7df; + color: #211d16; + padding: 0 11px; + font-weight: 900; + cursor: pointer; +} + +.toolRow .activeTool { + background: #211d16; + color: #fffdf7; +} + .territoryGrid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); @@ -254,13 +303,20 @@ } .territoryCard:hover, -.territoryCard:focus-visible { +.territoryCard:focus-visible, +.selectedCard { border-color: #211d16; box-shadow: 0 14px 26px rgba(64, 45, 18, 0.16); outline: none; transform: translateY(-2px); } +.selectedCard { + box-shadow: + inset 0 0 0 3px rgba(33, 29, 22, 0.78), + 0 14px 26px rgba(64, 45, 18, 0.16); +} + .territoryCard div { display: flex; justify-content: space-between; @@ -311,6 +367,65 @@ background: #fffdf7; } +.drillPanel { + border: 1px solid rgba(36, 31, 22, 0.28); + background: #fffdf7; + padding: 18px; + box-shadow: 0 18px 42px rgba(85, 65, 28, 0.08); +} + +.scoreRing { + display: grid; + place-items: center; + width: 190px; + height: 190px; + margin: 2px auto 18px; + border: 1px solid rgba(36, 31, 22, 0.36); + border-radius: 50%; + background: + radial-gradient(circle at center, #fffdf7 0 50%, transparent 51%), + conic-gradient(#7aaa5d 0 var(--score), #f0e5c8 var(--score) 360deg); +} + +.scoreRing strong { + font-family: Georgia, "Times New Roman", serif; + font-size: 2.5rem; + line-height: 1; +} + +.scoreRing span { + margin-top: -52px; + color: #756445; + font-size: 0.72rem; + font-weight: 900; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.detailList { + display: grid; + gap: 8px; +} + +.detailList div { + display: flex; + justify-content: space-between; + gap: 12px; + border-top: 1px solid rgba(36, 31, 22, 0.2); + padding-top: 10px; +} + +.detailList span { + color: #756445; + font-size: 0.78rem; + font-weight: 900; + text-transform: uppercase; +} + +.detailList strong { + text-align: right; +} + .protectionStack { display: grid; gap: 10px; @@ -345,7 +460,7 @@ .analyticsGrid { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(360px, 0.65fr); + grid-template-columns: minmax(0, 1fr) minmax(340px, 0.7fr) minmax(320px, 0.55fr); gap: 12px; } @@ -408,6 +523,34 @@ text-align: right; } +.priorityStack { + display: grid; + gap: 12px; +} + +.priorityStack div { + display: grid; + grid-template-columns: minmax(0, 1fr) 42px; + gap: 8px; + align-items: center; +} + +.priorityStack span { + font-weight: 900; +} + +.priorityStack strong { + text-align: right; +} + +.priorityStack i { + display: block; + grid-column: 1 / -1; + height: 18px; + border: 1px solid rgba(36, 31, 22, 0.28); + background: linear-gradient(90deg, #f4a58d, #f3ca64, #b9dc9b); +} + @media (max-width: 1480px) { .territoryGrid { grid-template-columns: repeat(5, minmax(0, 1fr)); @@ -417,6 +560,7 @@ @media (max-width: 1220px) { .hero, .mapGrid, + .biGrid, .analyticsGrid { grid-template-columns: 1fr; } diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index dbab4bea..cef1c459 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from "react"; import { AlertTriangle, + ArrowUpDown, CheckCircle2, ChevronDown, ClipboardList, @@ -11,11 +12,13 @@ import { Gavel, Landmark, MapPinned, + RefreshCw, Route, + Search, ShieldCheck, - Sparkles, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; +import type { CSSProperties } from "react"; import styles from "./dashboard.module.css"; @@ -23,6 +26,7 @@ type Region = "North" | "South" | "East" | "West" | "Central" | "North East"; type TerritoryKind = "State" | "Union Territory"; type Priority = "Critical" | "High" | "Watch" | "Stable"; type Domain = "Constitutional" | "Service" | "Resilience" | "Evidence"; +type SortKey = "cases" | "evidence" | "approval"; type Territory = { name: string; @@ -36,6 +40,13 @@ type Territory = { note: string; }; +type LiveTerritory = Territory & { + liveCases: number; + liveEvidence: number; + liveApproval: number; + closure: number; +}; + const territories: Territory[] = [ { name: "Andhra Pradesh", kind: "State", region: "South", domain: "Service", priority: "High", cases: 24, evidence: 73, approval: 5, note: "district service access" }, { name: "Arunachal Pradesh", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 10, evidence: 62, approval: 3, note: "border-area public assets" }, @@ -75,64 +86,77 @@ const territories: Territory[] = [ { name: "Puducherry", kind: "Union Territory", region: "South", domain: "Service", priority: "Stable", cases: 6, evidence: 76, approval: 1, note: "health-service records" }, ]; -const regions: Array<"All India" | Region> = [ - "All India", - "North", - "South", - "East", - "West", - "Central", - "North East", -]; - -const domains: Array<"All domains" | Domain> = [ - "All domains", - "Constitutional", - "Service", - "Resilience", - "Evidence", -]; - -const priorities: Array<"All priorities" | Priority> = [ - "All priorities", - "Critical", - "High", - "Watch", - "Stable", -]; +const regions: Array<"All India" | Region> = ["All India", "North", "South", "East", "West", "Central", "North East"]; +const domains: Array<"All domains" | Domain> = ["All domains", "Constitutional", "Service", "Resilience", "Evidence"]; +const priorities: Array<"All priorities" | Priority> = ["All priorities", "Critical", "High", "Watch", "Stable"]; -const domainMeta: Record = { - Constitutional: { icon: Landmark, label: "Constitutional audit" }, - Service: { icon: ClipboardList, label: "Service-gap protection" }, - Resilience: { icon: Route, label: "Infrastructure resilience" }, - Evidence: { icon: FileSearch, label: "Evidence verification" }, +const domainMeta: Record = { + Constitutional: { icon: Landmark, label: "Constitutional audit", color: "#d8a235" }, + Service: { icon: ClipboardList, label: "Service-gap protection", color: "#7aaa5d" }, + Resilience: { icon: Route, label: "Infrastructure resilience", color: "#d87855" }, + Evidence: { icon: FileSearch, label: "Evidence verification", color: "#5aa6a0" }, }; export default function DashboardPage() { const [selectedRegion, setSelectedRegion] = useState<(typeof regions)[number]>("All India"); const [selectedDomain, setSelectedDomain] = useState<(typeof domains)[number]>("All domains"); const [selectedPriority, setSelectedPriority] = useState<(typeof priorities)[number]>("All priorities"); + const [search, setSearch] = useState(""); + const [sortKey, setSortKey] = useState("cases"); + const [refreshTick, setRefreshTick] = useState(0); + const [selectedTerritory, setSelectedTerritory] = useState("Uttar Pradesh"); - const filtered = useMemo( + const liveTerritories = useMemo( () => - territories.filter((territory) => { + territories.map((territory, index) => { + const wave = ((refreshTick + index * 3) % 7) - 3; + const evidenceWave = ((refreshTick * 2 + index) % 5) - 2; + const liveCases = Math.max(1, territory.cases + wave); + const liveEvidence = clamp(territory.evidence + evidenceWave, 42, 94); + return { + ...territory, + liveCases, + liveEvidence, + liveApproval: Math.max(0, territory.approval + (wave > 1 ? 1 : 0)), + closure: clamp(100 - Math.round(liveCases * 1.3) + Math.round(liveEvidence / 4), 18, 91), + }; + }), + [refreshTick], + ); + + const filtered = useMemo(() => { + const query = search.trim().toLowerCase(); + return liveTerritories + .filter((territory) => { const regionMatch = selectedRegion === "All India" || territory.region === selectedRegion; const domainMatch = selectedDomain === "All domains" || territory.domain === selectedDomain; const priorityMatch = selectedPriority === "All priorities" || territory.priority === selectedPriority; - return regionMatch && domainMatch && priorityMatch; - }), - [selectedDomain, selectedPriority, selectedRegion], - ); + const searchMatch = + !query || + territory.name.toLowerCase().includes(query) || + territory.note.toLowerCase().includes(query) || + territory.region.toLowerCase().includes(query); + return regionMatch && domainMatch && priorityMatch && searchMatch; + }) + .sort((a, b) => getSortValue(b, sortKey) - getSortValue(a, sortKey)); + }, [liveTerritories, search, selectedDomain, selectedPriority, selectedRegion, sortKey]); - const totalCases = filtered.reduce((sum, territory) => sum + territory.cases, 0); - const approvalQueue = filtered.reduce((sum, territory) => sum + territory.approval, 0); - const averageEvidence = Math.round( - filtered.reduce((sum, territory) => sum + territory.evidence, 0) / Math.max(filtered.length, 1), - ); + const selected = + liveTerritories.find((territory) => territory.name === selectedTerritory) ?? + filtered[0] ?? + liveTerritories[0]; + const totalCases = filtered.reduce((sum, territory) => sum + territory.liveCases, 0); + const approvalQueue = filtered.reduce((sum, territory) => sum + territory.liveApproval, 0); + const averageEvidence = Math.round(filtered.reduce((sum, territory) => sum + territory.liveEvidence, 0) / Math.max(filtered.length, 1)); + const averageClosure = Math.round(filtered.reduce((sum, territory) => sum + territory.closure, 0) / Math.max(filtered.length, 1)); const criticalCount = filtered.filter((territory) => territory.priority === "Critical").length; - const leadTerritory = [...filtered].sort((a, b) => b.cases - a.cases)[0]; const regionRows = summarizeByRegion(filtered); const domainRows = summarizeByDomain(filtered); + const priorityRows = summarizeByPriority(filtered); + const lastUpdated = new Date(Date.now() + refreshTick * 60000).toLocaleTimeString("en-IN", { + hour: "2-digit", + minute: "2-digit", + }); return (
@@ -142,124 +166,113 @@ export default function DashboardPage() {

DISHA Bharat Operations View

Constitutional audit and public protection across India

- A civic command view for source-linked public evidence, service gaps, - resilience risk, and lawful action. The screen is demo operational - data only; it is not a government claim. + Interactive BI view for source-linked public evidence, service gaps, + resilience risk, and lawful action. Demo operational data only; not a + government claim.

-
- {selectedRegion} -

- {filtered.length} states and union territories in view. Every tile - keeps weak evidence away from public truth until review is complete. -

+ {lastUpdated} +
+
-
- - - +
+ + + - + +
-
+
- + +
+ + + +
{filtered.map((territory) => { const Icon = domainMeta[territory.domain].icon; + const isSelected = territory.name === selected.name; return ( - ); })}
-
-
- -
- {regionRows.map((row) => ( -
- {row.region} - {row.cases} -
-
- ))} -
-
- -
- -
- {domainRows.map((row) => { - const Icon = domainMeta[row.domain].icon; - return ( -
-
- ); - })} -
-
+ + +
); } -function SelectControl({ - label, - value, - values, - onChange, -}: { - label: string; - value: T; - values: readonly T[]; - onChange: (value: T) => void; -}) { +function SelectControl({ label, value, values, onChange }: { label: string; value: T; values: readonly T[]; onChange: (value: T) => void }) { return (
+
+
+ +
+ + + + +
+
+ {nationalPayload.sources.slice(0, 10).map((source) => ( +
+ {source.layer} + {source.authority} + {source.status.replaceAll("_", " ")} +

{source.dashboardUse}

+
+ ))} +
+
+
+

{payload.sourceNotice} Map geometry is a public GeoJSON asset rendered locally for product demonstration.

diff --git a/web/lib/national-data-registry.ts b/web/lib/national-data-registry.ts new file mode 100644 index 00000000..a251779a --- /dev/null +++ b/web/lib/national-data-registry.ts @@ -0,0 +1,189 @@ +export type SourceStatus = + | "connected" + | "registry_ready" + | "requires_api_key" + | "requires_bulk_import" + | "verify_required"; + +export type NationalSource = { + id: string; + layer: string; + scope: string; + authority: string; + sourceUrl: string; + status: SourceStatus; + updateModel: string; + repoPolicy: string; + dashboardUse: string; +}; + +export const nationalDataSources: NationalSource[] = [ + { + id: "constitutional-president", + layer: "Constitutional offices", + scope: "President of India office, speeches, releases, and official records", + authority: "Rashtrapati Bhavan", + sourceUrl: "https://presidentofindia.gov.in/", + status: "registry_ready", + updateModel: "Official website scrape or API wrapper with provenance hash", + repoPolicy: "Store source manifest and snapshot metadata; never invent current office-holder data", + dashboardUse: "Constitutional authority layer from President to Union executive", + }, + { + id: "constitutional-parliament", + layer: "Constitutional offices", + scope: "Lok Sabha, Rajya Sabha, members, questions, bills, committees, proceedings", + authority: "Parliament of India", + sourceUrl: "https://sansad.in/", + status: "registry_ready", + updateModel: "Scheduled open-page/API ingestion where available", + repoPolicy: "Store bill/session metadata and provenance, not unverifiable summaries", + dashboardUse: "Legislative audit and public authority trace", + }, + { + id: "constitutional-judiciary", + layer: "Constitutional offices", + scope: "Supreme Court public case/status/judgment surfaces", + authority: "Supreme Court of India", + sourceUrl: "https://www.sci.gov.in/", + status: "registry_ready", + updateModel: "Case-law connector with citation and date checks", + repoPolicy: "Store citation metadata and links; mark legal interpretation [VERIFY REQUIRED]", + dashboardUse: "Judicial reference layer for constitutional audit", + }, + { + id: "union-ministry-directory", + layer: "Union ministries and departments", + scope: "Union government ministries, departments, attached offices, and organizations", + authority: "Integrated Government Online Directory", + sourceUrl: "https://igod.gov.in/ug/E002/organizations", + status: "registry_ready", + updateModel: "Directory crawler/API wrapper with change detection", + repoPolicy: "Store organization records with source timestamp", + dashboardUse: "Ministry responsibility routing and escalation map", + }, + { + id: "open-government-data-api", + layer: "Open government datasets", + scope: "Data.gov.in catalogs and APIs across ministries and states", + authority: "Open Government Data Platform India", + sourceUrl: "https://www.data.gov.in/apis", + status: "requires_api_key", + updateModel: "API-key backed ingestion with dataset manifest and checksum", + repoPolicy: "Store connector, schema, and small normalized snapshots; large datasets remain cached artifacts", + dashboardUse: "Open-data evidence, source confidence, and stale-dataset alerts", + }, + { + id: "lgd-national-admin", + layer: "State to village hierarchy", + scope: "States, districts, subdistricts, blocks, villages, local bodies, and panchayat hierarchy", + authority: "Local Government Directory", + sourceUrl: "https://lgdirectory.gov.in/", + status: "requires_bulk_import", + updateModel: "Bulk import from LGD/download/API catalogs with versioned snapshots", + repoPolicy: "Store schema and manifest in repo; store full village-scale snapshot as generated data artifact", + dashboardUse: "Drill-down from India to state, district, block, panchayat, village", + }, + { + id: "panchayat-egovernance", + layer: "Panchayat and rural governance", + scope: "Panchayat planning, local bodies, and rural governance public records", + authority: "Ministry of Panchayati Raj", + sourceUrl: "https://egramswaraj.gov.in/", + status: "registry_ready", + updateModel: "Connector with state/panchayat key mapping through LGD codes", + repoPolicy: "Store panchayat identifiers and source provenance before metrics", + dashboardUse: "Panchayat-level service gap and planning audit", + }, + { + id: "cag-audit-reports", + layer: "Audit and accountability", + scope: "Union and state CAG audit reports, sectors, findings, and report metadata", + authority: "Comptroller and Auditor General of India", + sourceUrl: "https://cag.gov.in/en/audit-report", + status: "registry_ready", + updateModel: "Report index ingestion plus PDF metadata extraction", + repoPolicy: "Store report metadata and extracted citations with page provenance", + dashboardUse: "CAG audit heat map, finding lifecycle, and department accountability", + }, + { + id: "ncrb-crime-reports", + layer: "Crime and cybercrime", + scope: "Crime in India reports, cybercrime tables, state/UT trends, and year-wise comparisons", + authority: "National Crime Records Bureau", + sourceUrl: "https://ncrb.gov.in/crime-in-india-table-additional-table-and-chapter-contents/", + status: "requires_bulk_import", + updateModel: "Year-wise report ingestion; latest official year only, no guessed 2026 totals", + repoPolicy: "Store extracted tables with report year and table citation", + dashboardUse: "Cybercrime trend 2012 onward where official tables are available", + }, + { + id: "cybercrime-portal", + layer: "Cybercrime reporting", + scope: "Cybercrime public awareness, reporting flows, advisories, and available statistics", + authority: "Indian Cyber Crime Coordination Centre / MHA", + sourceUrl: "https://cybercrime.gov.in/", + status: "registry_ready", + updateModel: "Public advisory and statistics connector where officially exposed", + repoPolicy: "No victim/person tracking; aggregate/advisory evidence only", + dashboardUse: "Live advisory lane, cyber awareness, and aggregate reporting context", + }, + { + id: "ndma-disaster", + layer: "Disaster and resilience", + scope: "Disaster guidelines, plans, alerts, mitigation policy, and NDMA public resources", + authority: "National Disaster Management Authority", + sourceUrl: "https://ndma.gov.in/", + status: "registry_ready", + updateModel: "Document/source monitor with disaster category taxonomy", + repoPolicy: "Store guidelines metadata and verified alert references", + dashboardUse: "NDMA disaster layer, resilience risk, and district readiness scan", + }, + { + id: "bhuvan-geospatial", + layer: "Geospatial base map", + scope: "India geospatial services, map APIs, thematic layers, and official map surfaces", + authority: "ISRO NRSC Bhuvan", + sourceUrl: "https://bhuvan-app1.nrsc.gov.in/api/", + status: "registry_ready", + updateModel: "Bhuvan API/service connector with layer registry", + repoPolicy: "Use official geospatial services; avoid sensitive/unauthorized overlays", + dashboardUse: "Geospatial heat maps, district overlays, and public asset context", + }, + { + id: "police-station-open-data", + layer: "Police stations", + scope: "Police station directories where published by official open datasets or state police", + authority: "Data.gov.in and state police sources", + sourceUrl: "https://www.data.gov.in/", + status: "verify_required", + updateModel: "Dataset-by-dataset verification; all-India coverage not assumed", + repoPolicy: "Mark gaps per state; do not fabricate station coverage", + dashboardUse: "Lawful contact/routing layer, not surveillance", + }, +]; + +export function nationalDataCoverage() { + const total = nationalDataSources.length; + const counts = nationalDataSources.reduce>( + (acc, source) => { + acc[source.status] += 1; + return acc; + }, + { + connected: 0, + registry_ready: 0, + requires_api_key: 0, + requires_bulk_import: 0, + verify_required: 0, + }, + ); + + return { + total, + counts, + readyForConnector: counts.registry_ready + counts.requires_api_key + counts.requires_bulk_import, + verifiedLiveSources: counts.connected, + verificationGaps: counts.verify_required, + }; +} From a8d66ab84295bb5f4c4a65050a60536f42a67487 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 16:22:28 +0530 Subject: [PATCH 17/26] Add official national data connectors --- docs/NATIONAL_DATA_OPERATING_LAYER.md | 22 ++ web/app/api/dashboard/connectors/route.ts | 28 +++ web/app/dashboard/dashboard.module.css | 58 ++++- web/app/dashboard/page.tsx | 51 +++++ web/lib/national-connectors.ts | 263 ++++++++++++++++++++++ 5 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 web/app/api/dashboard/connectors/route.ts create mode 100644 web/lib/national-connectors.ts diff --git a/docs/NATIONAL_DATA_OPERATING_LAYER.md b/docs/NATIONAL_DATA_OPERATING_LAYER.md index be7c658e..40f6245b 100644 --- a/docs/NATIONAL_DATA_OPERATING_LAYER.md +++ b/docs/NATIONAL_DATA_OPERATING_LAYER.md @@ -59,3 +59,25 @@ The India scan feed lives in: - `web/app/api/dashboard/india/route.ts` The dashboard must read these sources and show missing layers as missing, not as finished. + +## Connector Contract + +The connector manifest lives in: + +- `web/lib/national-connectors.ts` +- `web/app/api/dashboard/connectors/route.ts` + +Each connector records: + +- official authority +- official endpoint +- connector kind +- refresh cadence +- API-key requirement +- bulk-import requirement +- provenance keys +- update detection method +- safety boundary + +DISHA should treat connector health as source reachability, not data completeness. +Completion requires ingestion, normalization, provenance checks, and dashboard coverage tests. diff --git a/web/app/api/dashboard/connectors/route.ts b/web/app/api/dashboard/connectors/route.ts new file mode 100644 index 00000000..2a1750e9 --- /dev/null +++ b/web/app/api/dashboard/connectors/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { + nationalConnectors, + probeConnector, +} from "@/lib/national-connectors"; + +export const dynamic = "force-dynamic"; + +export async function GET(request: NextRequest) { + const probe = request.nextUrl.searchParams.get("probe") === "1"; + const probes = probe + ? await Promise.all( + nationalConnectors.map(async (connector) => ({ + id: connector.id, + probe: await probeConnector(connector), + })), + ) + : []; + + return NextResponse.json({ + generatedAt: new Date().toISOString(), + notice: + "Official-source connector manifest. Probe results show source reachability, not data completeness.", + connectors: nationalConnectors, + probes, + }); +} diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index 95cc744b..d36136ee 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -525,6 +525,8 @@ .sourceGrid { display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 10px; margin-top: 10px; } @@ -563,6 +565,53 @@ gap: 8px; } +.connectorList { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.connectorList div { + min-height: 164px; + border: 1px solid rgba(157, 247, 242, 0.18); + background: rgba(3, 38, 58, 0.42); + padding: 10px; +} + +.connectorList span { + color: #a8e8e5; + font-size: 0.72rem; + font-weight: 900; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.connectorList strong { + display: block; + margin-top: 9px; + line-height: 1.2; +} + +.connectorList small, +.connectorList em { + display: inline-block; + margin-top: 8px; + border: 1px solid rgba(157, 247, 242, 0.2); + padding: 3px 6px; + color: #efffff; + font-size: 0.7rem; + font-style: normal; + font-weight: 900; + text-transform: uppercase; +} + +.connectorList p { + margin: 8px 0 0; + color: #a8d7d4; + font-size: 0.82rem; + line-height: 1.35; +} + .sourceList div { min-height: 142px; border: 1px solid rgba(157, 247, 242, 0.18); @@ -718,7 +767,8 @@ @media (max-width: 1280px) { .commandHeader, .operationsGrid, - .analyticsGrid { + .analyticsGrid, + .sourceGrid { grid-template-columns: 1fr; } @@ -731,7 +781,8 @@ } .sourceKpis, - .sourceList { + .sourceList, + .connectorList { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @@ -744,7 +795,8 @@ .filterBar, .kpiGrid, .sourceKpis, - .sourceList { + .sourceList, + .connectorList { grid-template-columns: 1fr; } diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 02e5464a..73b9ac39 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -86,6 +86,19 @@ type NationalPayload = { sources: NationalSource[]; }; +type ConnectorPayload = { + connectors: Array<{ + id: string; + label: string; + layer: string; + authority: string; + cadence: string; + needsApiKey: boolean; + needsBulkImport: boolean; + updateDetection: string; + }>; +}; + type Geometry = { type: "Polygon" | "MultiPolygon"; coordinates: number[][][] | number[][][][]; @@ -157,6 +170,10 @@ const fallbackNationalPayload: NationalPayload = { sources: [], }; +const fallbackConnectorPayload: ConnectorPayload = { + connectors: [], +}; + export default function DashboardPage() { const [selectedRegion, setSelectedRegion] = useState<(typeof regions)[number]>("All India"); const [selectedDomain, setSelectedDomain] = useState<(typeof domains)[number]>("All domains"); @@ -166,6 +183,7 @@ export default function DashboardPage() { const [selectedTerritory, setSelectedTerritory] = useState("Uttar Pradesh"); const [payload, setPayload] = useState(fallbackPayload); const [nationalPayload, setNationalPayload] = useState(fallbackNationalPayload); + const [connectorPayload, setConnectorPayload] = useState(fallbackConnectorPayload); const [mapFeatures, setMapFeatures] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); const [refreshNonce, setRefreshNonce] = useState(0); @@ -208,6 +226,21 @@ export default function DashboardPage() { }; }, [refreshNonce]); + useEffect(() => { + let alive = true; + + async function loadConnectors() { + const response = await fetch("/api/dashboard/connectors", { cache: "no-store" }); + const connectorManifest = (await response.json()) as ConnectorPayload; + if (alive) setConnectorPayload(connectorManifest); + } + + loadConnectors().catch(() => setConnectorPayload(fallbackConnectorPayload)); + return () => { + alive = false; + }; + }, [refreshNonce]); + useEffect(() => { let alive = true; @@ -415,6 +448,24 @@ export default function DashboardPage() { ))} +
+ +
+ {connectorPayload.connectors.slice(0, 12).map((connector) => ( +
+ {connector.layer} + {connector.label} + {connector.cadence} +

{connector.updateDetection}

+ + {connector.needsApiKey ? "API key required" : "No API key"} + {" · "} + {connector.needsBulkImport ? "Bulk import" : "Index/probe"} + +
+ ))} +
+

{payload.sourceNotice} Map geometry is a public GeoJSON asset rendered locally for product demonstration.

diff --git a/web/lib/national-connectors.ts b/web/lib/national-connectors.ts new file mode 100644 index 00000000..04c7ffaa --- /dev/null +++ b/web/lib/national-connectors.ts @@ -0,0 +1,263 @@ +export type ConnectorCadence = + | "realtime" + | "hourly" + | "daily" + | "weekly" + | "monthly" + | "on_publication"; + +export type ConnectorKind = + | "html_index" + | "json_api" + | "bulk_dataset" + | "geospatial_service" + | "document_index"; + +export type ConnectorProbe = { + ok: boolean; + status: number | null; + checkedAt: string; + endpoint: string; + etag?: string | null; + lastModified?: string | null; + contentType?: string | null; + error?: string; +}; + +export type NationalConnector = { + id: string; + label: string; + layer: string; + authority: string; + kind: ConnectorKind; + endpoint: string; + method: "HEAD" | "GET"; + cadence: ConnectorCadence; + needsApiKey: boolean; + needsBulkImport: boolean; + provenanceKeys: string[]; + updateDetection: string; + safetyBoundary: string; +}; + +export const nationalConnectors: NationalConnector[] = [ + { + id: "cag-audit-report-index", + label: "CAG audit report index", + layer: "Audit and accountability", + authority: "Comptroller and Auditor General of India", + kind: "html_index", + endpoint: "https://cag.gov.in/en/audit-report", + method: "HEAD", + cadence: "daily", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["report_title", "government_type", "sector", "published_date", "pdf_url", "source_url"], + updateDetection: "Last-Modified/ETag where available, plus report URL hash diff", + safetyBoundary: "Report findings must preserve CAG report citation and page provenance.", + }, + { + id: "union-budget-economic-survey", + label: "Union Budget and Economic Survey", + layer: "Finance", + authority: "Ministry of Finance", + kind: "document_index", + endpoint: "https://www.indiabudget.gov.in/economicsurvey/", + method: "HEAD", + cadence: "on_publication", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["document_name", "year", "table_name", "download_url", "source_url"], + updateDetection: "Publication-year index diff and linked XLS/PDF checksum", + safetyBoundary: "Economic interpretation remains [VERIFY REQUIRED] unless cited to official table.", + }, + { + id: "finance-department-index", + label: "Finance budget public index", + layer: "Finance", + authority: "Department of Economic Affairs / Ministry of Finance", + kind: "html_index", + endpoint: "https://www.indiabudget.gov.in/", + method: "HEAD", + cadence: "daily", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["department_page", "document_title", "published_date", "source_url"], + updateDetection: "Finance department index diff and linked document checksum", + safetyBoundary: "Use official fiscal/economic documents only; no forecast shown as actual.", + }, + { + id: "igod-ministry-directory", + label: "Union ministry and department directory", + layer: "Government directory", + authority: "Integrated Government Online Directory", + kind: "html_index", + endpoint: "https://igod.gov.in/ug/E002/organizations", + method: "HEAD", + cadence: "weekly", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["organization_name", "category", "website", "parent_body", "source_url"], + updateDetection: "Organization list diff with normalized URL identity", + safetyBoundary: "Do not infer department responsibility unless source explicitly maps it.", + }, + { + id: "data-gov-resource-api", + label: "Open Government Data API", + layer: "Open datasets", + authority: "Data.gov.in", + kind: "json_api", + endpoint: "https://api.data.gov.in/resource/9ef84268-d588-465a-a308-a864a43d0070?api-key=579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b&format=json&limit=1", + method: "GET", + cadence: "daily", + needsApiKey: true, + needsBulkImport: false, + provenanceKeys: ["catalog_uuid", "index_name", "updated_date", "org", "sector", "records"], + updateDetection: "API updated_date plus catalog UUID/index-name diff", + safetyBoundary: "Respect data.gov.in dataset ownership and Government Open Data License - India.", + }, + { + id: "lgd-admin-hierarchy", + label: "LGD state to village hierarchy", + layer: "Administrative geography", + authority: "Local Government Directory", + kind: "bulk_dataset", + endpoint: "https://lgdirectory.gov.in/", + method: "HEAD", + cadence: "weekly", + needsApiKey: false, + needsBulkImport: true, + provenanceKeys: ["state_code", "district_code", "subdistrict_code", "block_code", "village_code", "local_body_code", "snapshot_date"], + updateDetection: "Bulk snapshot version and LGD code-level diff", + safetyBoundary: "Administrative units only; no household/person tracking.", + }, + { + id: "egov-panchayat", + label: "Panchayat governance records", + layer: "Panchayat", + authority: "Ministry of Panchayati Raj", + kind: "html_index", + endpoint: "https://egramswaraj.gov.in/", + method: "HEAD", + cadence: "weekly", + needsApiKey: false, + needsBulkImport: true, + provenanceKeys: ["panchayat_code", "plan_year", "scheme", "source_url", "snapshot_date"], + updateDetection: "Panchayat/LGD key mapped snapshot diff", + safetyBoundary: "Show panchayat-level public records only, not individual beneficiaries.", + }, + { + id: "ncrb-crime-in-india", + label: "NCRB Crime in India reports", + layer: "Crime and cybercrime", + authority: "National Crime Records Bureau", + kind: "document_index", + endpoint: "https://ncrb.gov.in/crime-in-india-table-additional-table-and-chapter-contents/", + method: "HEAD", + cadence: "on_publication", + needsApiKey: false, + needsBulkImport: true, + provenanceKeys: ["report_year", "table_id", "chapter", "state", "crime_head", "source_pdf"], + updateDetection: "Report-year index diff and extracted table checksum", + safetyBoundary: "Use official published years only; no guessed current-year crime totals.", + }, + { + id: "cybercrime-advisory", + label: "Cybercrime portal advisory layer", + layer: "Cybercrime advisory", + authority: "Indian Cyber Crime Coordination Centre / MHA", + kind: "html_index", + endpoint: "https://cybercrime.gov.in/", + method: "HEAD", + cadence: "daily", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["advisory_title", "published_date", "source_url", "category"], + updateDetection: "Advisory URL/title diff", + safetyBoundary: "Aggregate advisory context only; no victim/person tracking.", + }, + { + id: "ndma-disaster-resilience", + label: "NDMA disaster and resilience layer", + layer: "Disaster management", + authority: "National Disaster Management Authority", + kind: "document_index", + endpoint: "https://ndma.gov.in/", + method: "HEAD", + cadence: "daily", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["document_title", "disaster_type", "published_date", "source_url"], + updateDetection: "Document/advisory index diff", + safetyBoundary: "Public disaster-resilience indicators only; avoid sensitive response details.", + }, + { + id: "bhuvan-geospatial-services", + label: "Bhuvan geospatial services", + layer: "Geospatial", + authority: "ISRO NRSC Bhuvan", + kind: "geospatial_service", + endpoint: "https://bhuvan-app1.nrsc.gov.in/api/", + method: "HEAD", + cadence: "weekly", + needsApiKey: false, + needsBulkImport: false, + provenanceKeys: ["layer_name", "service_url", "theme", "source_url"], + updateDetection: "Layer registry diff and service URL health", + safetyBoundary: "Use public official layers only; no sensitive or unauthorized overlays.", + }, + { + id: "police-station-directory", + label: "Police station open directory", + layer: "Police station", + authority: "Data.gov.in and state police sources", + kind: "bulk_dataset", + endpoint: "https://www.data.gov.in/", + method: "HEAD", + cadence: "weekly", + needsApiKey: false, + needsBulkImport: true, + provenanceKeys: ["state", "district", "station_name", "source_url", "verified_at"], + updateDetection: "State-source coverage diff and station identity hash", + safetyBoundary: "Public directory routing only; not policing intelligence or surveillance.", + }, +]; + +export async function probeConnector( + connector: NationalConnector, + timeoutMs = 8000, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const checkedAt = new Date().toISOString(); + + try { + const response = await fetch(connector.endpoint, { + method: connector.method, + signal: controller.signal, + cache: "no-store", + headers: { + "user-agent": "DISHA-source-registry/1.0", + }, + }); + return { + ok: response.ok, + status: response.status, + checkedAt, + endpoint: connector.endpoint, + etag: response.headers.get("etag"), + lastModified: response.headers.get("last-modified"), + contentType: response.headers.get("content-type"), + }; + } catch (error) { + return { + ok: false, + status: null, + checkedAt, + endpoint: connector.endpoint, + error: error instanceof Error ? error.message : "Unknown connector probe error", + }; + } finally { + clearTimeout(timeout); + } +} From 966d9ea9e81710df3101b2995d7498895c9709d1 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 20:30:49 +0530 Subject: [PATCH 18/26] Replace dashboard KPI cards with BI visuals --- web/app/dashboard/dashboard.module.css | 825 ++++++++++--------------- web/app/dashboard/page.tsx | 120 ++-- 2 files changed, 392 insertions(+), 553 deletions(-) diff --git a/web/app/dashboard/dashboard.module.css b/web/app/dashboard/dashboard.module.css index d36136ee..74e9861e 100644 --- a/web/app/dashboard/dashboard.module.css +++ b/web/app/dashboard/dashboard.module.css @@ -1,117 +1,116 @@ .page { min-height: 100vh; - background: - radial-gradient(circle at 72% 12%, rgba(58, 226, 216, 0.34), transparent 34%), - linear-gradient(135deg, #0c5562 0%, #0d344d 52%, #19204e 100%); - color: #efffff; - font-family: Arial, Helvetica, sans-serif; + background: #eef1f5; + color: #1f2933; + font-family: "Segoe UI", Arial, Helvetica, sans-serif; } .shell { - width: min(1840px, calc(100% - 24px)); + width: min(1880px, calc(100% - 20px)); margin: 0 auto; - padding: 12px 0 28px; + padding: 10px 0 22px; +} + +.commandHeader { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 10px; + align-items: stretch; + margin-bottom: 10px; } .commandHeader, .filterBar, -.kpiCard, +.measureVisual, .mapPanel, .drillPanel, .panel { - border: 1px solid rgba(157, 247, 242, 0.25); - background: rgba(8, 57, 78, 0.56); - box-shadow: 0 18px 44px rgba(0, 14, 30, 0.28); - backdrop-filter: blur(8px); + border: 1px solid #d7dde6; + background: #ffffff; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08); } -.commandHeader { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 12px; - align-items: stretch; - padding: 16px; +.commandHeader > div:first-child { + padding: 14px 16px; } .eyebrow, .filterTitle, .selectControl span, .searchControl span, -.kpiCard span, .panelHeader p, .scanStatus span, .detailList span, .notice { margin: 0; - color: #756445; - color: #a8e8e5; + color: #5d6b7c; font-size: 0.72rem; - font-weight: 900; - letter-spacing: 0.08em; + font-weight: 700; + letter-spacing: 0.04em; text-transform: uppercase; } .commandHeader h1 { - max-width: 1020px; - margin: 6px 0 0; - font-family: Georgia, "Times New Roman", serif; - font-size: clamp(2rem, 4.4vw, 4.6rem); - line-height: 0.96; + margin: 4px 0 0; + color: #111827; + font-size: clamp(1.7rem, 2.4vw, 2.7rem); + font-weight: 650; + line-height: 1.05; letter-spacing: 0; } .scanStatus { display: grid; - grid-template-columns: 28px minmax(0, 1fr); - gap: 8px 10px; + grid-template-columns: 24px minmax(0, 1fr); + gap: 6px 9px; align-items: center; - border: 1px solid rgba(157, 247, 242, 0.28); - background: rgba(18, 134, 141, 0.46); - padding: 14px; + border-left: 4px solid #f2c811; + padding: 12px; } .scanStatus svg { grid-row: span 2; - width: 24px; - height: 24px; + width: 20px; + height: 20px; + color: #2563eb; } .scanStatus strong { - font-family: Georgia, "Times New Roman", serif; - font-size: 2rem; + color: #111827; + font-size: 1.35rem; + font-weight: 750; } .scanStatus button { grid-column: 1 / -1; - min-height: 40px; - border: 1px solid rgba(157, 247, 242, 0.35); - background: rgba(234, 255, 250, 0.12); - color: #efffff; - font-weight: 900; + min-height: 34px; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #111827; + font-weight: 700; cursor: pointer; } .filterBar { display: grid; - grid-template-columns: 112px repeat(4, minmax(0, 1fr)); - gap: 10px; - margin: 10px 0; + grid-template-columns: 110px repeat(4, minmax(0, 1fr)); + gap: 8px; + margin-bottom: 10px; padding: 10px; } .filterTitle { display: flex; - gap: 7px; + gap: 6px; align-items: center; } .filterTitle svg, .selectControl svg, .searchControl svg, -.kpiCard svg, .panelHeader svg { - width: 20px; - height: 20px; + width: 18px; + height: 18px; stroke-width: 1.9; } @@ -120,10 +119,10 @@ position: relative; display: grid; gap: 4px; - min-height: 62px; - border: 1px solid rgba(157, 247, 242, 0.24); - background: rgba(3, 35, 56, 0.42); - padding: 8px 11px; + min-height: 58px; + border: 1px solid #d7dde6; + background: #f8fafc; + padding: 8px 10px; } .selectControl select, @@ -132,9 +131,9 @@ appearance: none; border: 0; background: transparent; - color: #efffff; + color: #111827; font: inherit; - font-weight: 850; + font-weight: 650; outline: none; } @@ -142,7 +141,8 @@ .searchControl svg { position: absolute; right: 10px; - bottom: 12px; + bottom: 11px; + color: #64748b; pointer-events: none; } @@ -150,34 +150,132 @@ padding-right: 28px; } -.kpiGrid { +.measureGrid { display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr) minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 10px; margin-bottom: 10px; } -.kpiCard { - min-height: 120px; - padding: 13px; +.measureVisual { + min-height: 154px; + padding: 12px; +} + +.heatMatrix { + display: grid; + gap: 8px; +} + +.heatMatrix div { + display: grid; + grid-template-columns: 82px minmax(0, 1fr) 34px; + gap: 8px; + align-items: center; } -.kpiCard svg { - color: #8ce8e4; +.heatMatrix span, +.eventRail span, +.dualGauge p { + color: #64748b; + font-size: 0.8rem; + font-weight: 700; } -.kpiCard strong { +.heatMatrix i { display: block; - margin-top: 12px; - font-family: Georgia, "Times New Roman", serif; - font-size: clamp(1.8rem, 2.8vw, 2.8rem); - line-height: 1; + height: 18px; + border: 1px solid #d7dde6; } -.kpiCard p { - margin: 7px 0 0; - color: #a8d7d4; - font-size: 0.88rem; +.heatMatrix strong { + color: #111827; + text-align: right; +} + +.heatMatrix div[data-priority="Critical"] i { + background: #d64545; +} + +.heatMatrix div[data-priority="High"] i { + background: #f59e0b; +} + +.heatMatrix div[data-priority="Watch"] i { + background: #38a169; +} + +.heatMatrix div[data-priority="Stable"] i { + background: #3b82f6; +} + +.flowVisual { + display: grid; + align-content: end; + min-height: 86px; + gap: 10px; +} + +.flowVisual span { + display: block; + height: 30px; + border: 1px solid #d7dde6; + background: repeating-linear-gradient(90deg, #f2c811 0 12px, #fde68a 12px 18px); +} + +.flowVisual p { + margin: 0; + color: #64748b; + line-height: 1.35; +} + +.dualGauge { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + min-height: 92px; + align-items: end; +} + +.dualGauge div { + display: grid; + justify-items: center; + gap: 6px; + height: 100%; +} + +.dualGauge span { + display: block; + width: 28px; + align-self: end; + border: 1px solid #d7dde6; + background: #2563eb; +} + +.eventRail { + display: grid; + gap: 6px; +} + +.eventRail button { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(100px, 0.8fr); + gap: 8px; + align-items: center; + border: 1px solid #e5e7eb; + background: #ffffff; + color: #111827; + padding: 7px 8px; + text-align: left; + cursor: pointer; +} + +.eventRail i { + color: #475569; + font-size: 0.78rem; + font-style: normal; + font-weight: 650; + text-align: right; } .operationsGrid { @@ -190,330 +288,150 @@ .mapPanel, .drillPanel, .panel { - padding: 15px; + padding: 12px; } .panelHeader { display: flex; justify-content: space-between; - gap: 12px; + gap: 10px; align-items: flex-start; - margin-bottom: 12px; + border-bottom: 1px solid #e5e7eb; + margin-bottom: 10px; + padding-bottom: 8px; } .panelHeader h2 { - margin: 5px 0 0; - font-family: Georgia, "Times New Roman", serif; - font-size: clamp(1.3rem, 1.8vw, 1.9rem); - line-height: 1.08; + margin: 3px 0 0; + color: #111827; + font-size: clamp(1.05rem, 1.35vw, 1.35rem); + font-weight: 700; + line-height: 1.1; } .panelHeader svg { - color: #8ce8e4; + color: #2563eb; } .scanCanvas { position: relative; overflow: hidden; - min-height: min(72vh, 790px); - border: 1px solid rgba(157, 247, 242, 0.18); - background: - radial-gradient(circle at 52% 46%, rgba(33, 231, 228, 0.22), transparent 23%), - radial-gradient(circle at 18% 74%, rgba(120, 71, 174, 0.28), transparent 34%), - linear-gradient(135deg, rgba(11, 108, 126, 0.82), rgba(10, 42, 72, 0.92)); -} - -.scanCanvas::before { - position: absolute; - inset: 0; - z-index: 1; - background-image: - radial-gradient(circle, rgba(205, 255, 249, 0.16) 1px, transparent 1.8px), - linear-gradient(30deg, transparent 0 48%, rgba(142, 240, 236, 0.09) 49% 51%, transparent 52%); - background-size: 18px 18px, 120px 120px; - content: ""; - opacity: 0.65; - pointer-events: none; + min-height: min(66vh, 720px); + border: 1px solid #d7dde6; + background: #f8fafc; } -.scanCanvas::after { - position: absolute; - inset: 0; - z-index: 2; - background: - linear-gradient(90deg, rgba(255, 255, 255, 0.08), transparent 17% 83%, rgba(255, 255, 255, 0.08)), - radial-gradient(circle at 50% 50%, transparent 0 22%, rgba(8, 18, 40, 0.18) 42%, rgba(8, 18, 40, 0.42) 100%); - content: ""; - pointer-events: none; +.scanCanvas::before, +.scanCanvas::after, +.scanLens, +.scanRings { + display: none; } .indiaMap { - position: relative; - z-index: 3; display: block; width: 100%; - height: min(72vh, 790px); - min-height: 560px; - border: 0; - background: transparent; - filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.22)); + height: min(66vh, 720px); + min-height: 520px; + background: #f8fafc; } .mapDistrict { cursor: pointer; - stroke: rgba(195, 255, 251, 0.42); - stroke-width: 0.42; + stroke: #ffffff; + stroke-width: 0.45; transition: - fill 180ms ease, - opacity 180ms ease, - stroke-width 180ms ease; + opacity 120ms ease, + stroke-width 120ms ease; } .mapDistrict[data-priority="Critical"] { - fill: rgba(252, 152, 116, 0.82); + fill: #d64545; } .mapDistrict[data-priority="High"] { - fill: rgba(246, 209, 91, 0.82); + fill: #f59e0b; } .mapDistrict[data-priority="Watch"] { - fill: rgba(105, 210, 176, 0.8); + fill: #38a169; } .mapDistrict[data-priority="Stable"] { - fill: rgba(103, 194, 222, 0.78); + fill: #3b82f6; } .mapMuted { - opacity: 0.13; + opacity: 0.14; } .mapActive { - opacity: 0.92; + opacity: 0.88; } .mapDistrict:hover, .mapDistrict:focus-visible, .mapSelected { opacity: 1; - stroke: #ffffff; + stroke: #111827; stroke-width: 1.4; outline: none; } -.mapSelected { - filter: drop-shadow(0 0 7px rgba(214, 255, 250, 0.82)); -} - -.floatingMetrics { - position: absolute; - top: 22px; - left: 22px; - z-index: 5; - display: grid; - grid-template-columns: repeat(2, 126px); - gap: 10px; -} - -.floatingMetrics div { - min-height: 92px; - border: 1px solid rgba(157, 247, 242, 0.24); - background: rgba(3, 38, 58, 0.55); - padding: 12px; -} - -.floatingMetrics span, -.signalReadout span { - color: #a8e8e5; - font-size: 0.72rem; - font-weight: 900; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.floatingMetrics strong { - display: block; - margin-top: 7px; - font-family: Georgia, "Times New Roman", serif; - font-size: 1.55rem; -} - -.floatingMetrics i, -.floatingMetrics b { - display: block; - height: 24px; - margin-top: 10px; - background: - linear-gradient(90deg, transparent 0 8%, rgba(214, 255, 250, 0.9) 9% 12%, transparent 13% 21%, rgba(214, 255, 250, 0.65) 22% 25%, transparent 26%), - linear-gradient(180deg, transparent 45%, rgba(214, 255, 250, 0.28) 46% 54%, transparent 55%); -} - -.scanLens { - position: absolute; - top: 47%; - left: 50%; - z-index: 6; - width: clamp(128px, 13vw, 190px); - aspect-ratio: 1; - border: 8px solid rgba(13, 10, 20, 0.82); - border-radius: 50%; - background: - radial-gradient(circle at 45% 48%, #ffffff 0 4%, transparent 5%), - radial-gradient(circle at 65% 31%, rgba(255, 255, 255, 0.38) 0 11%, transparent 12%), - radial-gradient(circle at 35% 60%, #5829a6 0 22%, #1d1640 23% 48%, #050713 49% 100%); - box-shadow: - 0 0 0 10px rgba(0, 0, 0, 0.28), - 0 0 36px rgba(105, 231, 228, 0.35); - transform: translate(-50%, -50%); - pointer-events: none; -} - -.scanLens span, -.scanLens strong, -.scanLens i { - position: absolute; - border-radius: 50%; - content: ""; -} - -.scanLens span { - inset: 22%; - border: 1px solid rgba(215, 255, 250, 0.36); -} - -.scanLens strong { - inset: 38%; - background: #0a0714; -} - -.scanLens i { - top: 28%; - left: 30%; - width: 13%; - height: 13%; - background: #ffffff; -} - -.scanRings { - position: absolute; - top: 47%; - left: 50%; - z-index: 5; - width: clamp(320px, 42vw, 640px); - aspect-ratio: 1; - transform: translate(-50%, -50%); - pointer-events: none; -} - -.scanRings span { - position: absolute; - border: 1px solid rgba(186, 255, 251, 0.34); - border-radius: 50%; -} - -.scanRings span:nth-child(1) { inset: 4%; } -.scanRings span:nth-child(2) { inset: 19%; } -.scanRings span:nth-child(3) { inset: 33%; } -.scanRings span:nth-child(4) { inset: 46%; border-style: dashed; } - -.signalReadout { - position: absolute; - right: 26px; - top: 30%; - z-index: 6; - width: min(290px, 34%); - border: 1px solid rgba(157, 247, 242, 0.22); - background: rgba(3, 38, 58, 0.48); - padding: 16px; -} - -.signalReadout strong { - display: block; - font-family: Georgia, "Times New Roman", serif; - font-size: 2.2rem; -} - -.signalReadout p { - margin: 8px 0 0; - color: #d8fffb; -} - -.chartOverlay { - position: absolute; - right: 26px; - bottom: 28px; - z-index: 6; - display: flex; - gap: 6px; - align-items: end; - height: 96px; - width: min(260px, 32%); - border-top: 1px solid rgba(214, 255, 250, 0.32); - border-bottom: 1px solid rgba(214, 255, 250, 0.32); - padding: 10px; -} - -.chartOverlay span { - flex: 1; - min-height: 12px; - background: rgba(214, 255, 250, 0.7); -} - .mapLoading { display: grid; place-items: center; - min-height: 560px; - border: 1px solid rgba(157, 247, 242, 0.18); - background: rgba(3, 38, 58, 0.42); - color: #a8e8e5; - font-weight: 900; + min-height: 520px; + background: #f8fafc; + color: #5d6b7c; + font-weight: 750; text-transform: uppercase; } .scoreRing { display: grid; place-items: center; - width: 190px; - height: 190px; - margin: 4px auto 18px; - border: 1px solid rgba(157, 247, 242, 0.28); + width: 170px; + height: 170px; + margin: 2px auto 14px; border-radius: 50%; background: - radial-gradient(circle at center, rgba(8, 57, 78, 0.96) 0 49%, transparent 50%), - conic-gradient(#87fff2 0 var(--score), rgba(255, 255, 255, 0.13) var(--score) 360deg); + radial-gradient(circle at center, #ffffff 0 50%, transparent 51%), + conic-gradient(#2563eb 0 var(--score), #e5e7eb var(--score) 360deg); } .scoreRing strong { - font-family: Georgia, "Times New Roman", serif; - font-size: 2.55rem; + color: #111827; + font-size: 2.25rem; + font-weight: 760; line-height: 1; } .scoreRing span { - margin-top: -50px; - color: #a8e8e5; + margin-top: -46px; + color: #5d6b7c; font-size: 0.72rem; - font-weight: 900; - letter-spacing: 0.08em; + font-weight: 800; + letter-spacing: 0.04em; text-transform: uppercase; } .detailList { display: grid; - gap: 8px; + gap: 7px; } .detailList div { display: flex; justify-content: space-between; - gap: 14px; - border-top: 1px solid rgba(157, 247, 242, 0.18); - padding-top: 10px; + gap: 12px; + border-top: 1px solid #e5e7eb; + padding-top: 9px; } .detailList strong { max-width: 58%; + color: #111827; text-align: right; } @@ -523,244 +441,190 @@ gap: 10px; } -.sourceGrid { - display: grid; - grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); - gap: 10px; - margin-top: 10px; -} - -.sourceKpis { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; - margin-bottom: 12px; -} - -.sourceKpis div { - border: 1px solid rgba(157, 247, 242, 0.18); - background: rgba(3, 38, 58, 0.42); - padding: 10px; -} - -.sourceKpis span, -.sourceList span { - color: #a8e8e5; - font-size: 0.72rem; - font-weight: 900; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.sourceKpis strong { - display: block; - margin-top: 7px; - font-family: Georgia, "Times New Roman", serif; - font-size: 1.4rem; -} - -.sourceList { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 8px; -} - -.connectorList { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.connectorList div { - min-height: 164px; - border: 1px solid rgba(157, 247, 242, 0.18); - background: rgba(3, 38, 58, 0.42); - padding: 10px; -} - -.connectorList span { - color: #a8e8e5; - font-size: 0.72rem; - font-weight: 900; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.connectorList strong { - display: block; - margin-top: 9px; - line-height: 1.2; -} - -.connectorList small, -.connectorList em { - display: inline-block; - margin-top: 8px; - border: 1px solid rgba(157, 247, 242, 0.2); - padding: 3px 6px; - color: #efffff; - font-size: 0.7rem; - font-style: normal; - font-weight: 900; - text-transform: uppercase; -} - -.connectorList p { - margin: 8px 0 0; - color: #a8d7d4; - font-size: 0.82rem; - line-height: 1.35; -} - -.sourceList div { - min-height: 142px; - border: 1px solid rgba(157, 247, 242, 0.18); - background: rgba(3, 38, 58, 0.42); - padding: 10px; -} - -.sourceList strong { - display: block; - margin-top: 9px; - line-height: 1.2; -} - -.sourceList small { - display: inline-block; - margin-top: 8px; - border: 1px solid rgba(157, 247, 242, 0.2); - padding: 3px 6px; - color: #efffff; - font-size: 0.7rem; - font-weight: 900; - text-transform: uppercase; -} - -.sourceList p { - margin: 8px 0 0; - color: #a8d7d4; - font-size: 0.82rem; - line-height: 1.35; -} - .barList { display: grid; - gap: 10px; + gap: 9px; } .barRow { display: grid; grid-template-columns: 120px 52px minmax(0, 1fr); - gap: 10px; + gap: 9px; align-items: center; } .barRow span { - font-weight: 900; + color: #111827; + font-weight: 700; } .barRow strong { + color: #111827; text-align: right; } .barRow div { overflow: hidden; - height: 20px; - border: 1px solid rgba(157, 247, 242, 0.2); - background: rgba(3, 38, 58, 0.42); + height: 18px; + border: 1px solid #d7dde6; + background: #eef1f5; } .barRow i { display: block; height: 100%; - background: linear-gradient(90deg, #87fff2, #efc44f, #e9866b); + background: #2563eb; } -.domainList { +.domainList, +.scanTable, +.sourceList, +.connectorList { display: grid; - gap: 10px; + gap: 7px; } .domainList div { display: grid; - grid-template-columns: 24px minmax(0, 1fr) 52px; - gap: 10px; + grid-template-columns: 22px minmax(0, 1fr) 50px; + gap: 9px; align-items: center; - border-top: 1px solid rgba(157, 247, 242, 0.18); - padding-top: 10px; + border-top: 1px solid #e5e7eb; + padding-top: 9px; } .domainList span { - font-weight: 850; + color: #111827; + font-weight: 700; } .domainList strong { - font-family: Georgia, "Times New Roman", serif; - font-size: 1.25rem; + color: #111827; + font-size: 1.15rem; text-align: right; } .sortRow { display: flex; flex-wrap: wrap; - gap: 8px; - margin-bottom: 10px; + gap: 7px; + margin-bottom: 9px; } .sortRow button, .activeTool { - min-height: 36px; - border: 1px solid rgba(157, 247, 242, 0.22); - background: rgba(3, 38, 58, 0.42); - color: #efffff; - padding: 0 11px; - font-weight: 900; + min-height: 32px; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #111827; + padding: 0 10px; + font-weight: 700; cursor: pointer; } .sortRow .activeTool { - background: rgba(135, 255, 242, 0.22); - color: #efffff; + border-color: #f2c811; + background: #fff7cc; } .scanTable { - display: grid; - gap: 7px; - max-height: 330px; + max-height: 320px; overflow: auto; } .scanTable button { display: grid; grid-template-columns: minmax(0, 1fr) 44px; - gap: 6px 10px; + gap: 5px 9px; align-items: center; - border: 1px solid rgba(157, 247, 242, 0.18); - background: rgba(3, 38, 58, 0.42); - color: #efffff; - padding: 9px; + border: 1px solid #e5e7eb; + background: #ffffff; + color: #111827; + padding: 8px; text-align: left; cursor: pointer; } .scanTable span { - font-weight: 900; + font-weight: 700; } .scanTable strong { - font-family: Georgia, "Times New Roman", serif; - font-size: 1.2rem; + font-size: 1.1rem; text-align: right; } .scanTable small { grid-column: 1 / -1; - color: #a8d7d4; - font-weight: 800; + color: #64748b; + font-weight: 650; +} + +.sourceGrid { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 10px; + margin-top: 10px; +} + +.sourceList div, +.connectorList div { + border: 1px solid #e5e7eb; + background: #ffffff; + padding: 9px; +} + +.sourceList span, +.connectorList span { + color: #5d6b7c; + font-size: 0.72rem; + font-weight: 750; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.sourceList strong, +.connectorList strong { + display: block; + margin-top: 7px; + color: #111827; + line-height: 1.2; +} + +.sourceList { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.connectorList { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.sourceList small, +.connectorList small, +.connectorList em { + display: inline-block; + margin-top: 7px; + border: 1px solid #d7dde6; + background: #f8fafc; + padding: 3px 6px; + color: #475569; + font-size: 0.68rem; + font-style: normal; + font-weight: 750; + text-transform: uppercase; +} + +.sourceList p, +.connectorList p { + margin: 7px 0 0; + color: #64748b; + font-size: 0.8rem; + line-height: 1.34; } .notice { margin-top: 10px; + color: #64748b; line-height: 1.4; } @@ -776,11 +640,10 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } - .kpiGrid { + .measureGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .sourceKpis, .sourceList, .connectorList { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -789,12 +652,11 @@ @media (max-width: 760px) { .shell { - width: min(100% - 16px, 1840px); + width: min(100% - 14px, 1880px); } .filterBar, - .kpiGrid, - .sourceKpis, + .measureGrid, .sourceList, .connectorList { grid-template-columns: 1fr; @@ -803,12 +665,13 @@ .mapPanel, .drillPanel, .panel, - .commandHeader { - padding: 12px; + .commandHeader > div:first-child { + padding: 10px; } - .indiaMap { - height: 62vh; + .indiaMap, + .scanCanvas { + height: 58vh; min-height: 420px; } @@ -819,18 +682,4 @@ .barRow div { grid-column: 1 / -1; } - - .floatingMetrics, - .signalReadout, - .chartOverlay { - position: relative; - inset: auto; - width: auto; - margin: 10px; - } - - .scanLens, - .scanRings { - display: none; - } } diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 73b9ac39..05c8a8a9 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, - CheckCircle2, ChevronDown, ClipboardList, FileSearch, @@ -286,13 +285,13 @@ export default function DashboardPage() { () => new Set(filtered.map((territory) => normalizeName(territory.name))), [filtered], ); - const totalCases = filtered.reduce((sum, territory) => sum + territory.cases, 0); const approvalQueue = filtered.reduce((sum, territory) => sum + territory.approval, 0); const averageEvidence = Math.round(filtered.reduce((sum, territory) => sum + territory.evidence, 0) / Math.max(filtered.length, 1)); const averageClosure = Math.round(filtered.reduce((sum, territory) => sum + territory.closure, 0) / Math.max(filtered.length, 1)); const criticalCount = filtered.filter((territory) => territory.priority === "Critical").length; const regionRows = summarizeByRegion(filtered); const domainRows = summarizeByDomain(filtered); + const priorityRows = summarizeByPriority(filtered); const lastUpdated = new Date(payload.generatedAt).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", @@ -304,12 +303,12 @@ export default function DashboardPage() {
-

DISHA Bharat Live Scan

-

Agentic India scan dashboard

+

DISHA National Intelligence Report

+

India public accountability dashboard

+
- +
- - - - -
- +
- - - + + +
{filtered.slice(0, 12).map((territory) => ( diff --git a/web/lib/india-dashboard-data.ts b/web/lib/india-dashboard-data.ts index 86a9bc36..38912b7d 100644 --- a/web/lib/india-dashboard-data.ts +++ b/web/lib/india-dashboard-data.ts @@ -17,42 +17,42 @@ export type Territory = { }; export const territories: Territory[] = [ - { name: "Andhra Pradesh", kind: "State", region: "South", domain: "Service", priority: "High", cases: 24, evidence: 73, approval: 5, note: "district service access" }, - { name: "Arunachal Pradesh", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 10, evidence: 62, approval: 3, note: "border-area public assets" }, - { name: "Assam", kind: "State", region: "North East", domain: "Resilience", priority: "Critical", cases: 31, evidence: 69, approval: 8, note: "flood and relief records" }, - { name: "Bihar", kind: "State", region: "East", domain: "Service", priority: "High", cases: 29, evidence: 66, approval: 7, note: "welfare delivery gaps" }, - { name: "Chhattisgarh", kind: "State", region: "Central", domain: "Evidence", priority: "Watch", cases: 18, evidence: 64, approval: 5, note: "field evidence review" }, - { name: "Goa", kind: "State", region: "West", domain: "Evidence", priority: "Stable", cases: 7, evidence: 81, approval: 1, note: "public record sampling" }, - { name: "Gujarat", kind: "State", region: "West", domain: "Resilience", priority: "High", cases: 25, evidence: 76, approval: 4, note: "coastal resilience" }, - { name: "Haryana", kind: "State", region: "North", domain: "Constitutional", priority: "Watch", cases: 16, evidence: 70, approval: 4, note: "authority-response review" }, - { name: "Himachal Pradesh", kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 21, evidence: 68, approval: 6, note: "landslide asset risk" }, - { name: "Jharkhand", kind: "State", region: "East", domain: "Service", priority: "High", cases: 23, evidence: 63, approval: 6, note: "education and welfare access" }, - { name: "Karnataka", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 19, evidence: 78, approval: 3, note: "open-data reconciliation" }, - { name: "Kerala", kind: "State", region: "South", domain: "Resilience", priority: "Watch", cases: 15, evidence: 82, approval: 2, note: "water and health continuity" }, - { name: "Madhya Pradesh", kind: "State", region: "Central", domain: "Service", priority: "High", cases: 27, evidence: 65, approval: 6, note: "district gap closure" }, - { name: "Maharashtra", kind: "State", region: "West", domain: "Constitutional", priority: "High", cases: 33, evidence: 79, approval: 5, note: "large-scale public records" }, - { name: "Manipur", kind: "State", region: "North East", domain: "Evidence", priority: "Critical", cases: 22, evidence: 57, approval: 9, note: "verification-gated claims" }, - { name: "Meghalaya", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 11, evidence: 67, approval: 3, note: "terrain and service continuity" }, - { name: "Mizoram", kind: "State", region: "North East", domain: "Service", priority: "Watch", cases: 9, evidence: 66, approval: 2, note: "health access records" }, - { name: "Nagaland", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 8, evidence: 61, approval: 3, note: "source review needed" }, - { name: "Odisha", aliases: ["Orissa"], kind: "State", region: "East", domain: "Resilience", priority: "High", cases: 26, evidence: 72, approval: 5, note: "cyclone and welfare records" }, - { name: "Punjab", kind: "State", region: "North", domain: "Service", priority: "Watch", cases: 14, evidence: 74, approval: 3, note: "public service queue" }, - { name: "Rajasthan", kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 28, evidence: 71, approval: 6, note: "water and rural access" }, - { name: "Sikkim", kind: "State", region: "North East", domain: "Resilience", priority: "Watch", cases: 6, evidence: 70, approval: 1, note: "mountain asset continuity" }, - { name: "Tamil Nadu", kind: "State", region: "South", domain: "Constitutional", priority: "High", cases: 30, evidence: 80, approval: 4, note: "public authority records" }, - { name: "Telangana", aliases: ["Telengana"], kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 17, evidence: 77, approval: 3, note: "audit-source matching" }, - { name: "Tripura", kind: "State", region: "North East", domain: "Service", priority: "Watch", cases: 8, evidence: 64, approval: 2, note: "service record review" }, - { name: "Uttar Pradesh", kind: "State", region: "North", domain: "Service", priority: "Critical", cases: 42, evidence: 67, approval: 10, note: "large district backlog" }, - { name: "Uttarakhand", aliases: ["Uttaranchal"], kind: "State", region: "North", domain: "Resilience", priority: "High", cases: 20, evidence: 69, approval: 5, note: "hill infrastructure risk" }, - { name: "West Bengal", kind: "State", region: "East", domain: "Constitutional", priority: "High", cases: 27, evidence: 73, approval: 5, note: "record contradiction review" }, - { name: "Andaman and Nicobar Islands", aliases: ["Andaman & Nicobar Island", "Andaman & Nicobar Islands"], kind: "Union Territory", region: "South", domain: "Resilience", priority: "Watch", cases: 5, evidence: 68, approval: 1, note: "island continuity" }, - { name: "Chandigarh", kind: "Union Territory", region: "North", domain: "Constitutional", priority: "Stable", cases: 4, evidence: 83, approval: 1, note: "urban record audit" }, - { name: "Dadra and Nagar Haveli and Daman and Diu", aliases: ["Dadra & Nagar Haveli", "Daman & Diu"], kind: "Union Territory", region: "West", domain: "Evidence", priority: "Stable", cases: 5, evidence: 75, approval: 1, note: "record consolidation" }, - { name: "Delhi", aliases: ["NCT of Delhi"], kind: "Union Territory", region: "North", domain: "Constitutional", priority: "High", cases: 24, evidence: 81, approval: 4, note: "authority accountability" }, - { name: "Jammu and Kashmir", aliases: ["Jammu & Kashmir"], kind: "Union Territory", region: "North", domain: "Resilience", priority: "High", cases: 18, evidence: 65, approval: 6, note: "public asset continuity" }, - { name: "Ladakh", kind: "Union Territory", region: "North", domain: "Resilience", priority: "Watch", cases: 7, evidence: 63, approval: 2, note: "remote infrastructure" }, - { name: "Lakshadweep", kind: "Union Territory", region: "South", domain: "Resilience", priority: "Watch", cases: 3, evidence: 69, approval: 1, note: "island public services" }, - { name: "Puducherry", aliases: ["Pondicherry"], kind: "Union Territory", region: "South", domain: "Service", priority: "Stable", cases: 6, evidence: 76, approval: 1, note: "health-service records" }, + { name: "Andhra Pradesh", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Arunachal Pradesh", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Assam", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Bihar", kind: "State", region: "East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Chhattisgarh", kind: "State", region: "Central", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Goa", kind: "State", region: "West", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Gujarat", kind: "State", region: "West", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Haryana", kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Himachal Pradesh", kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Jharkhand", kind: "State", region: "East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Karnataka", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Kerala", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Madhya Pradesh", kind: "State", region: "Central", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Maharashtra", kind: "State", region: "West", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Manipur", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Meghalaya", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Mizoram", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Nagaland", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Odisha", aliases: ["Orissa"], kind: "State", region: "East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Punjab", kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Rajasthan", kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Sikkim", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Tamil Nadu", kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Telangana", aliases: ["Telengana"], kind: "State", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Tripura", kind: "State", region: "North East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Uttar Pradesh", kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Uttarakhand", aliases: ["Uttaranchal"], kind: "State", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "West Bengal", kind: "State", region: "East", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Andaman and Nicobar Islands", aliases: ["Andaman & Nicobar Island", "Andaman & Nicobar Islands"], kind: "Union Territory", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Chandigarh", kind: "Union Territory", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Dadra and Nagar Haveli and Daman and Diu", aliases: ["Dadra & Nagar Haveli", "Daman & Diu"], kind: "Union Territory", region: "West", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Delhi", aliases: ["NCT of Delhi"], kind: "Union Territory", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Jammu and Kashmir", aliases: ["Jammu & Kashmir"], kind: "Union Territory", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Ladakh", kind: "Union Territory", region: "North", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Lakshadweep", kind: "Union Territory", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, + { name: "Puducherry", aliases: ["Pondicherry"], kind: "Union Territory", region: "South", domain: "Evidence", priority: "Watch", cases: 0, evidence: 0, approval: 1, note: "Official source coverage pending." }, ]; export const regions: Array<"All India" | Region> = ["All India", "North", "South", "East", "West", "Central", "North East"]; From 3134ca7a069f278f3f313cffc1688d0c87bbbaa3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 22:16:52 +0530 Subject: [PATCH 21/26] Add CAG audit report connector --- docs/CAG_AUDIT_CONNECTOR.md | 78 +++++++ web/app/api/dashboard/cag/route.ts | 56 +++++ web/lib/cag-audit-connector.ts | 344 +++++++++++++++++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 docs/CAG_AUDIT_CONNECTOR.md create mode 100644 web/app/api/dashboard/cag/route.ts create mode 100644 web/lib/cag-audit-connector.ts diff --git a/docs/CAG_AUDIT_CONNECTOR.md b/docs/CAG_AUDIT_CONNECTOR.md new file mode 100644 index 00000000..9e1a4926 --- /dev/null +++ b/docs/CAG_AUDIT_CONNECTOR.md @@ -0,0 +1,78 @@ +# CAG Audit Connector + +DISHA must treat CAG material as official audit evidence, not as dashboard decoration. + +The connector at `/api/dashboard/cag` fetches the public CAG audit-report index and returns source-backed report metadata: + +- report date +- report year +- government/state/UT label where present +- report type +- title +- official detail URL +- official PDF URL +- summary snippet from CAG +- sector labels +- DISHA topic tags + +The connector covers report-level metadata first. It does not invent audit findings. + +## Topics + +Current topic tags are keyword-based and conservative: + +- `digital_governance` +- `ai_data_systems` +- `disaster_ndma_flood` +- `security_terror_border` +- `infrastructure_collapse` +- `financial_audit` +- `health_welfare_education` +- `local_bodies` + +These are routing tags, not final findings. + +## API + +Example: + +```text +/api/dashboard/cag?yearFrom=2015&yearTo=2026&topics=financial_audit,disaster_ndma_flood&startPage=1&maxPages=5 +``` + +`maxPages` is capped at 25 per request so the application does not overload the public site. Use `startPage` to walk the CAG index in batches. A full backfill from 2015 onward should run as a scheduled ingest job that stores checksums and source timestamps. + +## Finding Discipline + +Every record currently has: + +```text +extractionStatus: requires_pdf_extraction +``` + +That is intentional. + +Department-wise CAG flags, audit objections, losses, irregularities, disaster gaps, Digital India failures, cyber/data issues, infrastructure failures, and financial audit findings must come from PDF text extraction with: + +- report title +- report URL +- PDF URL +- page number +- quoted finding excerpt within copyright limits +- extracted table/paragraph reference where available +- verification timestamp + +Until that exists, DISHA may show that a relevant CAG report exists. It must not claim that a specific finding has been proven. + +## Production Path + +1. Fetch index metadata from CAG. +2. Store report identity, date, sector, type, detail URL, and PDF URL. +3. Download PDF as a versioned artifact. +4. Extract text and tables. +5. Produce finding records with page provenance. +6. Classify finding domain: finance, disaster, digital governance, infrastructure, HSE, security, local body. +7. Attach each finding to the Constitutional Action Ledger. +8. Expose only source-linked findings in dashboard/API. + +This keeps DISHA evidence-first and prevents false audit claims. diff --git a/web/app/api/dashboard/cag/route.ts b/web/app/api/dashboard/cag/route.ts new file mode 100644 index 00000000..65967726 --- /dev/null +++ b/web/app/api/dashboard/cag/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { + defaultCagTopics, + fetchCagAuditRecords, + type CagAuditTopic, +} from "@/lib/cag-audit-connector"; + +export const dynamic = "force-dynamic"; + +const allowedTopics = new Set(defaultCagTopics.concat([ + "health_welfare_education", + "local_bodies", +])); + +export async function GET(request: NextRequest) { + const yearFrom = toBoundedYear(request.nextUrl.searchParams.get("yearFrom"), 2015); + const yearTo = toBoundedYear(request.nextUrl.searchParams.get("yearTo"), new Date().getFullYear()); + const startPage = toBoundedNumber(request.nextUrl.searchParams.get("startPage"), 1, 1, 10000); + const maxPages = toBoundedNumber(request.nextUrl.searchParams.get("maxPages"), 5, 1, 25); + const topics = parseTopics(request.nextUrl.searchParams.get("topics")); + + const result = await fetchCagAuditRecords({ + yearFrom: Math.min(yearFrom, yearTo), + yearTo: Math.max(yearFrom, yearTo), + startPage, + maxPages, + topics, + }); + + return NextResponse.json(result); +} + +function parseTopics(value: string | null): CagAuditTopic[] { + if (!value || value === "all") return defaultCagTopics; + const topics = value + .split(",") + .map((item) => item.trim()) + .filter((item): item is CagAuditTopic => allowedTopics.has(item as CagAuditTopic)); + return topics.length ? topics : defaultCagTopics; +} + +function toBoundedYear(value: string | null, fallback: number): number { + return toBoundedNumber(value, fallback, 2015, new Date().getFullYear()); +} + +function toBoundedNumber( + value: string | null, + fallback: number, + min: number, + max: number, +): number { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return fallback; + return Math.min(Math.max(Math.trunc(parsed), min), max); +} diff --git a/web/lib/cag-audit-connector.ts b/web/lib/cag-audit-connector.ts new file mode 100644 index 00000000..d3b452b9 --- /dev/null +++ b/web/lib/cag-audit-connector.ts @@ -0,0 +1,344 @@ +export type CagAuditTopic = + | "digital_governance" + | "ai_data_systems" + | "disaster_ndma_flood" + | "security_terror_border" + | "infrastructure_collapse" + | "financial_audit" + | "health_welfare_education" + | "local_bodies"; + +export type CagAuditRecord = { + id: string; + reportDate: string | null; + reportYear: number | null; + government: string | null; + reportTypes: string[]; + title: string; + summary: string; + sectors: string[]; + detailUrl: string | null; + pdfUrl: string | null; + pdfSize: string | null; + topics: CagAuditTopic[]; + extractionStatus: "metadata_only" | "requires_pdf_extraction"; + source: { + authority: "Comptroller and Auditor General of India"; + indexUrl: string; + fetchedAt: string; + }; +}; + +export type CagAuditFetchResult = { + generatedAt: string; + sourceNotice: string; + query: { + yearFrom: number; + yearTo: number; + topics: CagAuditTopic[]; + startPage: number; + pagesFetched: number; + maxPages: number; + }; + source: { + authority: "Comptroller and Auditor General of India"; + indexUrl: string; + totalRecordsOnSite: number | null; + paginationLastPage: number | null; + }; + coverage: { + fetchedRecords: number; + matchedRecords: number; + pdfLinks: number; + findingExtraction: "requires_pdf_text_extraction_with_page_provenance"; + }; + records: CagAuditRecord[]; +}; + +const CAG_BASE_URL = "https://cag.gov.in"; +const CAG_AUDIT_INDEX = `${CAG_BASE_URL}/en/audit-report`; + +const TOPIC_KEYWORDS: Record = { + digital_governance: [ + "digital", + "information technology", + "it system", + "e-governance", + "computerisation", + "database", + "portal", + "online", + "communication", + ], + ai_data_systems: [ + "artificial intelligence", + "ai", + "algorithm", + "analytics", + "data system", + "data centre", + "data center", + ], + disaster_ndma_flood: [ + "disaster", + "ndma", + "flood", + "cyclone", + "relief", + "rehabilitation", + "calamity", + "sdrf", + "ndrf", + ], + security_terror_border: [ + "terror", + "security", + "defence", + "defense", + "border", + "police", + "home affairs", + "paramilitary", + ], + infrastructure_collapse: [ + "infrastructure", + "bridge", + "road", + "railway", + "building", + "collapse", + "works", + "construction", + "irrigation", + "power", + ], + financial_audit: [ + "finance", + "financial", + "state finances", + "appropriation", + "revenue", + "tax", + "duties", + "expenditure", + "accounts", + "budget", + ], + health_welfare_education: [ + "health", + "hospital", + "education", + "school", + "welfare", + "nutrition", + "social", + ], + local_bodies: [ + "local bodies", + "panchayat", + "municipal", + "urban local", + "rural local", + "gram", + ], +}; + +export const defaultCagTopics: CagAuditTopic[] = [ + "digital_governance", + "ai_data_systems", + "disaster_ndma_flood", + "security_terror_border", + "infrastructure_collapse", + "financial_audit", +]; + +export async function fetchCagAuditRecords({ + yearFrom = 2015, + yearTo = new Date().getFullYear(), + topics = defaultCagTopics, + startPage = 1, + maxPages = 5, +}: { + yearFrom?: number; + yearTo?: number; + topics?: CagAuditTopic[]; + startPage?: number; + maxPages?: number; +} = {}): Promise { + const boundedMaxPages = Math.min(Math.max(maxPages, 1), 25); + const boundedStartPage = Math.max(Math.trunc(startPage), 1); + const generatedAt = new Date().toISOString(); + const pages = Array.from({ length: boundedMaxPages }, (_, index) => boundedStartPage + index); + const pageHtml = await Promise.all(pages.map((page) => fetchCagPage(page))); + const allRecords = pageHtml.flatMap(({ html, url }) => + parseCagAuditRecords(html, url, generatedAt), + ); + const filtered = allRecords.filter((record) => { + const inYear = + record.reportYear === null || + (record.reportYear >= yearFrom && record.reportYear <= yearTo); + const inTopic = + topics.length === 0 || + record.topics.some((topic) => topics.includes(topic)); + return inYear && inTopic; + }); + const firstPage = pageHtml[0]?.html ?? ""; + + return { + generatedAt, + sourceNotice: + "Official CAG audit-report metadata feed. Findings/flags require PDF text extraction with page provenance before public claim.", + query: { + yearFrom, + yearTo, + topics, + startPage: boundedStartPage, + pagesFetched: pageHtml.length, + maxPages: boundedMaxPages, + }, + source: { + authority: "Comptroller and Auditor General of India", + indexUrl: CAG_AUDIT_INDEX, + totalRecordsOnSite: extractTotalRecords(firstPage), + paginationLastPage: extractLastPage(firstPage), + }, + coverage: { + fetchedRecords: allRecords.length, + matchedRecords: filtered.length, + pdfLinks: filtered.filter((record) => record.pdfUrl).length, + findingExtraction: "requires_pdf_text_extraction_with_page_provenance", + }, + records: filtered, + }; +} + +async function fetchCagPage(page: number): Promise<{ html: string; url: string }> { + const url = page === 1 ? CAG_AUDIT_INDEX : `${CAG_AUDIT_INDEX}?page=${page}`; + const response = await fetch(url, { + cache: "no-store", + headers: { + "user-agent": "DISHA-cag-audit-connector/1.0", + }, + }); + return { + html: await response.text(), + url, + }; +} + +function parseCagAuditRecords( + html: string, + indexUrl: string, + fetchedAt: string, +): CagAuditRecord[] { + const blocks = html + .split('
') + .slice(1) + .map((block) => block.split('
')[0]) + .map((block) => block.split('