diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index d2abd77..588d027 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -13,7 +13,7 @@ jobs:
with:
python-version: "3.11"
- name: Install CivicCore release wheel
- run: python -m pip install https://github.com/CivicSuite/civiccore/releases/download/v1.1.0/civiccore-1.1.0-py3-none-any.whl
+ run: python -m pip install https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl
- name: Install package
run: python -m pip install -e ".[dev]"
- name: Run release gate
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a76c79d..3fbabbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,22 @@ The format follows Keep a Changelog, and this project follows Semantic Versionin
## [Unreleased]
+## [0.3.0] - 2026-06-25
+
+### Added
+
+- Added default local SQLite review persistence so installed CivicAccess is ready without hidden environment setup.
+- Added `/civicaccess/staff` for saved review queue, readiness, integration contracts, and records-ready export operations.
+- Added `GET /api/v1/civicaccess/reviews`, `POST /api/v1/civicaccess/reviews/{review_id}/records-export`, and `GET /api/v1/civicaccess/integration-contracts`.
+
+### Changed
+
+- Aligned CivicAccess to the published CivicCore v1.2.0 release wheel and SHA256.
+- Wired the public `/civicaccess` review form to the accessibility review API.
+- Added local schema status and `/ready` readiness gates for review-record persistence.
+- Added bounded request models and actionable validation errors for public API payloads.
+- Changed readiness from environment-gated optional persistence to default local persistence with explicit override support.
+
## [0.2.0] - 2026-05-21
### Corrected
diff --git a/README.md b/README.md
index fbb78f4..f898d1f 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,15 @@
CivicAccess is the CivicSuite module for accessibility, plain-language, multilingual, and ADA Title II review-support workflows.
-Current state: **v0.2.0 corrective demotion state**. This repo contains a deterministic scaffold with a FastAPI package, health/root endpoints, WCAG-aligned review support, optional database-backed review records via `CIVICACCESS_REVIEW_DB_URL`, accessible form planning, accessible publishing workflow checks, plain-language rewrites, multilingual draft variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, a public UI at `/civicaccess`, and `civiccore==1.1.0` dependency alignment. The previous `v1.0.0` release was published in error and is superseded by this honest sub-1.0.0 label.
+Current state: **v0.3.0 standalone readiness candidate**. This repo contains a FastAPI package aligned to the published CivicCore v1.2.0 release wheel, health/root endpoints, readiness gates, WCAG-aligned review support, local database-backed review records, accessible form planning, accessible publishing workflow checks, plain-language rewrites, multilingual draft variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, an API-backed public review UI at `/civicaccess`, and a staff review/export workspace at `/civicaccess/staff`. The previous `v1.0.0` release was published in error and remains historical evidence only.
CivicAccess does **not** provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval. City staff, ADA coordinators, translators, and qualified reviewers remain responsible for publication decisions.
## What CivicAccess Does
- Reviews public content for actionable WCAG-aligned issues.
+- Reports local review-record persistence readiness through `/ready`.
+- Provides a staff queue for saved accessibility reviews and records-ready exports.
- Checks accessible form publication basics: labels, required fields, validation copy, and record context.
- Builds a staff publication workflow with accessibility, plain-language, translation-review, export, and approval steps.
- Rewrites common municipal jargon into plainer language while preserving source/rewrite provenance.
@@ -16,17 +18,18 @@ CivicAccess does **not** provide legal advice, certified ADA compliance, officia
- Produces ADA Title II review-support checklists without claiming certification.
- Checks tagged-PDF heading expectations before publication.
- Builds records-ready export checklists that preserve source/rewrite provenance.
-- Provides a local accessibility support UI at `/civicaccess`.
+- Provides a local API-backed accessibility review UI at `/civicaccess`.
## Release Integrity Correction
-CivicAccess is not finished, shipping, city-ready, product-ready, or public-use ready. It has no real AI layer, no real municipal data/search layer, no production-grade frontend, and no independent Section 2 public-use gate sign-off. See [docs/release-integrity-correction-2026-05-21.md](docs/release-integrity-correction-2026-05-21.md).
+CivicAccess was previously demoted after a false `v1.0.0` release. The current branch is rebuilding that truthfully: it now has automatic local persistence, a staff workspace, and integration contracts, but it still must pass the current module-completion audit, suite integration proof, and clean-machine evidence before any finished/public-use label can be promoted. See [docs/release-integrity-correction-2026-05-21.md](docs/release-integrity-correction-2026-05-21.md).
## Developer Quickstart
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
+python -m pip install https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl
python -m pip install -e ".[dev]"
python -m pytest -q
bash scripts/verify-release.sh
@@ -36,9 +39,14 @@ bash scripts/verify-release.sh
- `GET /` returns current module status and boundaries.
- `GET /health` returns package and CivicCore version information.
-- `GET /civicaccess` returns the public UI.
+- `GET /ready` and `GET /api/v1/civicaccess/readiness` report whether local review persistence is configured and schema-ready.
+- `GET /civicaccess` returns the API-backed public accessibility review UI.
+- `GET /civicaccess/staff` returns the staff review queue and records-export workspace.
- `POST /api/v1/civicaccess/review` returns accessibility findings and next steps.
-- `GET /api/v1/civicaccess/reviews/{review_id}` retrieves persisted review records when `CIVICACCESS_REVIEW_DB_URL` is configured.
+- `GET /api/v1/civicaccess/reviews` lists saved review records.
+- `GET /api/v1/civicaccess/reviews/{review_id}` retrieves persisted review records.
+- `POST /api/v1/civicaccess/reviews/{review_id}/records-export` builds a CivicRecords-ready retention export.
+- `GET /api/v1/civicaccess/integration-contracts` publishes upstream/downstream integration contracts.
- `POST /api/v1/civicaccess/forms` returns accessible form publication checks.
- `POST /api/v1/civicaccess/publishing-workflow` returns staff publication workflow blockers and steps.
- `POST /api/v1/civicaccess/plain-language` returns a deterministic plain-language rewrite.
@@ -47,7 +55,9 @@ bash scripts/verify-release.sh
- `POST /api/v1/civicaccess/tagged-pdf` returns tagged-PDF heading expectations.
- `POST /api/v1/civicaccess/export` returns a records-ready accessibility export checklist.
-Set `CIVICACCESS_REVIEW_DB_URL` to enable persistent accessibility review records. When unset, CivicAccess continues to use deterministic in-memory review behavior for local examples.
+By default, CivicAccess stores review records in `data/civicaccess-reviews.db` under the process working directory. Set `CIVICACCESS_DATA_DIR` to choose a different local data directory, or set `CIVICACCESS_REVIEW_DB_URL` to use an explicit SQLAlchemy database URL. `/ready` is expected to be ready when the local schema can be created and verified.
+
+Use the `civicaccess-db-status` console script with an explicit SQLAlchemy URL when an operator wants to preflight a non-default review database.
## License
diff --git a/README.txt b/README.txt
index f77bf12..38fec7c 100644
--- a/README.txt
+++ b/README.txt
@@ -3,7 +3,7 @@ CivicAccess
CivicAccess is the CivicSuite module for accessibility, plain-language, multilingual, and ADA Title II review-support workflows.
-Current state: v0.2.0 corrective demotion state. This repo contains a deterministic scaffold with a FastAPI package, health/root endpoints, WCAG-aligned review support, optional database-backed review records via CIVICACCESS_REVIEW_DB_URL, accessible form planning, accessible publishing workflow checks, plain-language rewrites, multilingual draft variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, a public UI at /civicaccess, and civiccore==1.1.0 dependency alignment. The previous v1.0.0 release was published in error and is superseded by this honest sub-1.0.0 label.
+Current state: v0.3.0 corrective demotion state. This repo contains a deterministic scaffold with a FastAPI package aligned to the published CivicCore v1.2.0 release wheel, health/root endpoints, readiness gates, WCAG-aligned review support, optional database-backed review records via CIVICACCESS_REVIEW_DB_URL, accessible form planning, accessible publishing workflow checks, plain-language rewrites, multilingual draft variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, and an API-backed public review UI at /civicaccess. The previous v1.0.0 release was published in error and is superseded by this honest sub-1.0.0 label.
CivicAccess does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval. City staff, ADA coordinators, translators, and qualified reviewers remain responsible for publication decisions.
diff --git a/SECURITY.md b/SECURITY.md
index 49141b2..9ee7fd5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,6 +1,6 @@
# Security
-CivicAccess version: `0.2.0`.
+CivicAccess version: `0.3.0`.
CivicAccess is self-hosted municipal software. It provides advisory accessibility, plain-language, multilingual draft, and ADA Title II review-support workflows; it does not make legal, certification, translation, or publication decisions.
diff --git a/USER-MANUAL.md b/USER-MANUAL.md
index 701486a..a2d6826 100644
--- a/USER-MANUAL.md
+++ b/USER-MANUAL.md
@@ -4,17 +4,23 @@
CivicAccess helps cities make public information easier to read, reach, translate, review, and preserve. It supports accessibility review, accessible forms, public publishing workflows, plain-language rewrites, multilingual draft variants, ADA Title II review support, tagged-PDF expectations, and records-ready export checklists.
-Current state: `0.2.0` corrective demotion state. CivicAccess includes deterministic checks, optional database-backed review records, a public UI at `/civicaccess`, and `civiccore==1.1.0` dependency alignment. The previous `v1.0.0` release was published in error and is superseded by this honest sub-1.0.0 label. CivicAccess does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval.
+Current state: `0.3.0` standalone readiness candidate. CivicAccess includes deterministic checks, local database-backed review records, readiness gates, an API-backed public review UI at `/civicaccess`, a staff review/export workspace at `/civicaccess/staff`, and CivicCore v1.2.0 release-wheel alignment. The previous `v1.0.0` release was published in error and remains historical evidence only. CivicAccess does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval.
## For IT And Technical Staff
-CivicAccess is a FastAPI Python package pinned to `civiccore==1.1.0`. The current runtime exposes:
+CivicAccess is a FastAPI Python package pinned to the published `CivicCore v1.2.0` release wheel. The current runtime exposes:
- `GET /`
- `GET /health`
+- `GET /ready`
- `GET /civicaccess`
+- `GET /civicaccess/staff`
+- `GET /api/v1/civicaccess/readiness`
- `POST /api/v1/civicaccess/review`
-- `GET /api/v1/civicaccess/reviews/{review_id}` when `CIVICACCESS_REVIEW_DB_URL` is configured
+- `GET /api/v1/civicaccess/reviews`
+- `GET /api/v1/civicaccess/reviews/{review_id}`
+- `POST /api/v1/civicaccess/reviews/{review_id}/records-export`
+- `GET /api/v1/civicaccess/integration-contracts`
- `POST /api/v1/civicaccess/forms`
- `POST /api/v1/civicaccess/publishing-workflow`
- `POST /api/v1/civicaccess/plain-language`
@@ -23,11 +29,14 @@ CivicAccess is a FastAPI Python package pinned to `civiccore==1.1.0`. The curren
- `POST /api/v1/civicaccess/tagged-pdf`
- `POST /api/v1/civicaccess/export`
-Set `CIVICACCESS_REVIEW_DB_URL` to persist review requests, findings, WCAG references, disclaimers, and next steps. Leave it unset for deterministic sample behavior.
+By default, CivicAccess persists review requests, findings, WCAG references, disclaimers, and next steps in `data/civicaccess-reviews.db` under the process working directory. Set `CIVICACCESS_DATA_DIR` to choose a different local data directory, or set `CIVICACCESS_REVIEW_DB_URL` for an explicit SQLAlchemy database URL. Use `civicaccess-db-status` with the same explicit database URL when preflighting a non-default database.
+
+Before public use, check `/ready` or `/api/v1/civicaccess/readiness`. The readiness gate is `ready` when the local review database schema can be created and verified.
Run local verification with:
```powershell
+python -m pip install https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl
python -m pip install -e ".[dev]"
python -m pytest -q
bash scripts/verify-release.sh
@@ -38,9 +47,11 @@ bash scripts/verify-release.sh
```mermaid
flowchart LR
PublicUser["Resident or staff publisher"] --> CivicAccess["CivicAccess"]
- CivicAccess --> CivicCore["CivicCore v1.1.0"]
+ CivicAccess --> CivicCore["CivicCore v1.2.0"]
+ CivicAccess --> CivicRecords["CivicRecords AI records export"]
CivicClerk["CivicClerk"] -. uses accessibility support .-> CivicAccess
CivicCode["CivicCode"] -. uses accessibility support .-> CivicAccess
+ CivicAccess -. downstream contracts .-> LandUse["Zone / Plan / Permit / Inspect / Grants / Procure"]
```
CivicAccess depends on CivicCore. CivicCore does not depend on CivicAccess.
diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt
index 3b2f3a3..365fb62 100644
--- a/USER-MANUAL.txt
+++ b/USER-MANUAL.txt
@@ -3,13 +3,15 @@ CivicAccess User Manual
CivicAccess helps cities make public information easier to read, reach, translate, review, and preserve. It supports accessibility review, accessible forms, public publishing workflows, plain-language rewrites, multilingual draft variants, ADA Title II review support, tagged-PDF expectations, and records-ready export checklists.
-Current state: 0.2.0 corrective demotion state. CivicAccess includes deterministic checks, optional database-backed review records, a public UI at /civicaccess, and civiccore==1.1.0 dependency alignment. The previous v1.0.0 release was published in error and is superseded by this honest sub-1.0.0 label. It does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval.
+Current state: 0.3.0 corrective demotion state. CivicAccess includes deterministic checks, optional database-backed review records, readiness gates, an API-backed public review UI at /civicaccess, and CivicCore v1.2.0 release-wheel alignment. The previous v1.0.0 release was published in error and is superseded by this honest sub-1.0.0 label. It does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval.
Runtime routes:
- GET /
- GET /health
+- GET /ready
- GET /civicaccess
+- GET /api/v1/civicaccess/readiness
- POST /api/v1/civicaccess/review
- GET /api/v1/civicaccess/reviews/{review_id} when CIVICACCESS_REVIEW_DB_URL is configured
- POST /api/v1/civicaccess/forms
@@ -20,4 +22,6 @@ Runtime routes:
- POST /api/v1/civicaccess/tagged-pdf
- POST /api/v1/civicaccess/export
-Set CIVICACCESS_REVIEW_DB_URL to persist review requests, findings, WCAG references, disclaimers, and next steps. Leave it unset for deterministic sample behavior.
+Set CIVICACCESS_REVIEW_DB_URL to persist review requests, findings, WCAG references, disclaimers, and next steps. Use civicaccess-db-status with the same database URL to initialize and verify schema. Leave persistence unset for deterministic sample behavior.
+
+Before public use, check /ready or /api/v1/civicaccess/readiness. The readiness gate is not-ready until local review persistence is configured and schema-ready.
diff --git a/civicaccess/__init__.py b/civicaccess/__init__.py
index 4b32bb0..3f3c522 100644
--- a/civicaccess/__init__.py
+++ b/civicaccess/__init__.py
@@ -1,3 +1,3 @@
"""civicaccess package."""
-__version__ = "0.2.0"
+__version__ = "0.3.0"
diff --git a/civicaccess/access_review.py b/civicaccess/access_review.py
index 0593309..c30ce3a 100644
--- a/civicaccess/access_review.py
+++ b/civicaccess/access_review.py
@@ -49,6 +49,8 @@ class StoredAccessibilityReview:
metadata = sa.MetaData()
+SCHEMA_VERSION = "2026-06-05-001"
+
accessibility_review_records = sa.Table(
"accessibility_review_records",
metadata,
@@ -64,6 +66,23 @@ class StoredAccessibilityReview:
schema="civicaccess",
)
+schema_migrations = sa.Table(
+ "schema_migrations",
+ metadata,
+ sa.Column("schema_version", sa.String(40), primary_key=True),
+ sa.Column("applied_at", sa.DateTime(timezone=True), nullable=False),
+ schema="civicaccess",
+)
+
+
+@dataclass(frozen=True)
+class SchemaStatus:
+ schema_version: str | None
+ expected_schema_version: str
+ ready: bool
+ missing_tables: tuple[str, ...]
+ dialect: str
+
class AccessibilityReviewRepository:
"""SQLAlchemy-backed accessibility review records for local publication workflows."""
@@ -76,7 +95,52 @@ def __init__(self, *, db_url: str | None = None, engine: Engine | None = None) -
self.engine = base_engine
with self.engine.begin() as connection:
connection.execute(sa.text("CREATE SCHEMA IF NOT EXISTS civicaccess"))
+ self.migrate()
+
+ def migrate(self) -> SchemaStatus:
+ """Apply non-destructive local schema setup and return the resulting status."""
+
metadata.create_all(self.engine)
+ with self.engine.begin() as connection:
+ exists = connection.execute(
+ sa.select(schema_migrations.c.schema_version).where(
+ schema_migrations.c.schema_version == SCHEMA_VERSION
+ )
+ ).first()
+ if exists is None:
+ connection.execute(
+ schema_migrations.insert().values(
+ schema_version=SCHEMA_VERSION,
+ applied_at=datetime.now(UTC),
+ )
+ )
+ return self.schema_status()
+
+ def schema_status(self) -> SchemaStatus:
+ inspector = sa.inspect(self.engine)
+ translated_schema = None if self.engine.dialect.name == "sqlite" else "civicaccess"
+ available_tables = set(inspector.get_table_names(schema=translated_schema))
+ expected_tables = {"accessibility_review_records", "schema_migrations"}
+ missing_tables = tuple(sorted(expected_tables - available_tables))
+ schema_version = None
+ if "schema_migrations" not in missing_tables:
+ with self.engine.begin() as connection:
+ schema_version = connection.execute(
+ sa.select(schema_migrations.c.schema_version)
+ .order_by(schema_migrations.c.applied_at.desc())
+ .limit(1)
+ ).scalar_one_or_none()
+ return SchemaStatus(
+ schema_version=schema_version,
+ expected_schema_version=SCHEMA_VERSION,
+ ready=schema_version == SCHEMA_VERSION and not missing_tables,
+ missing_tables=missing_tables,
+ dialect=self.engine.dialect.name,
+ )
+
+ def review_count(self) -> int:
+ with self.engine.begin() as connection:
+ return connection.execute(sa.select(sa.func.count()).select_from(accessibility_review_records)).scalar_one()
def create_review(
self, *, title: str, body: str, has_alt_text: bool, language: str
@@ -125,6 +189,16 @@ def get_review(self, review_id: str) -> StoredAccessibilityReview | None:
return None
return _row_to_stored_review(row)
+ def list_reviews(self, *, limit: int = 25) -> tuple[StoredAccessibilityReview, ...]:
+ bounded_limit = max(1, min(limit, 100))
+ with self.engine.begin() as connection:
+ rows = connection.execute(
+ sa.select(accessibility_review_records)
+ .order_by(accessibility_review_records.c.created_at.desc())
+ .limit(bounded_limit)
+ ).mappings().all()
+ return tuple(_row_to_stored_review(row) for row in rows)
+
def review_accessibility(*, title: str, body: str, has_alt_text: bool, language: str) -> AccessibilityReview:
"""Return deterministic sample accessibility findings without live LLM calls."""
diff --git a/civicaccess/db_admin.py b/civicaccess/db_admin.py
new file mode 100644
index 0000000..41f048c
--- /dev/null
+++ b/civicaccess/db_admin.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import argparse
+
+from civicaccess.access_review import AccessibilityReviewRepository
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description="Check and initialize the local CivicAccess review database schema."
+ )
+ parser.add_argument(
+ "--db-url",
+ required=True,
+ help="SQLAlchemy database URL used by CIVICACCESS_REVIEW_DB_URL.",
+ )
+ args = parser.parse_args()
+
+ repository = AccessibilityReviewRepository(db_url=args.db_url)
+ try:
+ status = repository.schema_status()
+ finally:
+ repository.engine.dispose()
+
+ ready = "ready" if status.ready else "not ready"
+ missing = ", ".join(status.missing_tables) if status.missing_tables else "none"
+ version = status.schema_version or "none"
+ print(
+ "CivicAccess schema "
+ f"{ready}: version={version}; expected={status.expected_schema_version}; "
+ f"dialect={status.dialect}; missing_tables={missing}."
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/civicaccess/main.py b/civicaccess/main.py
index d8c0e3d..0dd8393 100644
--- a/civicaccess/main.py
+++ b/civicaccess/main.py
@@ -1,18 +1,20 @@
"""FastAPI runtime foundation for CivicAccess."""
import os
+from pathlib import Path
from civiccore import __version__ as CIVICCORE_VERSION
-from fastapi import FastAPI, HTTPException
-from fastapi.responses import HTMLResponse
-from pydantic import BaseModel
+from fastapi import FastAPI, HTTPException, Request
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import HTMLResponse, JSONResponse
+from pydantic import BaseModel, Field
from civicaccess import __version__
-from civicaccess.access_review import AccessibilityReviewRepository, StoredAccessibilityReview, review_accessibility
+from civicaccess.access_review import AccessibilityReviewRepository, StoredAccessibilityReview
from civicaccess.exports import build_accessible_export
from civicaccess.multilingual import create_language_variant
from civicaccess.plain_language import rewrite_plain_language
-from civicaccess.public_ui import render_public_lookup_page
+from civicaccess.public_ui import render_public_lookup_page, render_staff_page
from civicaccess.workflows import (
build_accessible_form_plan,
build_ada_title_ii_review_plan,
@@ -32,45 +34,45 @@
class AccessibilityReviewRequest(BaseModel):
- title: str = ""
- body: str
+ title: str = Field(default="", max_length=500)
+ body: str = Field(max_length=5000)
has_alt_text: bool = False
- language: str = "en"
+ language: str = Field(default="en", min_length=1, max_length=80)
class PlainLanguageRequest(BaseModel):
- text: str
+ text: str = Field(min_length=1, max_length=5000)
class LanguageVariantRequest(BaseModel):
- text: str
- language: str
+ text: str = Field(min_length=1, max_length=5000)
+ language: str = Field(min_length=1, max_length=80)
class AccessibleExportRequest(BaseModel):
- title: str
- format: str = "html"
+ title: str = Field(min_length=1, max_length=500)
+ format: str = Field(default="html", min_length=1, max_length=40)
class AccessibleFormRequest(BaseModel):
- form_name: str = ""
- fields: list[str] = []
+ form_name: str = Field(default="", max_length=500)
+ fields: list[str] = Field(default_factory=list, max_length=100)
class PublishingWorkflowRequest(BaseModel):
- title: str = ""
+ title: str = Field(default="", max_length=500)
has_review: bool = False
has_plain_language: bool = False
has_translation_review: bool = False
class AdaTitleIiReviewRequest(BaseModel):
- service_area: str = ""
+ service_area: str = Field(default="", max_length=500)
has_coordinator_review: bool = False
class TaggedPdfExpectationRequest(BaseModel):
- heading_levels: list[int] = []
+ heading_levels: list[int] = Field(default_factory=list, max_length=200)
@app.get("/")
@@ -80,12 +82,12 @@ def root() -> dict[str, str]:
return {
"name": "CivicAccess",
"version": __version__,
- "status": "corrective demotion state",
+ "status": "standalone readiness candidate",
"message": (
- "CivicAccess is an honest v0.2.0 deterministic scaffold with accessible-form planning, publishing workflow checks, WCAG-aligned review support, optional database-backed review records, plain-language rewrites, multilingual sample variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, and a public UI. "
+ "CivicAccess provides local accessibility review support with accessible-form planning, publishing workflow checks, WCAG-aligned review records, plain-language rewrites, multilingual sample variants, ADA Title II review-support packages, tagged-PDF expectations, records-ready export checklists, and API-backed public and staff interfaces. "
"It does not provide legal advice, certified ADA compliance, official translation certification, live LLM calls, or final publication approval."
),
- "next_step": "Use CivicAccess for local review support only; do not treat it as finished or public-use ready until a future independent audit signs off against the full CivicSuite Section 2 gate.",
+ "next_step": "Open /civicaccess/staff to review saved publication work and export records-ready packages.",
}
@@ -101,6 +103,47 @@ def health() -> dict[str, str]:
}
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(
+ _request: Request, exc: RequestValidationError
+) -> JSONResponse:
+ fields = sorted(
+ {
+ ".".join(str(part) for part in error.get("loc", [])[1:])
+ for error in exc.errors()
+ if len(error.get("loc", [])) > 1
+ }
+ )
+ field_list = ", ".join(fields) if fields else "request body"
+ return JSONResponse(
+ status_code=422,
+ content={
+ "detail": {
+ "message": f"CivicAccess could not validate: {field_list}.",
+ "fix": (
+ "Send a JSON body that includes the required field names listed in "
+ "the fields array, using strings for text inputs and booleans for yes/no inputs."
+ ),
+ "fields": fields,
+ }
+ },
+ )
+
+
+@app.get("/ready")
+def ready() -> dict[str, object]:
+ """Return public-use readiness for local review-record persistence."""
+
+ return _readiness_payload()
+
+
+@app.get("/api/v1/civicaccess/readiness")
+def readiness() -> dict[str, object]:
+ """Return detailed CivicAccess local persistence readiness for operators."""
+
+ return _readiness_payload()
+
+
@app.get("/civicaccess", response_class=HTMLResponse)
def public_civicaccess_page() -> str:
"""Return the accessible public sample UI."""
@@ -108,42 +151,50 @@ def public_civicaccess_page() -> str:
return render_public_lookup_page()
+@app.get("/civicaccess/staff", response_class=HTMLResponse)
+def staff_civicaccess_page() -> str:
+ """Return the staff publication review workspace."""
+
+ return render_staff_page()
+
+
@app.post("/api/v1/civicaccess/review")
def accessibility_review(request: AccessibilityReviewRequest) -> dict[str, object]:
- if _review_database_url() is not None:
- stored = _get_review_repository().create_review(
- title=request.title,
- body=request.body,
- has_alt_text=request.has_alt_text,
- language=request.language,
- )
- return _stored_review_response(stored)
-
- result = review_accessibility(
+ stored = _get_review_repository().create_review(
title=request.title,
body=request.body,
has_alt_text=request.has_alt_text,
language=request.language,
)
+ return _stored_review_response(stored)
+
+
+@app.get("/api/v1/civicaccess/reviews")
+def list_accessibility_reviews(limit: int = 25) -> dict[str, object]:
+ reviews = _get_review_repository().list_reviews(limit=limit)
return {
- "status": result.status,
- "findings": [finding.__dict__ for finding in result.findings],
- "disclaimer": result.disclaimer,
- "next_steps": list(result.next_steps),
- "review_id": None,
+ "status": "ok",
+ "count": len(reviews),
+ "reviews": [_stored_review_summary(review) for review in reviews],
}
@app.get("/api/v1/civicaccess/reviews/{review_id}")
def get_accessibility_review(review_id: str) -> dict[str, object]:
- if _review_database_url() is None:
+ stored = _get_review_repository().get_review(review_id)
+ if stored is None:
raise HTTPException(
- status_code=503,
+ status_code=404,
detail={
- "message": "CivicAccess review persistence is not configured.",
- "fix": "Set CIVICACCESS_REVIEW_DB_URL to retrieve persisted accessibility review records.",
+ "message": "Accessibility review record not found.",
+ "fix": "Use a review_id returned by POST /api/v1/civicaccess/review.",
},
)
+ return _stored_review_response(stored)
+
+
+@app.post("/api/v1/civicaccess/reviews/{review_id}/records-export")
+def export_accessibility_review_record(review_id: str) -> dict[str, object]:
stored = _get_review_repository().get_review(review_id)
if stored is None:
raise HTTPException(
@@ -153,7 +204,56 @@ def get_accessibility_review(review_id: str) -> dict[str, object]:
"fix": "Use a review_id returned by POST /api/v1/civicaccess/review.",
},
)
- return _stored_review_response(stored)
+ export = build_accessible_export(title=stored.title or "Untitled accessible publication")
+ return {
+ "status": "records-export-ready",
+ "module": "civicaccess",
+ "target_module": "civicrecords-ai",
+ "review": _stored_review_summary(stored),
+ "export": {
+ "title": export.title,
+ "format": export.format,
+ "checklist": list(export.checklist),
+ "retention_note": export.retention_note,
+ "status": export.status,
+ "fix": export.fix,
+ },
+ "provenance": {
+ "source_text_preserved": True,
+ "findings_preserved": True,
+ "disclaimer_preserved": True,
+ "created_at": stored.created_at.isoformat(),
+ },
+ }
+
+
+@app.get("/api/v1/civicaccess/integration-contracts")
+def integration_contracts() -> dict[str, object]:
+ return {
+ "status": "ok",
+ "module": "civicaccess",
+ "provides": [
+ {
+ "contract": "civicaccess.publication_accessibility_review.v1",
+ "endpoint": "/api/v1/civicaccess/review",
+ "purpose": "Create a persisted accessibility review before publication.",
+ },
+ {
+ "contract": "civicaccess.records_export.v1",
+ "endpoint": "/api/v1/civicaccess/reviews/{review_id}/records-export",
+ "target_module": "civicrecords-ai",
+ "purpose": "Export review provenance and checklist data for records retention.",
+ },
+ ],
+ "downstream_ready_for": [
+ "civiczone public notice text",
+ "civicplan policy summaries",
+ "civicpermit applicant forms",
+ "civicinspect notices",
+ "civicgrants public opportunity notices",
+ "civicprocure RFP and award packets",
+ ],
+ }
@app.post("/api/v1/civicaccess/plain-language")
@@ -237,14 +337,17 @@ def accessible_export(request: AccessibleExportRequest) -> dict[str, object]:
def _review_database_url() -> str | None:
- return os.environ.get("CIVICACCESS_REVIEW_DB_URL")
+ configured = os.environ.get("CIVICACCESS_REVIEW_DB_URL")
+ if configured:
+ return configured
+ data_dir = Path(os.environ.get("CIVICACCESS_DATA_DIR", Path.cwd() / "data")).resolve()
+ data_dir.mkdir(parents=True, exist_ok=True)
+ return f"sqlite:///{data_dir / 'civicaccess-reviews.db'}"
def _get_review_repository() -> AccessibilityReviewRepository:
global _review_db_url, _review_repository
db_url = _review_database_url()
- if db_url is None:
- raise RuntimeError("CIVICACCESS_REVIEW_DB_URL is not configured.")
if _review_repository is None or db_url != _review_db_url:
_dispose_review_repository()
_review_db_url = db_url
@@ -252,6 +355,27 @@ def _get_review_repository() -> AccessibilityReviewRepository:
return _review_repository
+def _readiness_payload() -> dict[str, object]:
+ db_url = _review_database_url()
+ repository = _get_review_repository()
+ schema_status = repository.schema_status()
+ blockers: list[str] = []
+ if not schema_status.ready:
+ blockers.append("Run the local CivicAccess schema status/migration check.")
+ ready_for_public_use = not blockers
+ return {
+ "status": "ready" if ready_for_public_use else "not-ready",
+ "ready": ready_for_public_use,
+ "review_database_configured": True,
+ "review_database_url": db_url,
+ "schema_ready": schema_status.ready,
+ "schema_version": schema_status.schema_version,
+ "expected_schema_version": schema_status.expected_schema_version,
+ "review_count": repository.review_count(),
+ "blockers": blockers,
+ }
+
+
def _dispose_review_repository() -> None:
global _review_repository
if _review_repository is not None:
@@ -274,3 +398,14 @@ def _stored_review_response(stored: StoredAccessibilityReview) -> dict[str, obje
"language": stored.language,
"created_at": stored.created_at.isoformat(),
}
+
+
+def _stored_review_summary(stored: StoredAccessibilityReview) -> dict[str, object]:
+ return {
+ "review_id": stored.review_id,
+ "title": stored.title,
+ "language": stored.language,
+ "status": stored.status,
+ "finding_count": len(stored.findings),
+ "created_at": stored.created_at.isoformat(),
+ }
diff --git a/civicaccess/public_ui.py b/civicaccess/public_ui.py
index 8ce2d5b..f301410 100644
--- a/civicaccess/public_ui.py
+++ b/civicaccess/public_ui.py
@@ -1,10 +1,10 @@
-"""Static public UI for CivicAccess v0.2.0."""
+"""Public UI for CivicAccess v0.3.0."""
from __future__ import annotations
def render_public_lookup_page() -> str:
- """Render the accessible public-facing CivicAccess page."""
+ """Render the accessible public-facing CivicAccess review page."""
return """
@@ -53,7 +53,7 @@ def render_public_lookup_page() -> str:
CivicSuite / CivicAccess
Make public information easier to read, reach, and preserve.
CivicAccess gives staff a deterministic review path for accessible forms, public notices, plain-language rewrites, multilingual samples, ADA Title II review support, tagged-PDF expectations, and municipal-record exports.
- v0.2.0 corrective demotion state
+ v0.3.0 standalone readiness candidate
@@ -119,37 +119,230 @@ def render_public_lookup_page() -> str:
if (initialParams.has("notice")) notice.value = initialParams.get("notice");
if (initialParams.get("alt") === "1") altText.checked = true;
+ function appendText(tagName, text) {
+ const node = document.createElement(tagName);
+ node.textContent = text;
+ result.appendChild(node);
+ return node;
+ }
+
function setResult(kind, heading, body, items) {
result.className = "result" + (kind ? " " + kind : "");
- const list = items && items.length ? "" + items.map((item) => "- " + item + "
").join("") + "
" : "";
- result.innerHTML = "" + heading + "
" + body + "
" + list;
+ result.replaceChildren();
+ appendText("h3", heading);
+ appendText("p", body);
+ if (items && items.length) {
+ const list = document.createElement("ul");
+ for (const item of items) {
+ const listItem = document.createElement("li");
+ listItem.textContent = item;
+ list.appendChild(listItem);
+ }
+ result.appendChild(list);
+ }
}
- runReview.addEventListener("click", () => {
+ runReview.addEventListener("click", async () => {
setResult("pending", "Loading review", "Checking the notice text and publication fields.", []);
- window.setTimeout(() => {
- try {
- const params = new URLSearchParams(window.location.search);
- if (params.get("force_error") === "1") {
- throw new Error("forced qa error");
- }
- const fixes = [];
- if (!title.value.trim()) fixes.push("Add a short title that names the service, deadline, or public action.");
- if (!notice.value.trim()) fixes.push("Add the resident-facing notice text before publication review.");
- if (!altText.checked) fixes.push("Add alt text for non-decorative images or mark decorative images as decorative.");
- if (fixes.length === 0) {
- fixes.push("Add a plain-language summary and preserve the source text with the record.");
- setResult("", "Sample checks passed", "Staff review is still required before publication.", fixes);
- } else if (!title.value.trim() && !notice.value.trim()) {
- setResult("warning", "No notice text yet", "Add a title, resident-facing notice text, and image context before running the review.", fixes);
- } else {
- fixes.push("Add a plain-language summary and preserve the source text with the record.");
- setResult("warning", "Needs fixes", "Resolve these items before staff publication approval.", fixes);
- }
- } catch (error) {
- setResult("warning", "Review could not finish", "Check that the notice text is present, then run the review again. If this happens in the installed stack, staff should check the CivicAccess service health endpoint.", []);
+ runReview.disabled = true;
+ try {
+ const response = await fetch("/api/v1/civicaccess/review", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ title: title.value,
+ body: notice.value,
+ has_alt_text: altText.checked,
+ language: "en",
+ }),
+ });
+ const payload = await response.json();
+ if (!response.ok) {
+ const detail = payload.detail || {};
+ throw new Error([detail.message, detail.fix].filter(Boolean).join(" "));
}
- }, 250);
+ const fixes = (payload.findings || []).map((finding) => finding.fix);
+ if (payload.status === "passes-sample-checks") {
+ setResult("", "Sample checks passed", "Staff review is still required before publication.", payload.next_steps || []);
+ } else if (fixes.length) {
+ setResult("warning", "Needs fixes", "Resolve these items before staff publication approval.", fixes);
+ } else {
+ setResult("warning", "Review needs staff attention", payload.disclaimer || "Staff review is required before publication.", []);
+ }
+ } catch (error) {
+ setResult("warning", "Review could not finish", error.message || "Check that the notice text is present, then run the review again. If this happens in the installed stack, staff should check the CivicAccess service health endpoint.", []);
+ } finally {
+ runReview.disabled = false;
+ }
+ });
+
+
+