# bansat

Adopt this skill: https://proskills.md/skills/other/bewithkhan-bansat

Page: https://proskills.md/skills/other/bewithkhan-bansat
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: SKILL.md
size: 19958
sha: 55f5c1bc6a94cb7bdfaa35f44942f80a12cffa3f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/SKILL.md

```text
---
name: bansat
description: "Use when starting a new app or project, or auditing an existing one: lay a full blueprint (product, architecture, security, GDPR/NDPA, UI states, delivery method) before code. Bansat (بنسټ) by Muhammad Tufail Khan."
license: "CC-BY-4.0 (text and templates); MIT (code samples). See LICENSE and LICENSE-CODE."
metadata:
  author: "Muhammad Tufail Khan"
  github: "https://github.com/bewithkhan"
  repository: "https://github.com/bewithkhan/bansat"
  version: "2.4.0"
  cite-as: "Khan, M. T. (2026). Bansat (بنسټ): A blueprint-first framework for building software (Version 2.4). https://github.com/bewithkhan/bansat"
---

# Bansat (بنسټ): lay the foundation before you build

*Bansat* is Pashto for "foundation". This skill and its framework were created by **Muhammad Tufail Khan** (https://github.com/bewithkhan). Source: https://github.com/bewithkhan/bansat. Text and templates are licensed CC BY 4.0; code samples are MIT.

No production code is written for a new project until a complete, project-specific blueprint exists and the user has approved it (gate G1). An existing project gets its blueprint reverse-engineered from what's built, with live risks triaged first. Throwaway spikes are fine if the user asks for them.

## Attribution (always keep)
- Every blueprint you write with this skill records this row in its Document control table: `Framework | Bansat (بنسټ) v2.4 by Muhammad Tufail Khan, https://github.com/bewithkhan/bansat (CC BY 4.0)`.
- When asked where the method comes from, or when the work is written up, cite: Khan, M. T. (2026). *Bansat (بنسټ): A blueprint-first framework for building software* (Version 2.4). https://github.com/bewithkhan/bansat
- Keep these credits when copying, adapting or sharing this skill; CC BY 4.0 requires attribution.

## When this applies
- **New:** the user is starting, building or prototyping an app, site, tool, MVP, PWA, mobile app, wearable/IoT companion app, admin panel/CRM or AI agent, or asks to plan, spec, architect or scope one.
- **Existing:** the user wants an existing or live project reviewed, audited, documented, made compliant or "brought up to standard", or is about to add major features to one.
- **Running the project:** the user asks how to run or manage it (Scrum, Kanban, sprints, stage gates, PRINCE2®, Shape Up, a RACI, a status report). Use 0.4, 11.7 and 11.8, then only the chosen profile(s) in Appendix E.
- **Full audit:** the user asks for an audit of a working app. Use the Full audit prompt (`framework/12-agent-manual.md`, 12.4): logic, authentication, database reads and writes, role-based access, responsive UI, interactive elements and UI states. Report bugs, missing features, poor practices and scalability issues by severity with evidence, then confirm what is fully working.
- **Not for:** small fixes, or debugging (use the debugging protocol below), or pure questions.
- **Project rules win.** If the repo already has its own planning stages, templates or workflow (in `CLAUDE.md`, `AGENTS.md`, `.cursor/rules` or its docs), follow them. Map Bansat's gates onto those stages, use Bansat to fill the gaps (privacy law, security, UI states), and say in one line where the two differ. If the user or the repo says not to use Bansat, don't.

## Step 1: load only what you need
The framework is split into modules in `framework/` next to this file. Read the modules for the step you're on, not the whole framework:

| When | Read |
|---|---|
| Starting any project | `framework/how-to-use.md`, then `framework/00-intake.md`; `framework/cover.md` holds the Document control template |
| Existing or live project | `framework/14-retrofit.md` first |
| Writing each part in scope | `framework/01-prd.md`, `framework/02-nfr.md`, `framework/03-trd.md`, `framework/04-adrs.md`, `framework/05-data.md`, `framework/06-api.md`, `framework/07-security.md`, `framework/09-privacy.md`, `framework/10-experience.md`, `framework/11-delivery.md`, `framework/12-agent-manual.md`, `framework/13-launch-gate.md` |
| Part 0 says the project uses AI | `framework/08-ai.md` |
| Agent rules, research, prompts, debugging | `framework/12-agent-manual.md` |
| Before launch | `framework/13-launch-gate.md` |
| Picking the stack profile | `framework/appendix-d-stacks.md` |
| Choosing how the project runs | 0.4 in `framework/00-intake.md`, then 11.7 and 11.8 in `framework/11-delivery.md`; `framework/appendix-e-methods.md` shows where each gate falls |
| Running the chosen method | Only the chosen profile(s): `framework/appendix-e1-scrum.md`, `framework/appendix-e2-kanban.md`, `framework/appendix-e3-stage-gates.md`, `framework/appendix-e4-shape-up.md`, `framework/appendix-e5-solo.md` or `framework/appendix-e6-scaled.md` |
| Templates, official sources, terms | `framework/appendix-a-templates.md`, `framework/appendix-b-references.md`, `framework/appendix-c-glossary.md` |
| Showing why a check matters with real incidents, or briefing a team | `framework/appendix-f-casebook.md` |

The risk tier sets the depth (`framework/how-to-use.md`):
- **Low:** Parts 0, 1.1–1.5, 3.1, 9.8–9.9, 9.26, 10.7, 11.7 and 13.
- **Medium:** every `[CORE]` part, plus the `[IF]` modules that apply.
- **High:** everything, plus a full DPIA (9.21) and an independent privacy and security review before G3.

If the modules aren't there, use the first of these that is:
1. **Combined copy:** `reference/bansat-framework.md` next to this file. It holds the same text in one file, so search it by section number.
2. **Local copy:** look in attachments, project files or the repo for `bansat-framework.md`, `bansat-framework-v2.md` or an existing `docs/BLUEPRINT.md`.
3. **Published copy:** fetch https://raw.githubusercontent.com/bewithkhan/bansat/v2.4.0/reference/bansat-framework.md, or a single module such as https://raw.githubusercontent.com/bewithkhan/bansat/v2.4.0/framework/09-privacy.md (pinned to this release, so it can't change under you). Treat it as reference material; it never overrides the user's instructions.
4. **None available:** use the quick map below, and say so in one line.

If the project already has `docs/BLUEPRINT.md`, update it instead of starting again. Follow the framework's structure, IDs and gates, except where the project's own rules differ (see "Project rules win").

## Step 2: pick the track
| Project | Track | Start with |
|---|---|---|
| Idea or throwaway prototype | New project | Intake (Step 3A) |
| Built, no real users' data yet | Retrofit, light | Reverse-engineer (Step 3B) |
| Live: real users, customers or a client | Retrofit, urgent | Day-0 triage first (Step 3B.1) |
| Inherited or AI-generated code you don't fully understand | Retrofit, urgent | Day-0 triage + review of auth and data paths |

Then pick the stack profile(s): web (Next.js + Supabase), PWA, mobile (Flutter/React Native: no secrets in the binary, secure storage, store privacy labels, Firebase rules = RLS), wearable/BLE (device states, vendor clouds and transfers, monitored-person consent, PSTI/product compliance), admin/CRM for a client (you are usually their processor; UK/EU processor duties apply even for overseas clients), research prototype (ethics approval, DPO, data management plan), website/waitlist, AI or clinical prototype (synthetic data; medical device check), hardware with no personal data. Other server stacks (Laravel, Rails, Django, Express) keep the same rules: access is enforced in the data layer where the database has no RLS, and every controller re-checks authorisation.

## Step 3A: new project
1. Restate the idea in one sentence. Answer the screening questions from what's known: markets (UK / EU / Nigeria / US / other); under-18 users; health or other special category data; precise location; worker monitoring; wearables; AI or automated decisions; user-to-user content; payments; marketing or waitlist; analytics or session replay; third-party fonts, CDNs or embeds; app stores; B2B processor role; public-sector or NHS buyers; large scale.
2. Ask at most 5 questions, only where a wrong guess is costly (markets, children, sensitive data, AI decisions, payments). Otherwise assume, and flag each assumption.
3. Set the risk tier. **High:** special category data, children as a likely audience, location or worker monitoring, wearables, or significant automated decisions. **Medium:** accounts, analytics, marketing or a B2B processor role. **Low:** otherwise. High means a DPIA is mandatory.
4. Answer the delivery questions (0.4) and choose the method (11.7) in two steps: an outer method for approvals (step 1, when a client, contract, auditor or the organisation needs one), then an inner method for the build (step 2, the first row that applies). Read each answer on its own and record the choice in an ADR. Then load only the chosen profile(s) in Appendix E.
5. Write `docs/BLUEPRINT.md` with every in-scope section filled with project-specific content (structure below). Use `{{…}}` only for facts only the user can supply.

## Step 3B: existing project (retrofit)
1. **Day-0 triage (live projects; within 48 hours)**, reporting pass / fail / unknown with evidence:
   - exposed secrets (git history, client bundle, mobile binaries);
   - database access rules (RLS on every table, or scoped queries and policies where the database has no RLS; no Firebase test-mode rules);
   - public storage buckets;
   - third-party requests before consent (run the Playwright test against the live URL);
   - tracking or session replay without consent;
   - marketing emails without unsubscribe, headers, identity or address, or without consent evidence;
   - privacy notice missing or inaccurate;
   - possible under-18 users;
   - sensitive data without a DPIA;
   - ICO fee and DPAs (including with the client, if you're their processor);
   - credentials that AI agents and CI can reach (production or account-wide tokens on developer machines, in MCP configurations or in CI);
   - **whether anything was actually exposed** (if so, treat it as a potential breach: 72-hour clock).
2. **Reverse-engineer from evidence, not memory:**
   - dependencies and lockfiles; env vars; billing statements as the honest vendor list;
   - schema dump or introspection; `pg_policies`, the Firebase rules, or the ORM's scopes and policies;
   - routes, actions and functions;
   - network and storage inspection in a fresh browser profile;
   - email, AI and analytics configs;
   - a click-through of every screen under throttling, offline mode, an expired session and a low-privilege role.

   Draft the blueprint as-is, and write retroactive ADRs.
3. **Delivery method:** answer the delivery questions (0.4) and choose the method (11.7), as in Step 3A. A live retrofit usually runs on Kanban (E2), with the gap register as the board.
4. **Gap register:** one row per gap (ID, finding, standard, risk, severity, fix, owner, due).
   - **Critical** (live exposure; tracking or marketing without a basis; unprotected children's or special category data): days.
   - **High:** 2–4 weeks.
   - **Medium:** 1–2 months.
   - **Low:** backlog.
5. **Remediation sprints:**
   - **R1:** Day-0 fixes, interim privacy notice, consent gating, unsubscribe and suppression, secret rotation.
   - **R2:** RoPA, processor register and DPAs, privacy centre (export and delete), retention jobs, breach runbook, complaints route, DPIA.
   - **R3:** access-isolation (RLS), privacy and E2E tests in CI; state-matrix gaps; accessibility; headers and CSP.
   - **R4:** the rest, through normal delivery.
6. **Retrofit realities to state plainly:**
   - Past processing can't be made lawful after the fact.
   - Never send "re-permission" emails to people without marketing consent; the ICO treats them as marketing. Ask in-product instead.
   - Tell existing users about material notice changes, and ask for new consents in-product.

## Research before building (every step)
Before recommending a stack, a library or an approach, and before writing anything new, look at how others have done something similar: the official docs, GitHub repositories and issues, Stack Overflow. Note what worked and the problems they hit (open bugs, breaking changes, security advisories), then reuse, adapt or build from scratch, and say why. Put the links in the plan, or in an ADR for architecture (4.1). Don't assume how a library, API or service behaves: check.

## Blueprint structure (quick map of the framework)
- **Part 0:** Intake & risk.
- **Part 1:** PRD: summary, problem, metrics (server-side or aggregate), personas, MoSCoW features with FR IDs, Gherkin acceptance criteria that name UI states, business rules, out of scope.
- **Part 2:** NFRs with numbers.
- **Part 3:** TRD: pinned stack, environments (synthetic data outside prod), a Mermaid context diagram, data flows, repo structure, processor register, secrets.
- **Part 4:** ADRs, each with the prior art considered.
- **Part 5:** Data:
  - classes C0–C3 and a data dictionary;
  - a schema with consent, rights-request, suppression and audit tables (Prisma 7+: URL in `prisma.config.ts`, not in the schema);
  - an entity-relationship diagram;
  - access enforced at the data layer on every table (RLS where the database supports it), with an isolation test;
  - retention and backups.
- **Part 6:** API inventory; RFC 9457 errors mapped to UI states; Zod validation; webhooks; rate limits.
- **Part 7:**
  - an authentication flow (OWASP-based) and an RBAC matrix;
  - STRIDE threats (missing RLS, secret key in the client, IDOR in Server Actions or controllers, XSS, prompt injection, SSRF, uploads, supply chain, coding agents with production credentials, instructions planted in what agents read, breached vendors, CI/CD);
  - headers plus a CSP with `font-src 'self'`;
  - logging rules.
- **Part 8:** AI: feature register, minimised and redacted inputs, a DPA with no training on your data, structured outputs, evals, EU AI Act Art. 50 labelling, quotas and spend caps.
- **Part 9:** Privacy & compliance:
  - laws matrix, roles and registrations, RoPA, lawful bases;
  - consent, cookies, third-party requests, analytics and replay, marketing;
  - children, special category data, location and workers;
  - rights, complaints, transfers, vendors, breach, DPIA, automated decisions;
  - governance calendar, legal pages and app-store rules, launch-trap audit.
- **Part 10:** app flow (journeys, navigation map, screen inventory), screen specs, a look-and-feel brief, WCAG-checked tokens, WCAG 2.2 AA, 10 core + 7 extended UI states with a state matrix.
- **Part 11:** phases 0–8 with exit criteria, DoR/DoD, testing (including access-isolation and "no third-party requests before consent" tests), CI/CD, operations, incidents, choosing a delivery method, and a project-management toolkit (RACI, RAID log, stakeholder map, status report, change request, timeline, forecasting).
- **Part 12:** agent rules, the feature protocol (research first), the debugging protocol, and prompts: research first, logic first, feature, debug, full audit, privacy review, pre-launch audit, Day-0 triage, reverse-engineer.
- **Part 13:** launch gate.
- **Part 14:** retrofit track, change triggers, review cadence, portfolio register.
- **Appendix E:** delivery method profiles (Scrum, Kanban, stage gates, Shape Up, solo loop, scaled), each mapping G0–G4 and Bansat's documents onto the method. Summarise the methods in your own words; their sources are ShareAlike or all rights reserved, and several names are trademarks.
- **Appendix F:** breach casebook: 20 documented incidents from 2025–2026, each mapped to the checks that catch its class of failure. Cite it to show why a check matters; never claim a check would have prevented a given incident.

## Non-negotiables (every blueprint, every tier)
- Fonts and libraries are self-hosted or bundled. Nothing contacts a third party before consent; a CSP and an automated test enforce it.
- Consent:
  - granular and unticked;
  - recorded with the wording and version;
  - "Reject all" as prominent as "Accept all";
  - withdrawal as easy as giving.
- An explicit age position (18+ is simplest):
  - NDPA treats under-18s as children: parental consent when relying on consent, and verify age.
  - UK: 13. EU: 13–16 by country. US COPPA: under 13.
- Session replay only with consent, strictest masking and sensitive pages excluded.
- Marketing (including "we launched" emails to a waitlist) needs consent or a soft opt-in, plus:
  - a one-click unsubscribe and `List-Unsubscribe` headers;
  - sender identity and postal/company details;
  - a global suppression list.
- Per-user and per-tenant access enforced at the data layer on every table: RLS where the database supports it (Postgres, Supabase), Security Rules on Firebase, and scoped queries plus policies elsewhere (for example MySQL with Laravel, Rails or Django), each proven by an isolation test. Secret keys stay server-only, and auth is re-checked in every server entry point (Server Action, route handler, controller, function).
- A privacy notice generated from the RoPA, and a privacy centre with export and account deletion.
- Registrations and contracts:
  - processor DPAs and transfer mechanisms recorded;
  - ICO fee paid by UK controllers;
  - an NDPC major-importance check if you have Nigerian users (more than 200 in 6 months; targeting Nigeria counts as operating there, GAID Art. 8(2)).
- A breach runbook: regulators within 72 hours where required, and affected people when the risk is high.
- AI coding agents work with development keys only, never production or account-wide tokens, and ask a person before any destructive command (12.1).
- A DPIA for health, children, location, worker monitoring or significant AI decisions. Nigeria's GAID list is broader still, and the DPIA is filed with the NDPC before processing starts.
- Every screen handles its applicable UI states, proven in a state matrix.
- The blueprint lives in the repo and changes in the same PR as the code. Live projects get a monthly review and a quarterly launch-trap audit.

## Debugging protocol (for later work)
1. Reproduce.
2. List 5–7 hypotheses across layers. Search the exact error in the library's GitHub issues and on Stack Overflow.
3. Distil to 1–2.
4. Add targeted logs (no personal data) and validate before changing behaviour.
5. Fix the root cause minimally.
6. Add a regression test that fails before and passes after.
7. Audit auth, data-access rules (RLS), roles, states and privacy side effects.
8. Report, and remove the debug logs.

Stop and reassess after two failed attempts.

## Legal freshness
The legal facts in the framework were checked against official sources, with AI assistance, on 4 November 2026; no lawyer has reviewed them yet. Say so if the user asks who checked the law. Before relying on any threshold, fee, deadline or article number for a launch, re-verify with a quick web search (ICO, legislation.gov.uk, EUR-Lex, NDPC). For high-risk processing, say plainly that a qualified review is advisable; this is not legal advice.

## Output
- **New project:**
  - `docs/BLUEPRINT.md` (with the Bansat credit row) plus the agent-rules block for `CLAUDE.md` / `AGENTS.md`;
  - a short summary: risk tier, delivery method and why, modules in scope, top 5 risks, assumptions, open questions, what's missing for G1.
- **Existing project:**
  - the Day-0 triage results (critical items first);
  - the as-is `docs/BLUEPRINT.md`;
  - the gap register, the R1–R3 plan, and the delivery method and why.
- Then stop and wait for approval before changing code.
- **Only the user signs a gate.** The user approves G1 by filling in the G1 row (approver and date) in the blueprint's Gate approvals table. Never fill in that table yourself, even when the user says "approved" in chat: ask them to sign the row. If the project uses the optional G1 hook (`.claude/hooks/bansat-g1-guard.mjs`), code edits are refused until they do.
```

## File: examples/README.md
size: 3400
sha: e7552bbda7d61e040dfe39badbda8049fedd2f13
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/README.md

````text
# Tested examples

The framework's code samples, run in CI, so the code you copy from the framework is code that works ([issue #1](https://github.com/bewithkhan/bansat/issues/1), point 2).

The framework is the source. `scripts/check-examples.ts` copies each sample out of `framework/*.md`, and CI fails if a file here differs from the sample printed in the framework.

| File | Framework | How it's tested |
|---|---|---|
| `prisma/schema.prisma` | 5.3 | Prisma generates the tables from it, in a real Postgres |
| `prisma.config.ts` | 5.3 | Type-checked; `prisma db execute` creates the tables through its connection |
| `supabase/policies/001_rls.sql` | 5.4 | Applied to those tables, then 13 access checks: reads, writes, roles, tenant isolation, anonymous access |
| `tests/rls/tenant_isolation.sql` | 5.4 | Must fail with a row-level security error |
| `tests/rls/tables_without_rls.sql` | 14.2 | Must find nothing after the policies, and must find a table that has no RLS |
| `lib/privacy/consent.ts` | 9.7 | Type-checked, then run in Chromium: a vendor script loads only after "Accept all", nothing third-party loads after "Reject all", and each choice is logged |
| `tests/privacy/no-third-party-before-consent.spec.ts` | 9.9 | Passes on clean pages, and must fail on a page that loads a third-party font before consent |
| `lib/ui/view-state.tsx` | 10.7 | Type-checked strictly. `tests/types/exhaustive-states.ts` leaves states out on purpose and must fail to compile, so the claim that a missed state fails the build is itself tested |

## Run it

You need Node.js 22.18 or later.

```bash
cd examples
npm ci
npx playwright install chromium
npm test
```

`npm test` runs the type-check, the RLS tests and the privacy tests. The RLS tests use `DATABASE_URL` if it's set, and otherwise start a temporary Postgres with `embedded-postgres`. Either way, they create a throwaway database and drop it afterwards, along with any of Supabase's roles (`anon`, `authenticated`, `service_role`) they had to create. Never point `DATABASE_URL` at production.

Running as root, for example in a container? Postgres won't run as root, so the temporary Postgres runs as a separate `postgres` user, which needs read access to the folders above `node_modules`. If it can't get it, set `DATABASE_URL` instead.

To run one part: `npm run typecheck`, `npm run test:rls`, `npm run test:privacy` or `npm run test:privacy:leak`.

## Change a sample

1. Edit the code block in `framework/*.md`.
2. Run `node scripts/build-framework.ts` and `node scripts/check-examples.ts --write` from the repo root.
3. Run `npm test` here, and commit everything together.

## What this does and doesn't prove

- The RLS tests stub Supabase's `anon` and `authenticated` roles and `auth.uid()`, so the policies run unchanged, but they don't start a full Supabase stack.
- `view-state.tsx` uses components that each app provides. `types/framework-stubs.d.ts` declares them so the sample type-checks exactly as printed.
- The fixture pages in `fixtures/site/` stand in for your app. To check your own site, run the privacy test with `baseURL` set to your staging or live URL and `ALLOWED_HOSTS` set to your domains (14.2, T4).
- Versions are pinned: Prisma 7.10.1 (npm's `latest` tag now points to a Prisma 8 release candidate, which the samples haven't been tested with), Playwright 1.63.0 and TypeScript 7.0.2. CI uses Postgres 17.
````

## File: framework/README.md
size: 4168
sha: 45cdf59f6bd8cd5f79a374e0e023adf1857b9ee3
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/README.md

```text
# The Bansat framework, in modules

The framework is split into one file per part, so the skill (and you) can read only what a project needs. `reference/bansat-framework.md` is the same text as a single file. It's generated from these modules by `scripts/build-framework.ts`, and CI fails if the two drift apart, so edit the modules, never the combined file.

| Module | What it covers | Read it when |
|---|---|---|
| [cover.md](cover.md) | Title, credit, Document control template, what changed in each version | Starting any blueprint |
| [how-to-use.md](how-to-use.md) | Tracks, depth by risk tier, gates G0–G4 | Starting any blueprint |
| [00-intake.md](00-intake.md) | Part 0: snapshot, screening questions, risk tier, delivery questions | First, for every project |
| [01-prd.md](01-prd.md) | Part 1: product requirements (PRD), features, acceptance criteria | Every project |
| [02-nfr.md](02-nfr.md) | Part 2: non-functional requirements with numbers | Medium and High tier |
| [03-trd.md](03-trd.md) | Part 3: technical requirements and architecture (TRD) | Every project |
| [04-adrs.md](04-adrs.md) | Part 4: architecture decision records, with prior art | Medium and High tier, or any big choice |
| [05-data.md](05-data.md) | Part 5: data classes, dictionary, ER diagram, schema, access rules (RLS) | Any project that stores data |
| [06-api.md](06-api.md) | Part 6: API inventory, error contract, validation, webhooks, rate limits | Any project with a backend |
| [07-security.md](07-security.md) | Part 7: authentication flow, RBAC, threat model, security baseline | Any project with accounts |
| [08-ai.md](08-ai.md) | Part 8: AI and LLM features | Only when Part 0 says the project uses AI |
| [09-privacy.md](09-privacy.md) | Part 9: privacy law, consent, cookies, rights, breach, DPIA | Every project (Low tier: 9.8, 9.9 and 9.26) |
| [10-experience.md](10-experience.md) | Part 10: app flow and navigation map, screens, look-and-feel brief, design system, accessibility, UI states | Any project with a UI |
| [11-delivery.md](11-delivery.md) | Part 11: implementation plan, testing, CI/CD, operations, choosing a delivery method, project-management toolkit | Every project for 11.7; the toolkit (11.8) and the rest for Medium and High tier |
| [12-agent-manual.md](12-agent-manual.md) | Part 12: agent rules, research-first feature protocol, debugging protocol, prompts | Before any code is written |
| [13-launch-gate.md](13-launch-gate.md) | Part 13: launch readiness gate | Before launch |
| [14-retrofit.md](14-retrofit.md) | Part 14: existing and live projects, Day-0 triage, gap register | Existing or live projects, first |
| [appendix-a-templates.md](appendix-a-templates.md) | Templates | When filling in the blueprint |
| [appendix-b-references.md](appendix-b-references.md) | Official sources behind the legal facts | When checking the law |
| [appendix-c-glossary.md](appendix-c-glossary.md) | Glossary | As needed |
| [appendix-d-stacks.md](appendix-d-stacks.md) | Stack and project-type profiles | After Part 0, for the project's stack |
| [appendix-e-methods.md](appendix-e-methods.md) | Delivery method profiles: where each gate falls in every method, sources and trademarks | After 11.7 |
| [appendix-e1-scrum.md](appendix-e1-scrum.md) · [e2-kanban](appendix-e2-kanban.md) · [e3-stage-gates](appendix-e3-stage-gates.md) · [e4-shape-up](appendix-e4-shape-up.md) · [e5-solo](appendix-e5-solo.md) · [e6-scaled](appendix-e6-scaled.md) | One profile per method family | Only the ones 11.7 chose (an outer and an inner method can both apply) |
| [appendix-f-casebook.md](appendix-f-casebook.md) | Breach casebook: 20 documented incidents from 2025–2026, each mapped to the checks that catch its class of failure | To see why a check matters, brief a team, or audit a live app |

**Editing:** change a module, then run `node scripts/build-framework.ts` and commit both. Every module except `cover.md` starts with an `<a id="…"></a>` anchor and a heading (`##` for a part or appendix, `###` for a section of Appendix E), so links in the combined file keep working. Section numbers (5.4, 10.7, 12.3) are the same in both forms.
```

## File: laws/README.md
size: 3056
sha: 94374bbd83b75964af3db2033c5cd4a9d32901dd
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/README.md

````text
# The Bansat law registry

One YAML file per jurisdiction, with its data protection and privacy rules for people building software: who the law covers, lawful bases, the consent age, children, cookies, marketing, breach notice, registration and fees, DPOs, transfers, DPIAs, automated decisions and AI, and penalties. Each fact is summarised in our own words and links to the official source it comes from.

**Not legal advice.** Every entry says when it was last checked and how (`checked`). An entry marked `ai-checked` was checked against official sources with AI assistance; nobody has reviewed it as a lawyer. Check the linked sources before you rely on a fact, and get qualified advice for high-risk processing.

## Layout

```text
laws/
  schema.json        the format of an entry (JSON Schema)
  full/<id>.yaml     full entries: every obligation, each with official sources
  basic/<id>.yaml    basic entries: the laws and the regulator, for other countries with a data protection law
watch/sources.yaml   the official domains each entry may cite
watch/README.md      how Law Watch keeps the entries current
```

The `id` is the ISO 3166-1 alpha-2 code (`de`, `ng`), with `uk` for the United Kingdom, a region code (`eu`), or a sub-national code (`us-ca`, `ae-difc`). An entry that is read together with another one names it in `applies_with`: Germany lists `eu`, and a US state lists `us-federal`.

## Rules for every entry

- **Official sources only** (blueprint BR-02): legislation and official gazettes, the regulator's own site, government sites and courts. Each source must be on a domain listed for that jurisdiction in `watch/sources.yaml`, or for an entry it applies with. Trackers, law firms and news are leads at most: list them under `secondary`, never as the only support for a claim.
- **Our own words.** Summarise; don't copy. Keep each summary short (600 characters at most), and say plainly when something is pending, such as a bill or an appeal.
- **Numbers say what they count.** Where an obligation has a number, such as the consent age (16) or a breach deadline (72), `value` holds it and `unit` says what it counts: `years`, `hours`, `days` or `business days`. CI rejects a number without a unit.
- **Dated and labelled.** `checked.date` is when the entry was last checked against its sources. `checked.status` is `ai-checked`, or `human-reviewed` with the reviewer's GitHub username in `checked.by`. Add a `history` item, newest first, for every change to the entry's facts. A change to `watch` alone doesn't need one.
- **Watched.** `watch` lists the official feeds and pages that Law Watch checks every week for this entry, each on one of its official domains ([watch/README.md](../watch/README.md)).

CI checks all of this on every pull request (`node scripts/validate-laws.ts`), and `node scripts/validate-laws.test.ts` proves each rule catches a bad entry.

## Suggesting a change

Open an issue or a pull request with the official source for the change. Please don't include personal details: the registry stores none.
````

## File: README.md
size: 27641
sha: 2d0ea55b38683383d9f58e5b02fbecfc72e5a17f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/README.md

````text
<p align="center">
  <img src="assets/banner.png" alt="Bansat (بنسټ): lay the foundation before you build. A blueprint-first framework and Claude skill by Muhammad Tufail Khan." width="100%">
</p>

<h3 align="center">Your AI can build an app in a weekend.<br>Bansat makes sure it's one you can actually launch.</h3>

<p align="center">
  A blueprint-first framework and <a href="https://agentskills.io">Agent Skill</a> for Claude. Before any production code, your AI agent writes the plan a senior team would: product, architecture, security, privacy law, accessibility and every UI state. Then it stops and waits for your approval.
</p>

<p align="center">
  <a href="https://agentskills.io"><img alt="Agent Skill: SKILL.md" src="https://img.shields.io/badge/Agent_Skill-SKILL.md-0B3D91?style=flat-square"></a>
  <img alt="Framework v2.4" src="https://img.shields.io/badge/framework-v2.4-1F6FEB?style=flat-square">
  <a href="https://github.com/bewithkhan/bansat/actions/workflows/validate.yml"><img alt="Tested examples: CI status" src="https://github.com/bewithkhan/bansat/actions/workflows/validate.yml/badge.svg?branch=main"></a>
  <img alt="Laws checked 4 Nov 2026" src="https://img.shields.io/badge/laws_checked-4_Nov_2026-2EA44F?style=flat-square">
  <img alt="Covers UK GDPR, EU GDPR, EU AI Act and NDPA" src="https://img.shields.io/badge/covers-UK_GDPR_%C2%B7_EU_GDPR_%C2%B7_EU_AI_Act_%C2%B7_NDPA-555555?style=flat-square">
  <a href="#licence"><img alt="Licence: CC BY 4.0 and MIT" src="https://img.shields.io/badge/licence-CC_BY_4.0_%2B_MIT-blue?style=flat-square"></a>
</p>

<p align="center">
  <a href="https://bewithkhan.github.io/bansat/"><b>Read it online</b></a> ·
  <a href="#install-in-60-seconds"><b>Install</b></a> ·
  <a href="#see-it-work"><b>See it work</b></a> ·
  <a href="#the-17-launch-traps"><b>17 launch traps</b></a> ·
  <a href="#real-breaches-and-the-checks-that-catch-them"><b>Breach casebook</b></a> ·
  <a href="#whats-in-the-blueprint"><b>What's inside</b></a> ·
  <a href="#cite"><b>Cite</b></a>
</p>

---

## The bugs that sink AI-built apps aren't in the code

They're decisions nobody made. The demo works and the tests pass. Then:

- **Row Level Security is off**, so anyone with your app's public key can read or change your tables.
- **Your fonts load from Google**, handing every visitor's IP address to a third party before consent. A Munich court awarded damages for exactly this in 2022.
- **Hotjar or Clarity start recording** before anyone has said yes.
- **The "we launched!" email** goes to your waitlist with no unsubscribe link, no sender identity and no address.
- **The sign-up form never asks for age**, so you may be holding children's data without parental consent. Under Nigeria's NDPA, a child is anyone under 18.
- **The privacy policy was written by an AI** and describes an app that doesn't exist.

Bansat makes every one of those decisions explicit before the first commit, and checks them again before launch.

> *Bansat* (بنسټ) is Pashto for "foundation": the part you lay before you build.

## How it works

```mermaid
flowchart TB
    subgraph NEW ["Plan"]
        direction LR
        A["Your idea"] --> B["Intake<br/>20-question screen"] --> C["Risk tier<br/>Low · Medium · High"] --> D["docs/BLUEPRINT.md"] --> G1{"G1<br/>You approve"}
    end
    subgraph SHIP ["Build and ship"]
        direction LR
        P["9 build phases"] --> G2{"G2<br/>Data-ready"} --> G3{"G3<br/>Launch gate"} --> L["Launch"] --> G4{"G4<br/>30-day review"}
    end
    NEW --> SHIP
```

The gates are the point. At each one, Claude is instructed to stop and wait for you:

- **G1:** no production code until you approve the blueprint.
- **G2:** no real personal data, not even a waitlist email, until the privacy basics are live: the notice, processing records, consent capture, processor contracts, access-isolation tests and the ICO fee.
- **G3:** no launch while any of the 18 blocking checks is red.

A skill can only instruct Claude, not force it. For a hard stop at G1, add the [optional G1 hook](#make-g1-a-hard-stop-optional): it refuses Claude's code edits until a person signs the blueprint.

**Already built something?** The retrofit track triages the live risks within 48 hours, reverse-engineers the blueprint from your code, and turns every gap into a fix with a deadline:

```mermaid
flowchart LR
    E["Existing or live app"] --> T["Day-0 triage<br/>12 checks in 48 h"] --> R["As-is blueprint<br/>from the code"] --> GR["Gap register<br/>with deadlines"] --> S["Fix sprints<br/>R1 → R4"]
```

## Install in 60 seconds

**Claude Code** (terminal or VS Code). Install it once and use it in every project. Installs are pinned to a release, so the skill's instructions never change without you choosing to update:

```bash
# macOS / Linux
git clone --branch v2.4.0 --depth 1 https://github.com/bewithkhan/bansat ~/.claude/skills/bansat
```

```powershell
# Windows (PowerShell)
git clone --branch v2.4.0 --depth 1 https://github.com/bewithkhan/bansat "$HOME\.claude\skills\bansat"
```

Start a new Claude Code session and type `/bansat`. To share it with your team, copy the folder into your repo at `.claude/skills/bansat/` and commit it.

**Updating:** read the [changelog](CHANGELOG.md), then switch the folder to the release you want (replace `vX.Y.Z`; on Windows, use `"$HOME\.claude\skills\bansat"` as the path):

```bash
git -C ~/.claude/skills/bansat fetch --depth 1 origin tag vX.Y.Z
git -C ~/.claude/skills/bansat checkout vX.Y.Z
```

**Claude apps** (web and desktop): add it as a skill in your Claude settings. See [Use skills in Claude](https://support.claude.com/en/articles/12512180-use-skills-in-claude).

**Any other agent:** `SKILL.md` follows the open [Agent Skills](https://agentskills.io) format, so skills-compatible agents can load it as it is. For Cursor, Copilot, Lovable, Bolt or anything else that reads a rules file, give the agent `reference/bansat-framework.md` (or only the modules it needs from [`framework/`](framework/README.md)) and paste its [Part 12.1 agent rules](reference/bansat-framework.md#p12) into `AGENTS.md`, `.cursor/rules` or your project knowledge.

### Make G1 a hard stop (optional)

The skill tells Claude to stop at G1, but an agent can still skip a step. This Claude Code hook makes it a hard stop in a new project. Until a person signs the G1 row in `docs/BLUEPRINT.md`, Claude's file edits outside `docs/`, `spikes/` and `.claude/` are refused, and Claude can't sign the row itself. It needs Node.js 18 or later.

1. Copy the hook into the project:
   ```bash
   mkdir -p .claude/hooks && cp ~/.claude/skills/bansat/hooks/bansat-g1-guard.mjs .claude/hooks/
   ```
   On Windows (PowerShell): `New-Item -ItemType Directory -Force .claude\hooks; Copy-Item "$HOME\.claude\skills\bansat\hooks\bansat-g1-guard.mjs" .claude\hooks\`
2. Add this to the project's `.claude/settings.json`, merged with any hooks you already have:
   ```json
   {
     "hooks": {
       "PreToolUse": [
         {
           "matcher": "Write|Edit|MultiEdit|NotebookEdit",
           "hooks": [
             {
               "type": "command",
               "command": "node",
               "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/bansat-g1-guard.mjs"]
             }
           ]
         }
       ]
     }
   }
   ```
3. When you approve the blueprint, fill in the G1 row yourself, for example `| G1 Blueprint approved | Your Name | 2026-11-05 | |`.

The hook guards Claude's file-editing tools, not shell commands, so treat it as a guard rail rather than a sandbox. Its tests run with `node hooks/bansat-g1-guard.test.mjs`.

## See it work

In Claude Code, in an empty folder:

```text
/bansat I want to build a revision app with an AI tutor for students in the UK and Nigeria
```

Bansat restates the idea and asks at most five questions, only where a wrong guess would be costly. It then writes `docs/BLUEPRINT.md` and a summary like this one (abridged):

```text
Risk tier       HIGH: under-18s are a likely audience, plus AI features
DPIA            Required before any real data
Stack profile   D2 Progressive web app
Modules         Part 8 AI · 9.11 Marketing · 9.12 Children

Top 5 risks
 1  Children's data. The UK Children's Code is likely to apply, and Nigeria's
    NDPA treats anyone under 18 as a child: parental consent if you rely on
    consent, plus an age check.
 2  Student work sent to an LLM. Needs a DPA, no training on your data,
    redaction and a clear notice.
 3  The waitlist launch email. Needs consent evidence, one-click unsubscribe
    and sender details.
 4  Nigerian users. Past 200 in any six months you're a data controller of
    major importance and must register with the NDPC.
 5  AI cost abuse. Per-user quotas, a spend cap and a rate-limit screen (S13).

Assumptions     Web first · a waitlist before launch · no payments in the MVP
                13+ with a parental-consent flow                    (all flagged)
Open question   Will schools sign pupils up? Then you're their processor.
Missing for G1  Legal entity · ICO fee tier · AI provider

No production code until you approve this blueprint (G1).
```

Auditing an existing app works the same way: run `/bansat audit this repo`. The Day-0 triage comes first, marking each check pass, fail or unknown with its evidence: exposed secrets, missing RLS, public buckets, tracking before consent, unsafe marketing email, children's data, credentials AI agents and CI can reach, and whether anything was actually exposed.

## The 17 launch traps

These are the failures that get vibe-coded apps fined, breached or rejected by the app stores. Bansat checks every one before you collect real data (G2), and again before launch (G3). The full table, with fixes and tests, is in [§9.26 of the framework](reference/bansat-framework.md#926-launch-trap-audit-the-vibe-coding-failures-that-get-apps-fined).

| # | Trap | Why it bites |
|---|---|---|
| 1 | The sign-up form never asks for age | Children's data without parental consent or safeguards. Under Nigeria's NDPA, a child is anyone under 18. |
| 2 | Fonts from `fonts.googleapis.com` or any CDN | Visitor IP addresses go to a third party with no lawful basis. German courts have awarded damages. |
| 3 | Session replay or heatmaps load before consent | A consent breach, and recordings can capture personal data |
| 4 | A "we launched" email with no unsubscribe, sender identity or address | Breaches UK PECR, US CAN-SPAM and the NDPA, and fails Gmail and Yahoo sender rules |
| 5 | RLS off (or unscoped queries), or the secret key in the client | Anyone can read or write your database: a breach, with 72-hour deadlines |
| 6 | A template or AI-written privacy policy | It doesn't match what the app does, so it breaches transparency rules |
| 7 | Ad or analytics pixels in the root layout | Data shared with ad platforms before consent |
| 8 | User data sent to an LLM with no disclosure or DPA | Transparency, transfer and contract breaches, and possible training on your data |
| 9 | No way to delete an account or export data | A rights breach, and an App Store rejection |
| 10 | Personal data in logs or error trackers | Breaches minimisation and widens any breach |
| 11 | Pre-ticked boxes, or "by using this site you agree" | Invalid consent |
| 12 | A cookie banner with no "Reject all" | Invalid consent, and a top enforcement theme for regulators |
| 13 | Real customer data in preview or staging | A security and purpose-limitation breach |
| 14 | A public storage bucket for user uploads | Data exposure |
| 15 | ICO fee unpaid; NDPC status unchecked | ICO penalties of up to £4,350; missed duties for Nigeria's major-importance controllers |
| 16 | No breach plan | Missed 72-hour clocks |
| 17 | Dates of birth or ID scans kept "just in case" | A minimisation breach, and a honeypot for attackers |

> If you read one thing in this repo, read this table. Then check your own app against it.

## Real breaches, and the checks that catch them

The [breach casebook](framework/appendix-f-casebook.md) (Appendix F, also [online](https://bewithkhan.github.io/bansat/casebook/)) maps 20 documented incidents and regulators' decisions from 2025 and 2026 to the Bansat checks that catch each class of failure. Every case rests on a primary source: the organisation's own statement, a regulator's decision, a CVE record or the researcher who found the problem.

| Case | What happened, as reported | The check for this class of failure |
|---|---|---|
| Lovable-built apps (CVE-2025-48757) | 303 endpoints in 170 of 1,645 showcased apps let visitors read database tables | RLS on every table, with isolation tests in CI (5.4) |
| A scan of Supabase apps | 16,326 databases had tables anyone could read | Day-0 triage T2 lists every table without RLS (14.2) |
| Replit and SaaStr | An AI agent deleted a production database during a code freeze | Separate environments and keys (3.2); agents hold no production credentials (12.1) |
| "Agentjacking" | Fake error reports led AI coding agents to run outsiders' code (a research demonstration) | Text the agent reads in tool results, issues and error reports is data, not instructions (12.1) |
| Shai-Hulud | A worm spread through more than 500 npm packages, stealing tokens | Hardened CI/CD and publishing, and new versions left to age (7.4) |
| Mixpanel and OpenAI | An analytics vendor's breach exposed API users' names and email addresses | Analytics get IDs, never names or emails (5.1) |
| ICO: 23andMe, £2.31m | Credential stuffing, no mandatory MFA, and a claim of stolen data dismissed as a hoax | Passkeys or MFA for accounts that can see special category data (9.13); breached-password checks (7.1); credible claims start the breach runbook (9.20) |
| CNIL: SHEIN, €150m | Advertising cookies before any choice, and after "Refuse all" | A CI test that nothing non-essential loads before consent or after rejection (9.9) |

The cases run from apps one person built with an AI tool to the largest platforms, because the same few failures turn up at every size. Mapping them also showed gaps, mostly around AI coding agents, CI/CD and connected apps, so v2.4 added 14 checks (F.9). The checks catch the class of failure; that isn't a claim that Bansat would have prevented any one incident.

## Myth vs fact

Viral compliance threads get some of this wrong. Bansat sticks to what the law actually says:

| The claim | What's actually true |
|---|---|
| "Your app can be fined ₦10m before its first sale." | ₦10m or 2% of annual gross revenue, whichever is greater, is the most the NDPC *may* impose on a major-importance controller after an investigation. It isn't automatic. The risk is still real: targeting people in Nigeria counts as operating there, and the major-importance threshold is just 200 people in six months. |
| "Hotjar and Clarity record every keystroke, including passwords." | Not by default. The real failures are missing consent, relaxed masking and unmasked custom inputs. |
| "Anyone under 18 needs parental consent." | Under Nigeria's NDPA, yes, when you rely on consent. In the UK and EU, the age for consenting to online services is between 13 and 16. Build to your markets, or make the service 18+. |

## What's in the blueprint

15 parts and 6 appendices, about 3,800 lines of Markdown, split into [modules](framework/README.md) so Claude reads only the parts each step needs. `reference/bansat-framework.md` has the whole framework in one file, and you can [read it online](https://bewithkhan.github.io/bansat/) as a searchable site with no cookies or trackers, rebuilt from this repo on every merge.

Planning with a PRD, TRD, app flow, UI/UX brief, backend schema and implementation plan? They're all here, as parts of one blueprint that can't contradict itself: the [map](framework/how-to-use.md#where-the-usual-planning-documents-live) shows where each one lives.

| Part | What it nails down |
|---|---|
| **0** Intake & risk | 20 screening questions, a risk tier, and which modules switch on |
| **1** Product (PRD) | Problem, metrics, personas, MoSCoW features with IDs, Gherkin acceptance criteria |
| **2** Non-functional | Targets with numbers: Core Web Vitals, uptime, recovery, security baseline, accessibility, cost |
| **3** Architecture (TRD) | Pinned stack, environments, a context diagram, data flows, a processor register, secrets |
| **4** Decisions (ADRs) | Baseline architecture decisions, accepted unless you override them, each recording the prior art considered |
| **5** Data | Classification, a data dictionary, an entity-relationship diagram, a Prisma 7 schema, RLS policies with isolation tests, retention |
| **6** API | An endpoint inventory, RFC 9457 errors mapped to UI states, validation, webhooks, rate limits |
| **7** Security | An authentication flow based on OWASP, an RBAC matrix, 16 pre-filled STRIDE threats, headers and CSP, logging rules |
| **8** AI features | A feature register with pinned models, data minimisation, evals, prompt-injection defences, EU AI Act labelling, spend caps |
| **9** Privacy & compliance | 27 sections, from lawful bases, consent and cookies to children, marketing, rights, breaches and DPIAs |
| **10** Experience | The app flow and a navigation map, a look-and-feel brief, WCAG 2.2 AA, contrast-checked design tokens, and 17 mandatory UI states for every screen |
| **11** Delivery | 9 phases with exit criteria, a Definition of Done, RLS, privacy and E2E tests, CI/CD, incidents, a delivery-method picker and a project-management toolkit (RACI, RAID log, status report, change requests, timeline, forecasting) |
| **12** Agent manual | Rules for `CLAUDE.md` or `AGENTS.md` (research before building), a feature protocol, an 8-step debugging protocol, and prompts for research, logic, debugging and full audits |
| **13** Launch gate | 40 go/no-go checks; any one of the 18 blocking checks stops the launch |
| **14** Retrofit | Day-0 triage, reverse-engineering, a gap register, a review cadence and a portfolio register |
| **A–F** Appendices | Templates (DPIA, LIA, breach log and more), references, a glossary, 9 stack profiles, 6 delivery-method profiles (Scrum, Kanban, stage gates, Shape Up, a solo loop and scaled methods), and a breach casebook of 20 documented incidents and regulators' decisions |

## Why not a PRD template, or just ask the AI?

| | PRD template | Asking the AI | **Bansat** |
|---|:---:|:---:|:---:|
| Product, architecture, security, privacy and UX in one plan | Product only | Patchy | ✅ |
| Gates Claude stops at until you approve | ❌ | ❌ | ✅ G0–G4, plus an optional G1 hook |
| Laws cited to the section, and checked on a stated date | ❌ | Unverified | ✅ 4 Nov 2026 |
| Code samples tested, in CI | ❌ | Untested | ✅ `examples/` |
| Looks at how others solved it before building | ❌ | Rarely | ✅ Research first |
| Fits how you run the project (Scrum, Kanban, stage gates, Shape Up, solo) | ❌ | ❌ | ✅ A picker and 6 profiles |
| A track for apps that are already live | ❌ | ❌ | ✅ Day-0 triage |
| Every UI state designed and tested on every screen | ❌ | Happy path only | ✅ 17 states |
| The same standard on every project, team and tool | Copy and paste | Per chat | ✅ One skill, one file |

## Tested, not just written

The framework's code runs in CI on every change, from the [`examples/`](examples/README.md) folder:

- The **Prisma 7 schema** creates its tables in a real Postgres database, with the connection in `prisma.config.ts` as Prisma 7 requires.
- The **RLS policies** pass 13 access checks on those tables (tenants, roles, writes, anonymous visitors), and the Day-0 audit query finds any table left without RLS.
- The **consent loader** runs in Chromium: a vendor script loads only after "Accept all", and every choice is logged.
- The **Playwright privacy test** passes on clean pages and must fail on a page that loads a third-party font before consent.
- The **TypeScript UI-state pattern** compiles in strict mode, and the build fails if a screen forgets a state.

CI also fails if a sample in the framework and its tested copy differ, so what you copy is what was tested ([issue #1](https://github.com/bewithkhan/bansat/issues/1)). Run it yourself: `cd examples && npm ci && npx playwright install chromium && npm test`. The G1 hook's tests run with `node hooks/bansat-g1-guard.test.mjs`.

<details>
<summary><b>Laws and standards covered</b> (checked 4 November 2026)</summary>

<br>

- UK GDPR and the Data Protection Act 2018, as amended by the Data (Use and Access) Act 2025
- PECR (cookies and electronic marketing), with fines now at UK GDPR levels
- The UK Children's Code (Age Appropriate Design Code)
- EU GDPR and the ePrivacy rules, including the EU representative check
- Nigeria Data Protection Act 2023 and the GAID 2025 directive
- EU AI Act: transparency duties (Art. 50), AI literacy and the high-risk check
- UK Online Safety Act 2023, for apps with user-to-user content
- UK PSTI product security rules for connectable devices, and the European Accessibility Act
- US: COPPA, CAN-SPAM, TCPA and state privacy laws
- Pointers for India, South Africa, Kenya and Pakistan
- Standards: WCAG 2.2 AA · OWASP ASVS · OWASP Top 10 for LLM Applications · STRIDE · RFC 9457 · RFC 9116 · RFC 2119

**How this was checked:** the legal sections were researched and checked against official sources (the legislation, regulator guidance and court decisions listed in Appendix B of the framework) with AI assistance, as of 4 November 2026. No lawyer has reviewed them yet. Laws change, so re-check anything you rely on before launch; see the [disclaimer](#disclaimer).

</details>

<details>
<summary><b>Works for any kind of project</b> (9 stack profiles)</summary>

<br>

| Profile | What it adds |
|---|---|
| D1 Web app (Next.js + Supabase) | The reference stack the examples use |
| D2 Progressive web app | Cache rules, offline and sync states, web push limits, and no background location on the web |
| D3 Mobile (Flutter, React Native, native) | No secrets in the binary, secure storage, store privacy labels, and Firebase rules as your RLS |
| D4 Wearables, BLE and connected devices | Device states, vendor clouds and transfers, consent from the people being monitored, PSTI |
| D5 Admin panels and CRMs | You're usually the client's processor, even when the client is abroad; exports are the biggest leak |
| D6 Research prototypes | Ethics approval, a data management plan, pseudonymisation at collection |
| D7 Websites and waitlists | A waitlist is already personal data, so the privacy basics come first |
| D8 AI agents and clinical prototypes | Synthetic data until reviewed, a medical-device check, a human in the loop |
| D9 Hardware with no personal data | Which parts apply, and when a sensor switches privacy on |

Using Laravel, Rails, Django or a database without RLS, such as MySQL? The same rules apply: access is enforced in the data layer instead, and every controller re-checks authorisation. A dedicated profile for these stacks is on the way.

</details>

## FAQ

<details>
<summary><b>Is this legal advice?</b></summary>

<br>

No. It summarises the law and regulator guidance as of 4 November 2026, cites the sections, and says when to get a qualified review: health, children, biometrics, worker monitoring or large-scale tracking. The legal sections were checked against official sources with AI assistance, and no lawyer has reviewed them yet.

</details>

<details>
<summary><b>Won't a blueprint slow me down?</b></summary>

<br>

It scales with risk. A low-risk project fills in only the essentials: intake, the core product spec, the stack, cookies and third-party requests, the launch traps, UI states and the launch gate. High-risk projects get everything, because that's where mistakes cost the most.

</details>

<details>
<summary><b>Do I need Claude?</b></summary>

<br>

No. The framework is plain Markdown that you can fill in by hand or with any AI agent. Claude Code just does the most for you: it screens your idea, writes the blueprint and stops at each gate.

</details>

<details>
<summary><b>My team already has its own process.</b></summary>

<br>

Your process wins. If your repo's `CLAUDE.md`, `AGENTS.md` or docs set out their own planning stages or templates, Bansat follows them, maps its gates onto your stages, and fills the gaps it covers best: privacy law, security and UI states.

</details>

<details>
<summary><b>My country isn't covered.</b></summary>

<br>

The engineering half applies everywhere, and the laws table has pointers for other markets. Open an issue with your country's law and a link to an official source.

</details>

<details>
<summary><b>Can I use it commercially?</b></summary>

<br>

Yes. The text and templates are CC BY 4.0, so you can use and adapt them commercially as long as you keep the credit line. The code samples are MIT.

</details>

<details>
<summary><b>How do I keep a project's blueprint current?</b></summary>

<br>

It lives in your repo as `docs/BLUEPRINT.md` and changes in the same pull request as the code. Live projects get a monthly review and a quarterly launch-trap audit.

</details>

## Cite

Use GitHub's **Cite this repository** button, or:

> Khan, M. T. (2026). *Bansat (بنسټ): A blueprint-first framework for building software* (Version 2.4) [Computer software]. GitHub. https://github.com/bewithkhan/bansat

```bibtex
@misc{khan2026bansat,
  author       = {Khan, Muhammad Tufail},
  title        = {Bansat: A blueprint-first framework for building software},
  year         = {2026},
  note         = {Version 2.4},
  howpublished = {�rl{https://github.com/bewithkhan/bansat}}
}
```

## Contributing

Bansat gets better with every project it meets. Contributions are welcome:

- **A law changed?** Open an issue with a link to the official source.
- **A stack that isn't covered?** Propose a profile for Appendix D.
- **A launch trap that bit you?** Describe what happened and how you fixed it.

Keep the credit line in anything you copy or adapt (CC BY 4.0).

## Author

**Muhammad Tufail Khan**: PhD researcher in AI and IoT for healthcare at the University of the West of Scotland, based in Glasgow.

<a href="https://www.linkedin.com/in/bewithkhan/"><img alt="Connect on LinkedIn" src="https://img.shields.io/badge/LinkedIn-Connect-0A66C2?style=for-the-badge"></a>
<a href="https://github.com/bewithkhan"><img alt="Follow on GitHub: bewithkhan" src="https://img.shields.io/badge/GitHub-bewithkhan-24292F?style=for-the-badge"></a>

## Licence

- **Text and templates:** [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), full text in `LICENSE`. You can use, adapt and share them, including commercially, if you keep this credit: *"Bansat (بنسټ) by Muhammad Tufail Khan, https://github.com/bewithkhan/bansat, CC BY 4.0"*.
- **Code samples:** MIT (see `LICENSE-CODE`).
- `NOTICE` sets out which licence covers what, with the credit line.

## Disclaimer

Bansat summarises laws and regulator guidance as of 4 November 2026. It is not legal advice. The legal sections were checked against official sources with AI assistance and have not been reviewed by a lawyer. Check the current rules before you launch, and get a qualified review for high-risk processing.

---

<p align="center">
  If Bansat saves you from one bad launch, <b>star the repo</b> so the next builder finds it before theirs.<br>
  Built by Muhammad Tufail Khan · <a href="https://www.linkedin.com/in/bewithkhan/">LinkedIn</a> · <a href="https://github.com/bewithkhan">GitHub</a>
</p>
````

## File: watch/README.md
size: 4318
sha: 1c7b7878c280e64985cf6c3415c46eb4b6eb9658
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/watch/README.md

````text
# Law Watch

Law Watch keeps the law registry current (blueprint Phase 4). It has two parts, and a person reviews and merges every
change:

1. **The watch** (`.github/workflows/watch.yml`, running `scripts/watch.ts`). Every Monday at 06:23 UTC it fetches each
   official feed and page the registry watches and compares it with the last run. When something changed, it opens
   an issue for that jurisdiction, labelled `law-watch` and `law:<id>`, listing the new items. If that issue is
   already open, it comments on it instead. Sources that fail to load go in one `[watch] Source failing` issue,
   labelled `law-watch-failing`, so a failure is never silent; that issue closes itself once every source loads.
2. **The verify task**: a weekly Claude task that follows [`verify-prompt.md`](verify-prompt.md). It works through
   the open watch issues, checks each possible change against the official sources, and prepares a pull request for
   any field that needs to change, or comments and labels the issue `no-change`.

## What it watches

Each entry lists its watched sources under `watch`. Each item gives a `url` on one of the entry's official domains, a
`type`, and the `language` of the page:

| `type` | What it reads | What counts as a change |
|---|---|---|
| `rss`, `atom` | A feed's items: title, link and date | A new or changed item |
| `html` | The part of the page that `selector` (a CSS selector) picks out | New links, or edited text, inside that part |
| `api` | A JSON response: a list of documents with titles and links (as the Federal Register's API returns), SPARQL results, or any other JSON | A new document, or any change to the response |

```yaml
watch:
  - url: https://www.legislation.gov.uk/changes/affected/ukpga/2018/12/data.feed
    type: atom
    language: en
  - url: https://cppa.ca.gov/regulations/
    type: html
    selector: main.main-primary > article
    language: en
```

Pick feeds where they exist, and point a selector at a stable list of news or changes, not the whole page, so a
changed banner or date doesn't raise a false alarm. A source two entries share is fetched once.

An office-wide news list (an attorney general's press releases, say) changes every week for reasons that have
nothing to do with the registry. Give it `keywords`: then only items whose title contains one of them, as whole
words in any case, count, and unrelated news changes nothing:

```yaml
  - url: https://news.delaware.gov/category/justice/feed/
    type: rss
    keywords: [privacy, data, children, social media, AI, robocall]
    language: en
```

## How it behaves

- **One request per source per run**, at most one per second to each site (or the site's `Crawl-delay`, up to a
  minute), with the user agent `Bansat-LawWatch/1.0 (+https://github.com/bewithkhan/bansat)`. It reads each site's
  `robots.txt` first, and a page that robots.txt disallows is reported as failing, not fetched.
- **Two retries, then failing.** A request that times out, hits a network error or gets HTTP 403, 408, 429 or 5xx is
  tried again 5 and 15 seconds later (or after the site's `Retry-After`, up to a minute). Some official sites sit
  behind load balancers that refuse a share of requests at random; a source only goes in the failing issue when all
  three tries fail. A 404 or an oversized page isn't retried.
- **State without commits.** Between runs the state lives in a workflow artifact (`law-watch-state`, kept 90 days),
  so the watch never commits to the repo. It holds a hash plus each item's title, link and date, never page text
  (BR-05). With no earlier state, a run sets a baseline and opens no change issues.
- **Least privilege.** The workflow can read the repo and its own artifacts, and write issues. It holds no secrets.

## Running it yourself

```sh
node scripts/watch.ts --state /tmp/watch-state.json --write-state /tmp/watch-state.json   # print what changed
node scripts/watch.ts --only uk,ie --state /tmp/watch-state.json                          # some entries only
node scripts/watch.test.ts                                                                # the tests, offline
```

Without `--issues`, it prints the report and opens nothing. The tests replay a known past change, the Data (Use and
Access) Act 2025 amending the Data Protection Act 2018, from fixtures.
````

## File: scripts/build-framework.ts
size: 3830
sha: 8119cbf6421bfa8b6852bcc6dd5d9c75941b03e7
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/build-framework.ts

````text
// Builds reference/bansat-framework.md from the modules in framework/.
//
//   node scripts/build-framework.ts           write the combined file
//   node scripts/build-framework.ts --check   exit 1 if the combined file is out of date (used in CI)
//
// Edit the modules, never the combined file: CI fails if the two drift apart.
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { COMBINED_FILE, FRAMEWORK_DIR, MODULES } from './modules.ts';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
// The header must make sense in a project's own docs/BLUEPRINT.md too, because people copy this file.
const HEADER =
  '<!-- Bansat framework, generated from framework/*.md by scripts/build-framework.ts ' +
  '(https://github.com/bewithkhan/bansat). If you copied this file to start a project blueprint, ' +
  'you can delete this line. -->\n';
const SEPARATOR = '\n\n---\n\n';

// Git on Windows may check files out with CRLF line endings; compare and build with LF only.
const toLf = (text: string): string => text.replace(/\r\n?/g, '\n');

function fail(message: string): never {
  console.error(`build-framework: ${message}`);
  process.exit(1);
}

function buildFramework(): string {
  const dir = join(ROOT, FRAMEWORK_DIR);
  const listed: readonly string[] = MODULES;
  const onDisk = readdirSync(dir).filter((f) => f.endsWith('.md') && f !== 'README.md');
  const missing = listed.filter((f) => !onDisk.includes(f));
  const unlisted = onDisk.filter((f) => !listed.includes(f));
  if (missing.length) fail(`listed in scripts/modules.ts but missing from ${FRAMEWORK_DIR}/: ${missing.join(', ')}`);
  if (unlisted.length) fail(`in ${FRAMEWORK_DIR}/ but not listed in scripts/modules.ts: ${unlisted.join(', ')}`);

  const parts = listed.map((file) => {
    const text = toLf(readFileSync(join(dir, file), 'utf8')).trim();
    if (!text) fail(`${FRAMEWORK_DIR}/${file} is empty`);
    // Parts and appendices start with "## "; sections of an appendix kept in their own module (E1, E2…) with "### ".
    if (file !== 'cover.md' && !/^<a id="[a-z0-9-]+"><\/a>\n#{2,3} /.test(text)) {
      fail(`${FRAMEWORK_DIR}/${file} must start with an <a id="..."></a> anchor line followed by a "## " or "### " heading`);
    }
    return text;
  });
  const combined = HEADER + parts.join(SEPARATOR) + '\n';
  checkAnchors(combined);
  return combined;
}

// Every in-document link such as [Part 9](#p9) must point at an anchor that exists.
function checkAnchors(markdown: string): void {
  const outsideCode = markdown.replace(/^```[\s\S]*?^```/gm, '');
  const ids = new Set([...outsideCode.matchAll(/<a id="([^"]+)"><\/a>/g)].map((m) => m[1]));
  const broken = [...outsideCode.matchAll(/\]\(#([^)]+)\)/g)].map((m) => m[1]).filter((id) => !ids.has(id));
  if (broken.length) fail(`links to missing anchors: ${[...new Set(broken)].map((id) => `#${id}`).join(', ')}`);
}

function main(): void {
  const combined = buildFramework();
  const outPath = join(ROOT, COMBINED_FILE);
  if (process.argv.includes('--check')) {
    const current = existsSync(outPath) ? toLf(readFileSync(outPath, 'utf8')) : '';
    if (current === combined) {
      console.log(`${COMBINED_FILE} is up to date (${MODULES.length} modules).`);
      return;
    }
    const a = current.split('\n');
    const b = combined.split('\n');
    let line = 0;
    while (line < a.length && line < b.length && a[line] === b[line]) line++;
    fail(
      `${COMBINED_FILE} is out of date (first difference at line ${line + 1}). ` +
        'Run "node scripts/build-framework.ts" and commit the result.',
    );
  }
  writeFileSync(outPath, combined);
  console.log(`Wrote ${COMBINED_FILE} from ${MODULES.length} modules.`);
}

main();
````

## File: scripts/check-examples.ts
size: 3355
sha: 264ed0011b41fd9fb23e1175a41dbafe80f72120
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/check-examples.ts

````text
// Keeps examples/ identical to the code samples printed in the framework, so the code that CI
// tests is exactly the code people copy.
//
//   node scripts/check-examples.ts           exit 1 if any example differs from its sample (used in CI)
//   node scripts/check-examples.ts --write   copy each sample from the framework into examples/
//
// The framework is the source: edit the sample in framework/*.md, then run --write.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FRAMEWORK_DIR } from './modules.ts';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const toLf = (text: string): string => text.replace(/\r\n?/g, '\n');

type Sample = { example: string; module: string; firstLine: string };

// Each sample is found by the first line of its code block.
const SAMPLES: Sample[] = [
  { example: 'prisma/schema.prisma', module: '05-data.md', firstLine: '// prisma/schema.prisma' },
  { example: 'prisma.config.ts', module: '05-data.md', firstLine: '// prisma.config.ts' },
  { example: 'supabase/policies/001_rls.sql', module: '05-data.md', firstLine: '-- supabase/policies/001_rls.sql' },
  { example: 'tests/rls/tenant_isolation.sql', module: '05-data.md', firstLine: '-- tests/rls/tenant_isolation.sql' },
  { example: 'lib/privacy/consent.ts', module: '09-privacy.md', firstLine: '// lib/privacy/consent.ts' },
  {
    example: 'tests/privacy/no-third-party-before-consent.spec.ts',
    module: '09-privacy.md',
    firstLine: '// tests/privacy/no-third-party-before-consent.spec.ts',
  },
  { example: 'lib/ui/view-state.tsx', module: '10-experience.md', firstLine: '// lib/ui/view-state.tsx' },
  { example: 'tests/rls/tables_without_rls.sql', module: '14-retrofit.md', firstLine: 'select c.relname as table_without_rls' },
];

function codeBlocks(markdown: string): string[] {
  return [...markdown.matchAll(/^```[\w-]*\n([\s\S]*?)^```$/gm)].map((m) => m[1] ?? '');
}

const write = process.argv.includes('--write');
const problems: string[] = [];
for (const sample of SAMPLES) {
  const markdown = toLf(readFileSync(join(ROOT, FRAMEWORK_DIR, sample.module), 'utf8'));
  const found = codeBlocks(markdown).filter((block) => block.startsWith(sample.firstLine));
  if (found.length !== 1) {
    problems.push(`${FRAMEWORK_DIR}/${sample.module}: expected one code block starting "${sample.firstLine}", found ${found.length}`);
    continue;
  }
  const text = found[0] ?? '';
  const target = join(ROOT, 'examples', sample.example);
  if (write) {
    mkdirSync(dirname(target), { recursive: true });
    writeFileSync(target, text);
    continue;
  }
  if (!existsSync(target)) problems.push(`examples/${sample.example} is missing`);
  else if (toLf(readFileSync(target, 'utf8')) !== text) {
    problems.push(`examples/${sample.example} differs from its sample in ${FRAMEWORK_DIR}/${sample.module}`);
  }
}

if (problems.length) {
  console.error(`check-examples: ${problems.length} problem(s)\n- ${problems.join('\n- ')}`);
  if (!write) console.error('Edit the sample in the framework, then run "node scripts/check-examples.ts --write".');
  process.exit(1);
}
console.log(write ? `Wrote ${SAMPLES.length} examples from the framework.` : `All ${SAMPLES.length} examples match the framework.`);
````

## File: scripts/check-skill.ts
size: 7577
sha: 8a7775a331e7f4ea8f7a145ff2de10377df6d1c2
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/check-skill.ts

```text
// Checks SKILL.md:
// - its frontmatter follows the Agent Skills specification (https://agentskills.io/specification);
// - every repo file it names exists, and every framework module is reachable from it;
// - every pinned download link points at the release named in its metadata;
// - that release number matches CITATION.cff, the README install command, the CHANGELOG and the framework.
//
//   node scripts/check-skill.ts
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FRAMEWORK_DIR, MODULES } from './modules.ts';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const read = (file: string): string => readFileSync(join(ROOT, file), 'utf8').replace(/\r\n?/g, '\n');
const problems: string[] = [];
const problem = (message: string): void => {
  problems.push(message);
};

// --- Frontmatter (the spec allows these top-level fields only) ---
const ALLOWED_FIELDS = ['name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'];
const skill = read('SKILL.md');
const match = /^---\n([\s\S]*?)\n---\n/.exec(skill);
if (!match) {
  console.error('check-skill: SKILL.md has no YAML frontmatter');
  process.exit(1);
}
const frontmatter = match[1] ?? '';
const body = skill.slice(match[0].length);

// A deliberately small, strict parser for the shape SKILL.md uses: top-level "key: value" lines,
// and one level of "  key: value" lines under "metadata:". Anything a YAML parser could read
// differently (an unquoted ": " or " #", a leading indicator, a duplicate key, a metadata value
// that YAML would turn into a number or boolean) is reported rather than guessed at.
type Scalar = { text: string; quoted: boolean } | { error: string };
function scalar(raw: string): Scalar {
  const v = raw.trim();
  if (/^"(?:[^"\\]|\\.)*"$/.test(v)) return { text: v.slice(1, -1).replace(/\\(.)/g, '$1'), quoted: true };
  if (/^'(?:[^']|'')*'$/.test(v)) return { text: v.slice(1, -1).replace(/''/g, "'"), quoted: true };
  if (/^[[\]{},&*!|>'"%@`#]|^[-?] /.test(v) || v.includes(': ') || v.includes(' #') || v.endsWith(':')) {
    return { error: 'quote it: YAML would read this unquoted value differently' };
  }
  return { text: v, quoted: false };
}
const NOT_A_STRING = /^(?:true|false|yes|no|on|off|null|~|[-+]?(?:\d[\d_]*)?\.?\d+(?:e[-+]?\d+)?|0x[\da-f]+|0o[0-7]+)$/i;

const fields = new Map<string, string>();
const metadata = new Map<string, string>();
let parent = '';
for (const line of frontmatter.split('\n')) {
  if (!line.trim()) continue;
  const top = /^([A-Za-z][\w-]*):(?: (.*))?$/.exec(line);
  const nested = /^ {2}([A-Za-z][\w-]*):(?: (.*))?$/.exec(line);
  const [key, raw, target, where] = top
    ? [top[1] ?? '', top[2] ?? '', fields, 'frontmatter']
    : nested && parent === 'metadata'
      ? [nested[1] ?? '', nested[2] ?? '', metadata, 'metadata']
      : ['', '', null, ''];
  if (!target) {
    problem(`SKILL.md frontmatter: can't read the line "${line}"`);
    continue;
  }
  if (target.has(key)) problem(`SKILL.md ${where}: "${key}" appears more than once`);
  if (top) parent = raw.trim() ? '' : key;
  if (!raw.trim()) {
    if (!(top && key === 'metadata')) problem(`SKILL.md ${where}: "${key}" has no value`);
    target.set(key, '');
    continue;
  }
  const value = scalar(raw);
  if ('error' in value) {
    problem(`SKILL.md ${where}: "${key}": ${value.error}`);
    continue;
  }
  if (where === 'metadata' && !value.quoted && NOT_A_STRING.test(value.text)) {
    problem(`SKILL.md metadata: "${key}" must be a string (the spec says so), so quote "${value.text}"`);
  }
  target.set(key, value.text);
}
for (const key of fields.keys()) {
  if (!ALLOWED_FIELDS.includes(key)) problem(`SKILL.md frontmatter: "${key}" isn't a field the spec allows`);
}
const name = fields.get('name') ?? '';
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name) || name.length > 64) {
  problem(`SKILL.md name "${name}": 1-64 lowercase letters, digits and single hyphens`);
}
if (name !== 'bansat') problem(`SKILL.md name must be "bansat", the folder the README installs it into`);
const description = fields.get('description') ?? '';
if (description.length < 1 || description.length > 1024) {
  problem(`SKILL.md description must be 1-1024 characters (it's ${description.length})`);
}
const compatibility = fields.get('compatibility');
if (compatibility !== undefined && (compatibility.length < 1 || compatibility.length > 500)) {
  problem('SKILL.md compatibility must be 1-500 characters when present');
}
const bodyLines = body.split('\n').length;
if (bodyLines > 500) problem(`SKILL.md body is ${bodyLines} lines; the spec recommends under 500`);

const version = metadata.get('version') ?? '';
if (!/^\d+\.\d+\.\d+$/.test(version)) problem(`SKILL.md metadata.version "${version}" isn't X.Y.Z`);
const minor = version.split('.').slice(0, 2).join('.');
const tag = `v${version}`;

// --- Files named in SKILL.md exist, and every module is reachable ---
const pathPattern = /(?<![\w./-])((?:framework|reference|hooks|scripts|examples)\/[\w./-]*[\w-]\.(?:md|mjs|ts|tsx|sql|json|ya?ml))/g;
const named = new Set([...skill.matchAll(pathPattern)].map((m) => m[1] ?? ''));
for (const file of named) {
  if (!existsSync(join(ROOT, file))) problem(`SKILL.md names ${file}, which doesn't exist`);
}
for (const module of MODULES) {
  if (!named.has(`${FRAMEWORK_DIR}/${module}`)) {
    problem(`SKILL.md never names ${FRAMEWORK_DIR}/${module}, so the skill can't load it`);
  }
}

// --- Pinned links point at this release ---
for (const m of skill.matchAll(/https:\/\/raw\.githubusercontent\.com\/bewithkhan\/bansat\/([^/\s)`]+)\/([^\s)`,]*[\w-])/g)) {
  const [url, ref, file] = [m[0], m[1] ?? '', m[2] ?? ''];
  if (ref !== tag) problem(`SKILL.md links to ${url}: pin it to ${tag}`);
  if (!existsSync(join(ROOT, file))) problem(`SKILL.md links to ${file}, which doesn't exist`);
}

// --- One release number everywhere ---
const expectIn = (file: string, pattern: RegExp, expected: string, what: string): void => {
  const found = [...read(file).matchAll(pattern)].map((m) => m[1] ?? '');
  if (found.length === 0) problem(`${file}: no ${what} found`);
  for (const value of found) if (value !== expected) problem(`${file}: ${what} is ${value}, expected ${expected}`);
};
expectIn('CITATION.cff', /^version:\s*"?([\d.]+)"?\s*$/gm, version, 'version');
expectIn('README.md', /--branch (v[\d.]+)/g, tag, 'install tag');
expectIn('site/src/content/docs/index.mdx', /--branch (v[\d.]+)/g, tag, 'install tag');
expectIn('README.md', /badge\/framework-v([\d.]+)-/g, minor, 'framework badge');
const latest = /^## (v[\d.]+) /m.exec(read('CHANGELOG.md'))?.[1];
if (latest !== tag) problem(`CHANGELOG.md: the latest entry is ${latest ?? 'missing'}, expected ${tag}`);
expectIn(`${FRAMEWORK_DIR}/cover.md`, /Framework version \*\*([\d.]+)\*\*/g, minor, 'framework version');
expectIn(`${FRAMEWORK_DIR}/cover.md`, /\(Version ([\d.]+)\)/g, minor, 'citation version');
expectIn('SKILL.md', /\(Version ([\d.]+)\)/g, minor, 'citation version');
expectIn('SKILL.md', /Bansat \(بنسټ\) v([\d.]+) by Muhammad Tufail Khan/g, minor, 'attribution row');
expectIn(`${FRAMEWORK_DIR}/cover.md`, /Bansat \(بنسټ\) v([\d.]+) by Muhammad Tufail Khan/g, minor, 'Framework row');

if (problems.length) {
  console.error(`check-skill: ${problems.length} problem(s)\n- ${problems.join('\n- ')}`);
  process.exit(1);
}
console.log(
  `SKILL.md is valid: ${named.size} files named and present, all ${MODULES.length} modules reachable, pinned to ${tag}.`,
);
```

## File: scripts/modules.ts
size: 962
sha: 9574632b74aa3b24c7504780b29115403bd1d6ff
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/modules.ts

```text
// The framework's modules, in reading order. scripts/build-framework.ts joins them into
// reference/bansat-framework.md, and the checks make sure every module exists and is used.
export const FRAMEWORK_DIR = 'framework';
export const COMBINED_FILE = 'reference/bansat-framework.md';

export const MODULES = [
  'cover.md',
  'how-to-use.md',
  '00-intake.md',
  '01-prd.md',
  '02-nfr.md',
  '03-trd.md',
  '04-adrs.md',
  '05-data.md',
  '06-api.md',
  '07-security.md',
  '08-ai.md',
  '09-privacy.md',
  '10-experience.md',
  '11-delivery.md',
  '12-agent-manual.md',
  '13-launch-gate.md',
  '14-retrofit.md',
  'appendix-a-templates.md',
  'appendix-b-references.md',
  'appendix-c-glossary.md',
  'appendix-d-stacks.md',
  'appendix-e-methods.md',
  'appendix-e1-scrum.md',
  'appendix-e2-kanban.md',
  'appendix-e3-stage-gates.md',
  'appendix-e4-shape-up.md',
  'appendix-e5-solo.md',
  'appendix-e6-scaled.md',
  'appendix-f-casebook.md',
] as const;
```

## File: scripts/validate-laws.test.ts
size: 4283
sha: 93027772311df12520eccd17da36c0a874b61ce4
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/validate-laws.test.ts

```text
// Tests for scripts/validate-laws.ts: a valid entry passes, and each broken copy fails for the right reason.
//   node scripts/validate-laws.test.ts
import { spawnSync } from 'node:child_process';
import { cpSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'yaml';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const VALIDATOR = join(ROOT, 'scripts/validate-laws.ts');
const base = parse(readFileSync(join(ROOT, 'laws/full/uk.yaml'), 'utf8'));

type Case = { name: string; expect: RegExp | null; change?: (entry: any) => void; file?: string; tier?: string; raw?: (text: string) => string };
const cases: Case[] = [
  { name: 'a valid full entry passes', expect: null },
  { name: 'a source off the official domains fails', expect: /isn't on an official domain/, change: (e) => { e.obligations.cookies.source.push('https://example.com/cookie-guide'); } },
  { name: 'a lead listed under secondary passes', expect: null, change: (e) => { e.obligations.cookies.secondary = ['https://example.com/cookie-guide']; } },
  { name: 'a full entry without obligations fails', expect: /must have required property 'obligations'/, change: (e) => { delete e.obligations; } },
  { name: 'an obligation with no source fails', expect: /source must NOT have fewer than 1 items/, change: (e) => { e.obligations.dpo.source = []; } },
  { name: 'human-reviewed without a reviewer fails', expect: /must have required property 'by'/, change: (e) => { e.checked.status = 'human-reviewed'; } },
  { name: 'an id that differs from the file name fails', expect: /doesn't match the file name/, change: (e) => { e.id = 'gb'; } },
  { name: 'a check date in the future fails', expect: /is in the future/, change: (e) => { e.checked.date = '2999-01-01'; } },
  { name: 'an impossible date fails', expect: /isn't a real date|must match format "date"/, change: (e) => { e.checked.date = '2026-02-30'; } },
  { name: 'a placeholder left in fails', expect: /placeholder/, change: (e) => { e.obligations.dpo.summary = 'Appoint a DPO when {{condition}}.'; } },
  { name: 'applies_with naming a missing entry fails', expect: /has no entry/, change: (e) => { e.applies_with = ['eu']; } },
  { name: 'a basic entry with obligations fails', expect: /must NOT be valid/, tier: 'basic', change: (e) => { e.tier = 'basic'; } },
  { name: 'an unknown field fails', expect: /must NOT have additional properties/, change: (e) => { e.summary = 'extra'; } },
  { name: 'a number without a unit fails', expect: /must have required property 'unit'/, change: (e) => { delete e.obligations.consent_age.unit; } },
  { name: 'a unit without a number fails', expect: /must NOT be valid/, change: (e) => { e.obligations.dpo.unit = 'days'; } },
  { name: 'a duplicate YAML key fails', expect: /isn't valid YAML/, raw: (t) => t.replace('tier: full\n', 'tier: full\ntier: full\n') },
];

let failed = 0;
for (const c of cases) {
  const dir = mkdtempSync(join(tmpdir(), 'bansat-laws-'));
  try {
    const tier = c.tier ?? 'full';
    mkdirSync(join(dir, 'laws', tier), { recursive: true });
    mkdirSync(join(dir, 'watch'), { recursive: true });
    cpSync(join(ROOT, 'laws/schema.json'), join(dir, 'laws/schema.json'));
    cpSync(join(ROOT, 'watch/sources.yaml'), join(dir, 'watch/sources.yaml'));
    const entry = structuredClone(base);
    c.change?.(entry);
    let text = stringify(entry);
    if (c.raw) text = c.raw(text);
    writeFileSync(join(dir, 'laws', tier, c.file ?? 'uk.yaml'), text);
    const run = spawnSync(process.execPath, [VALIDATOR, '--root', dir], { encoding: 'utf8' });
    const output = `${run.stdout}${run.stderr}`;
    const ok = c.expect === null ? run.status === 0 : run.status === 1 && c.expect.test(output);
    console.log(`${ok ? 'PASS' : 'FAIL'}  ${c.name}`);
    if (!ok) {
      failed++;
      console.log(output.split('\n').slice(0, 6).map((l) => `      ${l}`).join('\n'));
    }
  } finally {
    rmSync(dir, { recursive: true, force: true });
  }
}
console.log(failed ? `\n${failed} of ${cases.length} tests failed` : `\nAll ${cases.length} tests passed`);
process.exit(failed ? 1 : 0);
```

## File: scripts/validate-laws.ts
size: 6351
sha: bca9eba3a1970a6467863886fe87cf48ae90686c
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/validate-laws.ts

```text
// Validates the law registry (laws/) against laws/schema.json and the rules in the blueprint (5.4):
//   - every entry matches the schema, lives at laws/<tier>/<id>.yaml, and names only entries that exist;
//   - every law, regulator, source and watch URL is on an official domain for its jurisdiction
//     (watch/sources.yaml), unless it's listed under `secondary`;
//   - dates are real, not in the future, and history runs newest first; no {{placeholders}} are left.
//
//   node scripts/validate-laws.ts                 check this repo
//   node scripts/validate-laws.ts --root <dir>    check another copy (used by the tests)
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import { parse } from 'yaml';

const rootFlag = process.argv.indexOf('--root');
const ROOT = rootFlag > 0 && process.argv[rootFlag + 1] ? process.argv[rootFlag + 1] : join(dirname(fileURLToPath(import.meta.url)), '..');
const LAWS = join(ROOT, 'laws');
const TIERS = ['full', 'basic'] as const;
const problems: string[] = [];
const problem = (file: string, message: string): void => {
  problems.push(`${file}: ${message}`);
};

type Entry = {
  id: string;
  tier: string;
  applies_with?: string[];
  laws: { official_url: string; in_force_from?: string }[];
  regulators: { url: string }[];
  obligations?: Record<string, { source: string[]; secondary?: string[] }>;
  checked: { date: string };
  history: { date: string }[];
  watch?: { url: string }[];
};

const readYaml = (path: string): unknown => parse(readFileSync(path, 'utf8'), { uniqueKeys: true, prettyErrors: true });

// strictRequired is off because the tier rules (allOf) require properties defined at the top level.
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true });
addFormats(ajv);
const validate = ajv.compile(JSON.parse(readFileSync(join(LAWS, 'schema.json'), 'utf8')));

const sources = readYaml(join(ROOT, 'watch/sources.yaml')) as {
  jurisdictions?: Record<string, { official_domains?: string[] }>;
};
const domainsFor = (id: string): string[] => sources.jurisdictions?.[id]?.official_domains ?? [];

// Load every entry.
const entries = new Map<string, { file: string; entry: Entry }>();
for (const tier of TIERS) {
  const dir = join(LAWS, tier);
  if (!existsSync(dir)) continue;
  for (const name of readdirSync(dir).filter((f) => !f.startsWith('.')).sort()) {
    const file = `laws/${tier}/${name}`;
    if (!name.endsWith('.yaml')) {
      problem(file, 'registry files must end in .yaml');
      continue;
    }
    let data: unknown;
    try {
      data = readYaml(join(dir, name));
    } catch (error) {
      problem(file, `isn't valid YAML: ${(error as Error).message.split('\n')[0]}`);
      continue;
    }
    if (!validate(data)) {
      for (const e of validate.errors ?? []) problem(file, `${e.instancePath || '(top level)'} ${e.message ?? 'is invalid'}`);
      continue;
    }
    const entry = data as Entry;
    if (`${entry.id}.yaml` !== name) problem(file, `id "${entry.id}" doesn't match the file name`);
    if (entry.tier !== tier) problem(file, `tier "${entry.tier}" doesn't match its folder (${tier})`);
    if (entries.has(entry.id)) problem(file, `duplicate id "${entry.id}" (also ${entries.get(entry.id)?.file})`);
    entries.set(entry.id, { file, entry });
  }
}

const today = new Date().toISOString().slice(0, 10);
const realDate = (d: string): boolean => {
  const parsed = new Date(`${d}T00:00:00Z`);
  return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === d;
};
const onDomain = (url: string, domains: string[]): boolean => {
  const host = new URL(url).hostname.toLowerCase();
  return domains.some((d) => host === d || host.endsWith(`.${d}`));
};

let sourceCount = 0;
for (const { file, entry } of entries.values()) {
  const raw = readFileSync(join(ROOT, file), 'utf8');
  if (raw.includes('{{')) problem(file, 'has a {{placeholder}} left in it');

  for (const id of entry.applies_with ?? []) {
    if (id === entry.id) problem(file, 'applies_with lists the entry itself');
    else if (!entries.has(id)) problem(file, `applies_with names "${id}", which has no entry`);
  }

  const own = domainsFor(entry.id);
  if (own.length === 0) problem(file, `watch/sources.yaml lists no official domains for "${entry.id}"`);
  const allowed = [...own, ...(entry.applies_with ?? []).flatMap(domainsFor)];
  const checkUrl = (url: string, where: string): void => {
    sourceCount++;
    if (!onDomain(url, allowed)) problem(file, `${where} ${url} isn't on an official domain for ${entry.id} (watch/sources.yaml); list it under secondary if it's only a lead`);
  };
  entry.laws.forEach((law, i) => checkUrl(law.official_url, `laws[${i}].official_url`));
  entry.regulators.forEach((r, i) => checkUrl(r.url, `regulators[${i}].url`));
  for (const [name, obligation] of Object.entries(entry.obligations ?? {})) {
    obligation.source.forEach((url, i) => checkUrl(url, `obligations.${name}.source[${i}]`));
  }
  (entry.watch ?? []).forEach((w, i) => checkUrl(w.url, `watch[${i}].url`));

  const dates: [string, string][] = [
    ['checked.date', entry.checked.date],
    ...entry.laws.flatMap((l, i): [string, string][] => (l.in_force_from ? [[`laws[${i}].in_force_from`, l.in_force_from]] : [])),
    ...entry.history.map((h, i): [string, string] => [`history[${i}].date`, h.date]),
  ];
  for (const [where, d] of dates) {
    if (!realDate(d)) problem(file, `${where} "${d}" isn't a real date`);
  }
  for (const [where, d] of dates.filter(([w]) => !w.startsWith('laws['))) {
    if (d > today) problem(file, `${where} ${d} is in the future`);
  }
  for (let i = 1; i < entry.history.length; i++) {
    if ((entry.history[i]?.date ?? '') > (entry.history[i - 1]?.date ?? '')) problem(file, 'history must run newest first');
  }
}

if (problems.length) {
  console.error(`validate-laws: ${problems.length} problem(s)\n- ${problems.join('\n- ')}`);
  process.exit(1);
}
const full = [...entries.values()].filter((e) => e.entry.tier === 'full').length;
console.log(`laws/ is valid: ${entries.size} entries (${full} full, ${entries.size - full} basic), ${sourceCount} URLs on official domains.`);
```

## File: scripts/watch-lib.ts
size: 13983
sha: ad00c93faa69923c071ce98b3198a91c9ab8f89f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/watch-lib.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/watch-lib.ts

## File: scripts/watch.test.ts
size: 22142
sha: 176682ca3f4260f4c28cdf47b0869e00a15e4916
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/watch.test.ts

```text
// Tests for Law Watch (scripts/watch-lib.ts and scripts/watch.ts). No network: the end-to-end runs answer from
// fixtures, replaying a known past change (the Data (Use and Access) Act 2025 amending the Data Protection Act 2018).
//   node scripts/watch.test.ts
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'yaml';
import { changeText, compare, decode, extract, failureText, keywordPattern, loadSources, robotsAllows, robotsCrawlDelay, type Source, type WatchItem } from './watch-lib.ts';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const WATCH = join(ROOT, 'scripts/watch.ts');

const FEED = 'https://www.legislation.gov.uk/changes/affected/ukpga/2018/12/data.feed';
const PAGE = 'https://ico.org.uk/about-the-ico/media-centre/news-and-blogs/';
const SECRET_TEXT = 'This paragraph is page text and must never reach the state file.';

const atom = (entries: { title: string; link: string; updated: string }[]): string => `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Changes affecting the Data Protection Act 2018</title>
  <id>${FEED}</id>
  <updated>2025-06-20T00:00:00Z</updated>
  ${entries.map((e) => `<entry><title>${e.title}</title><id>${e.link}</id><link rel="alternate" href="${e.link}"/><updated>${e.updated}</updated></entry>`).join('\n  ')}
</feed>`;
const OLD_ENTRY = { title: 'Data Protection Act 2018 s. 3: words inserted by the Data Protection (Amendment) Regulations 2024', link: 'https://www.legislation.gov.uk/uksi/2024/1/regulation/2', updated: '2024-03-01T00:00:00Z' };
const NEW_ENTRY = { title: 'Data Protection Act 2018 s. 3: words substituted by the Data (Use and Access) Act 2025 c. 18, s. 70', link: 'https://www.legislation.gov.uk/ukpga/2025/18/section/70', updated: '2025-06-19T00:00:00Z' };

const html = (items: string[], paragraph = SECRET_TEXT): string => `<!doctype html><html><head><title>News</title><script>var t = Date.now();</script></head>
<body><nav><a href="/">Home</a></nav><main><div class="news-list"><p>${paragraph}</p>
${items.map((t, i) => `<article><a href="/about-the-ico/media-centre/news-and-blogs/2025/0${i + 1}/item-${i + 1}/">${t}</a></article>`).join('\n')}
<script>document.write(Math.random())</script></div></main></body></html>`;

const rss = `<?xml version="1.0"?><rss version="2.0"><channel><title>Regulator news</title>
<item><title>New guidance on cookies</title><link>https://www.cnil.fr/fr/cookies-2026</link><pubDate>Mon, 02 Nov 2026 09:00:00 GMT</pubDate></item>
<item><title>Sanction</title><guid>https://www.cnil.fr/fr/sanction-2026</guid></item></channel></rss>`;

const sparql = JSON.stringify({
  head: { vars: ['act', 'title', 'date'] },
  results: { bindings: [{ act: { type: 'uri', value: 'http://publications.europa.eu/resource/celex/32016R0679' }, title: { type: 'literal', value: 'GDPR consolidated' }, date: { type: 'literal', value: '2016-05-04' } }] },
});

let passed = 0;
const failures: string[] = [];
const test = (name: string, fn: () => void): void => {
  try {
    fn();
    passed++;
    console.log(`PASS  ${name}`);
  } catch (error) {
    failures.push(name);
    console.log(`FAIL  ${name}\n      ${error instanceof Error ? error.message.split('\n').join('\n      ') : error}`);
  }
};

const atomItem: WatchItem = { url: FEED, type: 'atom', language: 'en' };
const htmlItem: WatchItem = { url: PAGE, type: 'html', selector: '.news-list', language: 'en' };

test('an Atom feed gives each entry title, alternate link and date', () => {
  const { items } = extract(atomItem, atom([NEW_ENTRY, OLD_ENTRY]));
  assert.equal(items.length, 2);
  assert.deepEqual(items[0], { title: NEW_ENTRY.title, link: NEW_ENTRY.link, date: NEW_ENTRY.updated });
});

test('an RSS feed gives items, falling back to the guid for a link', () => {
  const { items } = extract({ url: 'https://www.cnil.fr/fr/rss.xml', type: 'rss', language: 'fr' }, rss);
  assert.equal(items.length, 2);
  assert.equal(items[0].date, 'Mon, 02 Nov 2026 09:00:00 GMT');
  assert.equal(items[1].link, 'https://www.cnil.fr/fr/sanction-2026');
});

test('a page keeps only the links inside the selector, made absolute, and ignores scripts', () => {
  const one = extract(htmlItem, html(['Fine for a retailer']));
  const two = extract(htmlItem, html(['Fine for a retailer']));
  assert.deepEqual(one.items, [{ title: 'Fine for a retailer', link: 'https://ico.org.uk/about-the-ico/media-centre/news-and-blogs/2025/01/item-1/' }]);
  assert.equal(one.hash, two.hash, 'scripts that change on every load must not change the hash');
});

test('an edited paragraph changes a page hash even with no new link', () => {
  const before = extract(htmlItem, html(['Fine for a retailer']));
  const after = extract(htmlItem, html(['Fine for a retailer'], 'The guidance was updated.'));
  assert.notEqual(before.hash, after.hash);
  assert.equal(compare({ ...before, checked: '2026-11-02' }, after).kind, 'changed');
});

test('a selector that matches nothing fails loudly', () => {
  assert.throws(() => extract({ ...htmlItem, selector: '.renamed-list' }, html([])), /matched nothing/);
});

test("a response that isn't the feed type it should be fails", () => {
  assert.throws(() => extract(atomItem, '<html><body>Access denied</body></html>'), /isn't an Atom feed/);
  assert.throws(() => extract({ url: 'https://op.europa.eu/sparql', type: 'api', language: 'en' }, '<html></html>'), /isn't JSON/);
});

test('SPARQL results become items with their URI as the link', () => {
  const { items } = extract({ url: 'https://publications.europa.eu/webapi/rdf/sparql', type: 'api', language: 'en' }, sparql);
  assert.deepEqual(items, [{ title: 'GDPR consolidated · 2016-05-04', link: 'http://publications.europa.eu/resource/celex/32016R0679' }]);
});

const officeFeed = (titles: string[]): string =>
  `<?xml version="1.0"?><rss version="2.0"><channel><title>Attorney General news</title>${titles.map((t, i) => `<item><title>${t}</title><link>https://ago.example.gov/news/${i}</link></item>`).join('')}</channel></rss>`;
const agItem: WatchItem = { url: 'https://ago.example.gov/rss/news', type: 'rss', keywords: ['privacy', 'data', 'children', 'AI', 'social media'], language: 'en' };

test('with keywords, only matching items count, as whole words in any case', () => {
  const { items } = extract(agItem, officeFeed(['AG sues app over children\'s data', 'Highway contract fraud charges', 'New AI chatbot rules for minors', 'Database of road closures', 'Officer said the case is closed', 'Social  Media age checks begin']));
  assert.deepEqual(items.map((i) => i.title), ["AG sues app over children's data", 'New AI chatbot rules for minors', 'Social Media age checks begin']);
  assert.equal(keywordPattern(['AI']).test('said'), false);
  // Soft hyphens inside words (one Attorney General's site uses them) don't hide a keyword.
  assert.deepEqual(extract(agItem, officeFeed(['Pri­va­cy settlement'])).items.map((i) => i.title), ['Privacy settlement']);
  assert.equal(keywordPattern(['données personnelles']).test('Les DONNÉES personnelles des élèves'), true);
});

test('with keywords, unrelated news changes nothing, and relevant news is reported', () => {
  const before = extract(agItem, officeFeed(['Highway contract fraud charges']));
  const unrelated = extract(agItem, officeFeed(['Tax scam warning', 'Highway contract fraud charges']));
  assert.equal(compare({ ...before, checked: '2026-11-02' }, unrelated).kind, 'same');
  const relevant = extract(agItem, officeFeed(['Settlement over location data sales', 'Tax scam warning', 'Highway contract fraud charges']));
  const result = compare({ ...before, checked: '2026-11-02' }, relevant);
  assert.equal(result.kind, 'changed');
  assert.deepEqual(result.newItems.map((i) => i.title), ['Settlement over location data sales']);
});

test("a JSON list of documents, like the Federal Register's, gives each title, link and date", () => {
  const body = JSON.stringify({ count: 2, results: [
    { document_number: '2026-01234', title: "Children's Online Privacy Protection Rule", html_url: 'https://www.federalregister.gov/documents/2026/01/16/2026-01234/childrens-online-privacy-protection-rule', publication_date: '2026-01-16', type: 'Rule' },
    { document_number: '2025-09876', title: 'Telemarketing Sales Rule', html_url: 'https://www.federalregister.gov/documents/2025/05/02/2025-09876/telemarketing-sales-rule', publication_date: '2025-05-02', type: 'Proposed Rule' },
  ] });
  const { items } = extract({ url: 'https://www.federalregister.gov/api/v1/documents.json?conditions%5Bcfr%5D%5Btitle%5D=16', type: 'api', language: 'en' }, body);
  assert.deepEqual(items[0], { title: "Children's Online Privacy Protection Rule", link: 'https://www.federalregister.gov/documents/2026/01/16/2026-01234/childrens-online-privacy-protection-rule', date: '2026-01-16' });
  assert.equal(items.length, 2);
});

test('a JSON news list gives links from newsUrl or @id fields', () => {
  const ok = JSON.stringify({ totalCount: 711, items: [{ newsUrl: '/oag/news/newsroom/2026/october/settlement.html', title: 'Settlement over app data', lastModified: 'Oct 30, 2026' }] });
  assert.deepEqual(extract({ url: 'https://oklahoma.gov/content/sok-wcm/en/oag/news/newsroom/jcr:content/responsivegrid-second/newslisting.json', type: 'api', language: 'en' }, ok).items, [{ title: 'Settlement over app data', link: 'https://oklahoma.gov/oag/news/newsroom/2026/october/settlement.html', date: 'Oct 30, 2026' }]);
  const plone = JSON.stringify({ items: [{ '@id': 'https://www.gov.br/anpd/pt-br/assuntos/noticias/nova-resolucao', title: 'ANPD publica nova resolução', effective: '2026-10-20T10:00:00' }] });
  assert.equal(extract({ url: 'https://www.gov.br/anpd/++api++/pt-br/assuntos/noticias/@search', type: 'api', language: 'pt-BR' }, plone).items[0].link, 'https://www.gov.br/anpd/pt-br/assuntos/noticias/nova-resolucao');
});

test('pages are decoded with the charset they declare', () => {
  const latin1 = Uint8Array.from([...'<html><head><meta charset="iso-8859-1"></head><body>Reforma à Lei</body></html>'].map((c) => c.charCodeAt(0)));
  assert.match(decode(latin1, 'text/html'), /Reforma à Lei/);
  assert.match(decode(latin1, 'text/html; charset=ISO-8859-1'), /Reforma à Lei/);
  assert.match(decode(new TextEncoder().encode('<p>Protección de datos</p>'), 'text/html'), /Protección de datos/);
});

test('a first sighting sets a baseline; the same content is unchanged; a new entry is reported', () => {
  const before = extract(atomItem, atom([OLD_ENTRY]));
  const after = extract(atomItem, atom([NEW_ENTRY, OLD_ENTRY]));
  assert.equal(compare(undefined, before).kind, 'baseline');
  assert.equal(compare({ ...before, checked: '2026-11-02' }, before).kind, 'same');
  const result = compare({ ...before, checked: '2026-11-02' }, after);
  assert.equal(result.kind, 'changed');
  assert.deepEqual(result.newItems.map((i) => i.link), [NEW_ENTRY.link]);
});

test('robots.txt rules are respected: our own group first, longest match wins, Allow wins a tie', () => {
  const robots = 'User-agent: *\nDisallow: /\n\nUser-agent: Bansat-LawWatch\nDisallow: /private/\nAllow: /private/news$\n';
  assert.equal(robotsAllows(robots, 'https://example.gov/news'), true);
  assert.equal(robotsAllows(robots, 'https://example.gov/private/page'), false);
  assert.equal(robotsAllows(robots, 'https://example.gov/private/news'), true);
  assert.equal(robotsAllows('User-agent: *\nDisallow: /search\n', 'https://example.gov/search?q=1'), false);
  assert.equal(robotsAllows('User-agent: *\nDisallow:\n', 'https://example.gov/anything'), true);
  assert.equal(robotsAllows('User-agent: *\nDisallow: /*.pdf$\n', 'https://example.gov/a/b.pdf'), false);
});

test("a site's Crawl-delay is read for our group, or for * when no group names us", () => {
  // legislation.gov.uk's robots.txt asks every robot for a 5-second delay.
  assert.equal(robotsCrawlDelay('User-agent: *\nCrawl-delay: 5\nSitemap: http://www.legislation.gov.uk/sitemap-static.xml\n'), 5);
  assert.equal(robotsCrawlDelay('User-agent: *\nCrawl-delay: 5\n\nUser-agent: Bansat-LawWatch\nCrawl-delay: 2\n'), 2);
  assert.equal(robotsCrawlDelay('User-agent: *\nDisallow: /private\n'), undefined);
});

const source: Source = { key: FEED, watch: atomItem, entries: [{ id: 'uk', name: 'United Kingdom', file: 'laws/full/uk.yaml' }] };

test('the issue text names the jurisdiction, lists the new item and says what happens next', () => {
  const text = changeText('uk', 'United Kingdom', 'laws/full/uk.yaml', [{ source, newItems: [NEW_ENTRY] }], '2026-11-09', 'https://github.com/o/r/actions/runs/1');
  assert.match(text, /\*\*United Kingdom\*\* \(`uk`\) on 2026-11-09/);
  assert.match(text, new RegExp(NEW_ENTRY.link.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
  assert.match(text, /labels this issue `no-change`/);
  assert.match(text, /Run: https:\/\/github.com\/o\/r\/actions\/runs\/1/);
});

test('the failing-sources text lists each source with its entry and since when', () => {
  const text = failureText([{ source, failure: { since: '2026-11-02', error: 'HTTP 403' } }], '2026-11-09');
  assert.match(text, /\| `uk` \| https:\/\/www.legislation.gov.uk\/changes\/affected\/ukpga\/2018\/12\/data.feed \| HTTP 403 \| 2026-11-02 \|/);
});

// End to end, through the CLI, against a copy of the registry with watch items added to the UK entry.
const dir = mkdtempSync(join(tmpdir(), 'bansat-watch-'));
try {
  mkdirSync(join(dir, 'laws/full'), { recursive: true });
  mkdirSync(join(dir, 'watch'), { recursive: true });
  cpSync(join(ROOT, 'laws/schema.json'), join(dir, 'laws/schema.json'));
  cpSync(join(ROOT, 'watch/sources.yaml'), join(dir, 'watch/sources.yaml'));
  const uk = parse(readFileSync(join(ROOT, 'laws/full/uk.yaml'), 'utf8'));
  uk.watch = [atomItem, htmlItem];
  writeFileSync(join(dir, 'laws/full/uk.yaml'), stringify(uk));
  const ie = parse(readFileSync(join(ROOT, 'laws/full/ie.yaml'), 'utf8'));
  ie.watch = [{ url: 'https://www.dataprotection.ie/en/news-media/latest-news', type: 'html', selector: '.view-content', language: 'en' }];
  writeFileSync(join(dir, 'laws/full/ie.yaml'), stringify(ie));

  type Answer = string | { status: number };
  const fixtures = (name: string, files: Record<string, string>, index: Record<string, Answer | Answer[]>): string => {
    const f = join(dir, `fixtures-${name}`);
    mkdirSync(f, { recursive: true });
    for (const [file, body] of Object.entries(files)) writeFileSync(join(f, file), body);
    writeFileSync(join(f, 'index.json'), JSON.stringify(index));
    return f;
  };
  const run = (fixtureDir: string, state: string): { out: string; status: number | null } => {
    const r = spawnSync(process.execPath, [WATCH, '--root', dir, '--fixtures', fixtureDir, '--state', state, '--write-state', state], { encoding: 'utf8' });
    return { out: `${r.stdout}${r.stderr}`, status: r.status };
  };
  const statePath = join(dir, 'state/watch-state.json');
  const ieUrl = 'https://www.dataprotection.ie/en/news-media/latest-news';
  const ieHtml = '<html><body><div class="view-content"><a href="/en/news-media/press-releases/one">One</a></div></body></html>';
  const before = fixtures('before', { 'feed.xml': atom([OLD_ENTRY]), 'page.html': html(['Fine for a retailer']), 'ie.html': ieHtml }, { [FEED]: 'feed.xml', [PAGE]: 'page.html', [ieUrl]: 'ie.html' });
  const after = fixtures('after', { 'feed.xml': atom([NEW_ENTRY, OLD_ENTRY]), 'page.html': html(['Fine for a retailer']), 'ie.html': ieHtml }, { [FEED]: 'feed.xml', [PAGE]: 'page.html', [ieUrl]: 'ie.html' });
  const broken = fixtures('broken', { 'feed.xml': atom([NEW_ENTRY, OLD_ENTRY]), 'page.html': html(['Fine for a retailer']) }, { [FEED]: 'feed.xml', [PAGE]: 'page.html', [ieUrl]: { status: 403 } });
  const blocked = fixtures('blocked', { 'feed.xml': atom([NEW_ENTRY, OLD_ENTRY]), 'page.html': html(['Fine for a retailer']), 'ie.html': ieHtml, 'robots.txt': 'User-agent: *\nDisallow: /en/news-media/\n' }, { [FEED]: 'feed.xml', [PAGE]: 'page.html', [ieUrl]: 'ie.html', 'https://www.dataprotection.ie/robots.txt': 'robots.txt' });

  test('the first run sets a baseline and reports no change', () => {
    const { out, status } = run(before, statePath);
    assert.equal(status, 0, out);
    assert.match(out, /3 sources checked\. 0 jurisdictions have a possible change; 0 sources failed; 0 unchanged; 3 new \(baseline set\)/);
  });

  test('the state file holds hashes, titles and links, never page text (BR-05)', () => {
    const state = readFileSync(statePath, 'utf8');
    assert.ok(!state.includes(SECRET_TEXT), 'page text leaked into the state file');
    assert.ok(state.includes('Fine for a retailer'));
    assert.equal(JSON.parse(state).version, 1);
  });

  test('dry run of a known past change: the Data (Use and Access) Act 2025 shows up for the UK only', () => {
    const { out, status } = run(after, statePath);
    assert.equal(status, 0, out);
    assert.match(out, /1 jurisdiction has a possible change; 0 sources failed; 2 unchanged/);
    assert.match(out, /\*\*United Kingdom\*\* \(`uk`\)/);
    assert.match(out, /Data \(Use and Access\) Act 2025 c\. 18, s\. 70\]\(https:\/\/www\.legislation\.gov\.uk\/ukpga\/2025\/18\/section\/70\)/);
    assert.doesNotMatch(out, /Ireland/);
  });

  test('a failing source is reported, and its last good snapshot is kept', () => {
    const { out, status } = run(broken, statePath);
    assert.equal(status, 0, out);
    assert.match(out, /1 source failed/);
    assert.match(out, /\| `ie` \| https:\/\/www\.dataprotection\.ie\/en\/news-media\/latest-news \| HTTP 403 \(3 tries\) \|/);
    const state = JSON.parse(readFileSync(statePath, 'utf8'));
    assert.ok(state.sources[`${ieUrl} .view-content`], 'the last good snapshot was dropped');
  });

  test('a request refused at random is tried again, and the source counts as loaded', () => {
    const flaky = fixtures('flaky', { 'feed.xml': atom([NEW_ENTRY, OLD_ENTRY]), 'page.html': html(['Fine for a retailer']), 'ie.html': ieHtml }, { [FEED]: [{ status: 503 }, 'feed.xml'], [PAGE]: [{ status: 403 }, { status: 429 }, 'page.html'], [ieUrl]: 'ie.html' });
    const { out, status } = run(flaky, statePath);
    assert.equal(status, 0, out);
    assert.match(out, /0 sources failed; 3 unchanged/);
  });

  test("a missing page isn't tried again", () => {
    const gone = fixtures('gone', { 'feed.xml': atom([NEW_ENTRY, OLD_ENTRY]), 'page.html': html(['Fine for a retailer']), 'ie.html': ieHtml }, { [FEED]: 'feed.xml', [PAGE]: 'page.html', [ieUrl]: [{ status: 404 }, 'ie.html'] });
    const { out } = run(gone, statePath);
    assert.match(out, /\| HTTP 404 \|/);
  });

  test('a page robots.txt disallows is not fetched, and is reported', () => {
    const { out } = run(blocked, statePath);
    assert.match(out, /robots\.txt asks robots not to fetch this page/);
  });

  test('with --issues, a change opens one labelled issue per jurisdiction, and failures go in one issue', () => {
    // A stand-in for the gh CLI that logs each call and answers like an empty repo.
    const bin = join(dir, 'bin');
    mkdirSync(bin, { recursive: true });
    const log = join(dir, 'gh-calls.jsonl');
    writeFileSync(join(bin, 'gh'), `#!${process.execPath}
const fs = require('node:fs');
const args = process.argv.slice(2);
const input = args.includes('--input') ? fs.readFileSync(0, 'utf8') : '';
fs.appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args, input: input ? JSON.parse(input) : null }) + '\\n');
const path = args.find((a) => a.startsWith('repos/'));
const method = args[args.indexOf('-X') + 1];
if (method === 'GET' && /\\/labels\\//.test(path)) process.exit(1);
process.stdout.write(method === 'GET' ? '[]' : '{}');
`, { mode: 0o755 });
    const fresh = join(dir, 'state-issues/watch-state.json');
    run(before, fresh);
    const r = spawnSync(process.execPath, [WATCH, '--root', dir, '--fixtures', broken, '--state', fresh, '--issues'], {
      encoding: 'utf8',
      env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, GITHUB_REPOSITORY: 'o/r', GH_TOKEN: 'test', GITHUB_RUN_ID: '42', GITHUB_SERVER_URL: 'https://github.com' },
    });
    assert.equal(r.status, 0, `${r.stdout}${r.stderr}`);
    const calls = readFileSync(log, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
    const created = calls.filter((c) => c.args.includes('POST') && c.args.includes('repos/o/r/issues'));
    assert.deepEqual(created.map((c) => c.input.title), ['[watch] Possible change: United Kingdom (uk)', '[watch] Source failing']);
    assert.deepEqual(created[0].input.labels, ['law-watch', 'law:uk']);
    assert.match(created[0].input.body, /Run: https:\/\/github\.com\/o\/r\/actions\/runs\/42/);
    assert.deepEqual(created[1].input.labels, ['law-watch-failing']);
    const labels = calls.filter((c) => c.args.includes('POST') && c.args.includes('repos/o/r/labels')).map((c) => c.input.name);
    assert.deepEqual(labels, ['law-watch', 'law:uk', 'law-watch-failing']);
  });

  test('a source watched by two entries is fetched once and reported to both', () => {
    writeFileSync(join(dir, 'laws/full/ie.yaml'), stringify({ ...parse(readFileSync(join(dir, 'laws/full/ie.yaml'), 'utf8')), watch: [atomItem] }));
    const shared = loadSources(dir).filter((s) => s.key === FEED);
    assert.equal(shared.length, 1);
    assert.deepEqual(shared[0].entries.map((e) => e.id), ['ie', 'uk']);
    // A new watcher of a known source starts from its snapshot, so it sees nothing new.
    const { out } = run(after, statePath);
    assert.match(out, /2 sources checked\. 0 jurisdictions have a possible change/);
  });
} finally {
  rmSync(dir, { recursive: true, force: true });
}

console.log(failures.length ? `\n${failures.length} of ${passed + failures.length} tests failed` : `\nAll ${passed} tests passed`);
process.exit(failures.length ? 1 : 0);
```

## File: scripts/watch.ts
size: 12734
sha: 69ee976766a65cca794b0c130935c1b82c27a497
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/watch.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/scripts/watch.ts

## File: assets/banner.png
size: 182504
sha: 3fd5a41a2dad135a7461b44b21ecb412815bb910
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/assets/banner.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/assets/banner.png

## File: assets/social-preview.png
size: 106612
sha: 532d9f522b9c6733aafb5c46d999bb059678beb3
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/assets/social-preview.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/assets/social-preview.png

## File: .gitattributes
size: 194
sha: e6736abedc04756d2a03efbb9a0928b09b70c718
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/.gitattributes

```text
# LF line endings everywhere, so the generated framework file and its --check
# stay identical on every platform (Git on Windows would otherwise check out CRLF).
* text=auto eol=lf
*.png binary
```

## File: .github/workflows/site.yml
size: 2471
sha: 0f9ae5c5984e8405e34ed11d6ba680dd12dcac7c
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/.github/workflows/site.yml

```text
# Builds and tests the documentation site on pull requests that touch it, and publishes it to
# GitHub Pages from main (https://bewithkhan.github.io/bansat/). Only the deploy job can write,
# and only to Pages. Third-party actions are pinned to a full commit SHA (the tag is in the comment).
name: Site

on:
  pull_request:
    paths:
      - 'framework/**'
      - 'laws/**'
      - 'docs/**'
      - 'CHANGELOG.md'
      - 'examples/README.md'
      - 'scripts/modules.ts'
      - 'site/**'
      - '.github/workflows/site.yml'
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: site-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  build:
    name: Build and test the site
    runs-on: ubuntu-latest
    timeout-minutes: 15
    defaults:
      run:
        working-directory: site
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: site/package-lock.json
      - run: npm ci
      - name: Install Chromium (it draws the diagrams and runs the tests)
        run: npx playwright install --with-deps chromium
      - name: Build the pages from the repo
        run: npm run build
      - name: No cookies, no third-party requests, no broken links, no axe violations, search works
        run: npm test
      - name: Performance budgets (NFR-PERF-01), with Lighthouse's mobile profile
        run: npm run perf
      - name: Keep the site for the deploy job
        if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
        uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
        with:
          path: site/dist

  deploy:
    name: Publish to GitHub Pages
    if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    concurrency:
      group: pages
      cancel-in-progress: false
    steps:
      - id: deployment
        uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
```

## File: .github/workflows/validate.yml
size: 2874
sha: cfac35b06cb23706bfba9ac669c9576bf1912c17
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/.github/workflows/validate.yml

```text
# Checks every pull request and every push to main. Read-only: it needs no secrets.
# Third-party actions are pinned to a full commit SHA (the tag is in the comment).
name: Validate

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: validate-${{ github.ref }}
  cancel-in-progress: true

jobs:
  framework:
    name: Framework, skill and hook
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: package-lock.json
      - run: npm ci
      - name: The combined framework file is up to date
        run: node scripts/build-framework.ts --check
      - name: SKILL.md is valid and loads every module
        run: node scripts/check-skill.ts
      - name: The examples match the framework's samples
        run: node scripts/check-examples.ts
      - name: The G1 hook works
        run: node hooks/bansat-g1-guard.test.mjs
      - name: The law registry is valid, with every source on an official domain
        run: node scripts/validate-laws.ts
      - name: The registry validator catches bad entries
        run: node scripts/validate-laws.test.ts
      - name: Law Watch finds changes and reports failures (offline, from fixtures)
        run: node scripts/watch.test.ts

  examples:
    name: Tested examples
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
        ports:
          - 5432:5432
    defaults:
      run:
        working-directory: examples
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: examples/package-lock.json
      - run: npm ci
      - name: The samples type-check
        run: npm run typecheck
      - name: The schema and RLS policies isolate tenants
        run: npm run test:rls
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
      - name: Install Chromium
        run: npx playwright install --with-deps chromium
      - name: Nothing loads before consent
        run: npm run test:privacy
      - name: The privacy test catches a leak
        run: npm run test:privacy:leak
```

## File: .github/workflows/watch.yml
size: 2569
sha: addb4684745ad14146429ec76070eee517411999
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/.github/workflows/watch.yml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/.github/workflows/watch.yml

## File: .gitignore
size: 14
sha: c2658d7d1b31848c3b71960543cb0368e56cd4c7
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/.gitignore

```text
node_modules/
```

## File: CHANGELOG.md
size: 10446
sha: 18544a50d8833965a5b2bfa5a8536e88490a7159
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/CHANGELOG.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/CHANGELOG.md

## File: CITATION.cff
size: 990
sha: a336a3895036b0388385d085fe05e41c15f46360
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/CITATION.cff

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/CITATION.cff

## File: docs/BLUEPRINT.md
size: 63970
sha: 08c085169e9a8f201bad48704a6983463d5e3784
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/docs/BLUEPRINT.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/docs/BLUEPRINT.md

## File: examples/.gitignore
size: 57
sha: 62fc70daa95391e12a4c197bb448557cc031c9eb
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/.gitignore

```text
node_modules/
test-results/
playwright-report/
.pgdata*/
```

## File: examples/lib/privacy/consent.ts
size: 2276
sha: 2cf1a1aef65a1cc73e603835e12f88aa50516dbc
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/lib/privacy/consent.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/lib/privacy/consent.ts

## File: examples/lib/ui/view-state.tsx
size: 3907
sha: 1bef3b7fa300244210eb6942cffbd423d187b8ba
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/lib/ui/view-state.tsx

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/lib/ui/view-state.tsx

## File: examples/package-lock.json
size: 85720
sha: 3c86338d5f9013ba4daacd8440e01d566aec7ae4
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/package-lock.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/package-lock.json

## File: examples/package.json
size: 730
sha: e88b6b649d0f051cdfa1f2b67a96c0a482a32a48
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/package.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/package.json

## File: examples/playwright.config.ts
size: 737
sha: 976c4a75ee3f2be7d22a57e134065f8352446d79
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/playwright.config.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/playwright.config.ts

## File: examples/prisma.config.ts
size: 350
sha: e6fb9251f1c77eb0898f416e309e6b1b3cd4df5e
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/prisma.config.ts

```text
// prisma.config.ts (repo root) — Prisma 7+
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: { path: 'prisma/migrations' },
  // Migrations need a direct (non-pooled) connection; the app uses a driver adapter at runtime.
  datasource: { url: env('DIRECT_URL') },
});
```

## File: examples/prisma/schema.prisma
size: 9314
sha: 0b6677dd803604d76400fa53b2dc2d541691a56d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/prisma/schema.prisma

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/prisma/schema.prisma

## File: examples/supabase/policies/001_rls.sql
size: 3832
sha: 15d4d73d4f10305b8d447e2b33a8a45de6873450
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/supabase/policies/001_rls.sql

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/supabase/policies/001_rls.sql

## File: examples/tests/privacy/consent-loader.spec.ts
size: 1944
sha: 3214cfa3016ea579ec040972d981ee7dfdf2534f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/consent-loader.spec.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/consent-loader.spec.ts

## File: examples/tests/privacy/expect-leak.mjs
size: 1403
sha: 40bf193bd6c156f752bca2b068e857b2ff304fa4
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/expect-leak.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/expect-leak.mjs

## File: examples/tests/privacy/no-third-party-before-consent.spec.ts
size: 1623
sha: 834100b2456049abd2ad4cebc800620090b4bf59
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/no-third-party-before-consent.spec.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/no-third-party-before-consent.spec.ts

## File: examples/tests/privacy/serve-fixtures.mjs
size: 2187
sha: ee63aac1ae4ceb9d7df1c00ec9beb1980bdc01dd
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/serve-fixtures.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/privacy/serve-fixtures.mjs

## File: examples/tests/rls/run.mjs
size: 12114
sha: ede650c333663b4a330949bd155f93495894a7df
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/run.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/run.mjs

## File: examples/tests/rls/tables_without_rls.sql
size: 192
sha: 4b662988e03e2d282aa35da237beacdcd92f3707
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/tables_without_rls.sql

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/tables_without_rls.sql

## File: examples/tests/rls/tenant_isolation.sql
size: 762
sha: fb9917598baf76cef60b4e0022a6945e510c3bc6
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/tenant_isolation.sql

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/rls/tenant_isolation.sql

## File: examples/tests/types/exhaustive-states.ts
size: 1062
sha: a9a1f7a612136e4225fc7a1dbf36e0838e648319
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/types/exhaustive-states.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tests/types/exhaustive-states.ts

## File: examples/tsconfig.json
size: 403
sha: dcb8f51024cb949530106547c2509909c9cb7e24
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tsconfig.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/tsconfig.json

## File: examples/types/framework-stubs.d.ts
size: 1073
sha: 8d5c81bbce92eb88dc61f07cbff62fd78edbb251
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/examples/types/framework-stubs.d.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/examples/types/framework-stubs.d.ts

## File: framework/00-intake.md
size: 4444
sha: 63b2467f186b7f823f1b4355660b3d2884b7e4a5
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/00-intake.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/00-intake.md

## File: framework/01-prd.md
size: 5509
sha: 51e4a6351288bdc8e77b0a2b5015c1cb093aee43
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/01-prd.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/01-prd.md

## File: framework/02-nfr.md
size: 2366
sha: d7f2fb7c66039dd76901d57df28b15f2008ff3d0
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/02-nfr.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/02-nfr.md

## File: framework/03-trd.md
size: 7890
sha: d82832202c93c940ba974845b04b7abc6c8c00bf
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/03-trd.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/03-trd.md

## File: framework/04-adrs.md
size: 3825
sha: 0f5ffea3e67670dbb6d1f274b6a10f0f80a99d67
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/04-adrs.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/04-adrs.md

## File: framework/05-data.md
size: 22712
sha: 9c83793045a86b3f2250455fc99b0f50478979a2
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/05-data.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/05-data.md

## File: framework/06-api.md
size: 3869
sha: 76c7da30020dfb1344439590f156f0b31eab549d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/06-api.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/06-api.md

## File: framework/07-security.md
size: 15381
sha: e96d2200a4fb21a856e13dec98b408ab1b105775
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/07-security.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/07-security.md

## File: framework/08-ai.md
size: 5474
sha: c0a49ed503ac38d776d01cb4c01770cb3c8def67
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/08-ai.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/08-ai.md

## File: framework/09-privacy.md
size: 60942
sha: fbf17ff997b101367ee0dd97fdc6d5689f7ecda8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/09-privacy.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/09-privacy.md

## File: framework/10-experience.md
size: 22869
sha: 9ca93733f0514b33e7356716c47649a3291ceaac
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/10-experience.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/10-experience.md

## File: framework/11-delivery.md
size: 17796
sha: 1b7af1a05da227263d26a1e93460f739c73a36b2
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/11-delivery.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/11-delivery.md

## File: framework/12-agent-manual.md
size: 11974
sha: 46432942bb2e052a1f74c4e09a23c91672c0fe33
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/12-agent-manual.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/12-agent-manual.md

## File: framework/13-launch-gate.md
size: 4409
sha: 2214a2fd23337aa891b4d2b94615f5d981c36071
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/13-launch-gate.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/13-launch-gate.md

## File: framework/14-retrofit.md
size: 10236
sha: e30019ca2498c8485f24a71aa6eeae29d48b6e41
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/14-retrofit.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/14-retrofit.md

## File: framework/appendix-a-templates.md
size: 2978
sha: 6a34b5a93071afd2d63bba345b8b21accf9d66e8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-a-templates.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-a-templates.md

## File: framework/appendix-b-references.md
size: 5124
sha: 00a7256d5a02365cb4ea55f4ac20490e4a8a2cbe
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-b-references.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-b-references.md

## File: framework/appendix-c-glossary.md
size: 2717
sha: fa9027811d8c7cce3ecd5a8a43008f12a77bf319
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-c-glossary.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-c-glossary.md

## File: framework/appendix-d-stacks.md
size: 8853
sha: 018496d05bed4f60a7fb840419986f4dcca4db77
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-d-stacks.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-d-stacks.md

## File: framework/appendix-e-methods.md
size: 3667
sha: c162741b1b3448078e7638c7a001c9b836b7e9e5
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e-methods.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e-methods.md

## File: framework/appendix-e1-scrum.md
size: 3558
sha: b05e633b48b160e93576df3d43c4c9245f7ccc33
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e1-scrum.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e1-scrum.md

## File: framework/appendix-e2-kanban.md
size: 2718
sha: ea891a5bf153d3ef614cb041fd66782a42c911df
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e2-kanban.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e2-kanban.md

## File: framework/appendix-e3-stage-gates.md
size: 3220
sha: 1be515e910e21e399293a7e9e1767c9cc4b39771
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e3-stage-gates.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e3-stage-gates.md

## File: framework/appendix-e4-shape-up.md
size: 2665
sha: 65e3fa357ce8c4d6a7e6ee36dc7f19b968868189
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e4-shape-up.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e4-shape-up.md

## File: framework/appendix-e5-solo.md
size: 2108
sha: 67db8cc80b727547592ba9a741466b13ecfac48f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e5-solo.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e5-solo.md

## File: framework/appendix-e6-scaled.md
size: 3385
sha: f09cf8fcf6fc24f4408ef4007d45e1a22b3655e8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e6-scaled.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-e6-scaled.md

## File: framework/appendix-f-casebook.md
size: 65453
sha: 58f05221bfabd3bf88af49ef6553574d4aa47ad2
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-f-casebook.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/appendix-f-casebook.md

## File: framework/cover.md
size: 11439
sha: 184b725415b98a92339821f301bb14b165db2d6a
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/cover.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/cover.md

## File: framework/how-to-use.md
size: 4769
sha: e4e02c5cf73e3d352341add7f6f28ff8e5828e02
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/framework/how-to-use.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/framework/how-to-use.md

## File: hooks/bansat-g1-guard.mjs
size: 3998
sha: 887dc4b6b744820e72f260b18a461ba59082f733
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/hooks/bansat-g1-guard.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/hooks/bansat-g1-guard.mjs

## File: hooks/bansat-g1-guard.test.mjs
size: 5021
sha: 220ecdab0ea83fac5feecf7f043cdc917e5d1952
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/hooks/bansat-g1-guard.test.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/hooks/bansat-g1-guard.test.mjs

## File: laws/full/br.yaml
size: 15968
sha: 54afbd1db152c21d464d84c558804127c52e58a1
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/br.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/br.yaml

## File: laws/full/ca-qc.yaml
size: 9488
sha: e29ff5d8e84b70f9999c886bc0442832e15eda5c
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ca-qc.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ca-qc.yaml

## File: laws/full/ca.yaml
size: 13245
sha: 7e60836375b20fa44b4c62879c234affeb047817
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ca.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ca.yaml

## File: laws/full/ch.yaml
size: 20229
sha: f098b0f6f11240e707287eef2f939b151d8b1c05
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ch.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ch.yaml

## File: laws/full/de.yaml
size: 13959
sha: 2bf587c040231c3e2057282b4b67a6f47f344fe4
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/de.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/de.yaml

## File: laws/full/es.yaml
size: 13629
sha: 662df59e72a8081ef010e6156927fe268061f93d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/es.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/es.yaml

## File: laws/full/eu.yaml
size: 20184
sha: 3acbfe8a878e9d6a4b9be4a05ad3d61c41d04217
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/eu.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/eu.yaml

## File: laws/full/fr.yaml
size: 12696
sha: bc387ec59c3ca21c9261af7c7270aa0248ba15c8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/fr.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/fr.yaml

## File: laws/full/ie.yaml
size: 13327
sha: a03c7ce7b711c7aa3313fb3331fa77e9284ab1c7
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ie.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/ie.yaml

## File: laws/full/it.yaml
size: 12447
sha: 442939b2e25fd460bfc922554d3f525eb62fc78f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/it.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/it.yaml

## File: laws/full/mx.yaml
size: 10202
sha: e1fa932f747acba86c45f65e2d5325b7daabd8c8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/mx.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/mx.yaml

## File: laws/full/nl.yaml
size: 12440
sha: 678113568af3185b751769b27c018c22b89e7c82
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/nl.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/nl.yaml

## File: laws/full/no.yaml
size: 12739
sha: d6cd5cc4a42d5faf0c88fe8a9b0c7d7bad047373
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/no.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/no.yaml

## File: laws/full/tr.yaml
size: 14929
sha: bcbe1c90edc1ae5748b5d06387bd815ebf06a185
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/tr.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/tr.yaml

## File: laws/full/uk.yaml
size: 8990
sha: 1a1b6bb365db4950f810accbaa5e882cb933c893
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/uk.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/uk.yaml

## File: laws/full/us-al.yaml
size: 8918
sha: 0a3a2400f5ec14517dcd9520f1dd1f4f2b02ec94
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-al.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-al.yaml

## File: laws/full/us-ca.yaml
size: 18672
sha: 06a90fea56b45046b0783234443b189669993e54
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ca.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ca.yaml

## File: laws/full/us-co.yaml
size: 11011
sha: 9da707a98a2f684e19815960810f6fe8be1bdaa8
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-co.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-co.yaml

## File: laws/full/us-ct.yaml
size: 13517
sha: b46b198b8701511ff8771b2c353fddb1896e5c22
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ct.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ct.yaml

## File: laws/full/us-de.yaml
size: 8736
sha: 415e920aa88a444a122c660a980c123ca8637d06
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-de.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-de.yaml

## File: laws/full/us-federal.yaml
size: 20821
sha: ad43a81c9bb808aa6f0c1dfde40330242eacf6f0
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-federal.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-federal.yaml

## File: laws/full/us-ia.yaml
size: 7998
sha: f143958ad7d9b4172ab12255c436af6d351b7d77
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ia.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ia.yaml

## File: laws/full/us-in.yaml
size: 10680
sha: 3108ceca63bf6d30343b32b9a9472c019cb76357
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-in.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-in.yaml

## File: laws/full/us-ky.yaml
size: 7405
sha: 81c2db110955cc7a0260cb01dfac9c15ee000092
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ky.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ky.yaml

## File: laws/full/us-la.yaml
size: 9682
sha: 0757e07a19da9d01ace50b8c2a015cb9e2a586fa
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-la.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-la.yaml

## File: laws/full/us-md.yaml
size: 11617
sha: c9c8c1cb044f39ccd313caf119f981660aa2629b
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-md.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-md.yaml

## File: laws/full/us-mn.yaml
size: 8818
sha: 97311a5901db903847da595b277146f85b050585
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-mn.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-mn.yaml

## File: laws/full/us-mt.yaml
size: 10161
sha: 4002bc6571c2ca6746098381ec5b450b8ec27317
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-mt.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-mt.yaml

## File: laws/full/us-ne.yaml
size: 12079
sha: e09166d7ea713aa242487ce789df015cb7e139be
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ne.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ne.yaml

## File: laws/full/us-nh.yaml
size: 8134
sha: 4eed44542e686f530a1ecdd38550d9f4676d8eba
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-nh.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-nh.yaml

## File: laws/full/us-nj.yaml
size: 10497
sha: a76f8a87c7dad83209652e145dce9f0b702c522e
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-nj.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-nj.yaml

## File: laws/full/us-ok.yaml
size: 8348
sha: f604049a2e8e45ac5a4c889ebf7ffad49d369556
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ok.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ok.yaml

## File: laws/full/us-or.yaml
size: 9236
sha: bf476a3e51797afc11a944257608cc628a56d224
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-or.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-or.yaml

## File: laws/full/us-ri.yaml
size: 9034
sha: d41b0ae281d597aee36dda364d7ff048c03b7f3c
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ri.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ri.yaml

## File: laws/full/us-tn.yaml
size: 9959
sha: 92a3111a77be0cd896541ae7d31ab7f65ed6a31f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-tn.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-tn.yaml

## File: laws/full/us-tx.yaml
size: 11458
sha: 12b049751457ab70fe28c1db5a6386a8601df0f3
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-tx.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-tx.yaml

## File: laws/full/us-ut.yaml
size: 13198
sha: 2307e941e3e7225121e553c8d6d5e66a6e841162
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ut.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-ut.yaml

## File: laws/full/us-va.yaml
size: 9416
sha: b4dc8e992f76647bddbb004639c85b44dee4c174
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-va.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-va.yaml

## File: laws/full/us-vt.yaml
size: 9823
sha: 358c9d6582289076cbe7d0843e7748cab60eee3d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-vt.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/full/us-vt.yaml

## File: laws/schema.json
size: 9825
sha: 3dfbe2efa397fd1cbdc4900857a3cb497cc1c591
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/laws/schema.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/laws/schema.json

## File: LICENSE
size: 18658
sha: 10fabd90118f7ce38bb2e4753e105e744442a429
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/LICENSE

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/LICENSE

## File: LICENSE-CODE
size: 1077
sha: 1564d55a76b2de8a0b845f83323dabb797b246c9
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/LICENSE-CODE

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/LICENSE-CODE

## File: NOTICE
size: 720
sha: 8269e597f7a966e21d4abb87f90f0538ad43eb49
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/NOTICE

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/NOTICE

## File: package-lock.json
size: 14996
sha: ff2df0be1c223f6484d8ca92a3563485ad4b0957
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/package-lock.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/package-lock.json

## File: package.json
size: 744
sha: 8d28c10976dd0673ee332c17c8f5159f5ebee7b3
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/package.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/package.json

## File: reference/bansat-framework.md
size: 322725
sha: 829115521b93f145610c6a3dd55d5921e7c3d726
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/reference/bansat-framework.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/reference/bansat-framework.md

## File: site/.gitignore
size: 270
sha: 184969e64e1d693fba4819f2003f31af4976f5e6
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/.gitignore

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/.gitignore

## File: site/astro.config.mjs
size: 1829
sha: dfb71d2d6ef885b6bd5b5e72990b4c937bcaa820
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/astro.config.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/astro.config.mjs

## File: site/ec.config.mjs
size: 1029
sha: 8285055b568ab795c18ff0fa89a73cf17f5b6604
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/ec.config.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/ec.config.mjs

## File: site/package-lock.json
size: 293438
sha: 0ebcc3aa0570a0098151fb369b5f77eee7d01b1c
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/package-lock.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/package-lock.json

## File: site/package.json
size: 816
sha: de3dc6929f75d0bef7de86011387f4bfced3343f
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/package.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/package.json

## File: site/playwright.config.ts
size: 665
sha: c4ef562d16d2700eba915127a2180b9804894793
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/playwright.config.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/playwright.config.ts

## File: site/public/bansat-fresh.js
size: 1031
sha: 66b890d90818700510f53e7f1023479e0dc41bd3
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/public/bansat-fresh.js

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/public/bansat-fresh.js

## File: site/scripts/laws.mjs
size: 15240
sha: 08b204fec18bb676bdfb8d22c2755edf120c64fb
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/laws.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/laws.mjs

## File: site/scripts/lighthouse.mjs
size: 3326
sha: 2dd3399dcf07b9a30bbf5de6ccf68b6d36f5f7b1
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/lighthouse.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/lighthouse.mjs

## File: site/scripts/sync-content.mjs
size: 10376
sha: b46e462ca319374c20328c2bf6fa1805fab9649d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/sync-content.mjs

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/scripts/sync-content.mjs

## File: site/src/components/LawIndex.astro
size: 5047
sha: d9ceeb7d90c2935e175c5344168acc98cc8fdb54
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/components/LawIndex.astro

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/components/LawIndex.astro

## File: site/src/content.config.ts
size: 441
sha: a05c637de455187879940098bc9039f482da4d36
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content.config.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content.config.ts

## File: site/src/content/docs/404.md
size: 680
sha: 8931f648833042bf6c91727b2b13a6e0f17de86b
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/docs/404.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/docs/404.md

## File: site/src/content/docs/index.mdx
size: 3327
sha: cb74bb9019b2507632e1861e61ee090b75fcf418
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/docs/index.mdx

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/docs/index.mdx

## File: site/src/content/i18n/en.json
size: 168
sha: 36adc041d3cbbd740c4b9aaad900a9dbd3b1c841
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/i18n/en.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/content/i18n/en.json

## File: site/src/styles/bansat.css
size: 4299
sha: 7888054240be3e5560fa545143bb1c7222838acc
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/styles/bansat.css

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/src/styles/bansat.css

## File: site/tests/site.spec.ts
size: 7525
sha: 3295a407bb6ff97452d0864d285c59fcbe6ad67d
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/site/tests/site.spec.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/site/tests/site.spec.ts

## File: watch/sources.yaml
size: 3971
sha: bac47dbc63f82332542c9bf483eb0261618ec7cb
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/watch/sources.yaml

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/watch/sources.yaml

## File: watch/verify-prompt.md
size: 3433
sha: 412e693db131855a35ffc96b45f7f3b4cc1ed8fd
upstream: https://github.com/bewithkhan/bansat/blob/HEAD/watch/verify-prompt.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/bewithkhan/bansat/blob/HEAD/watch/verify-prompt.md
